From f2f6fb31879bec207dce307b25cec76b3a1f6f26 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Mon, 6 Feb 2017 22:28:28 +0100 Subject: [PATCH 01/62] supports newest jcsg (now without dynamic version syntax, since maven central does not support it) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 110d31bb..576bd381 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group = eu.mihosoft.vrl.jcsg -version = 0.5.2 +version = 0.5.3-SNAPSHOT From e6d6b3c35a479fde53fe5a7c9b6befe10b1f7028 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Tue, 7 Feb 2017 19:35:53 +0100 Subject: [PATCH 02/62] plane changed --- src/main/java/eu/mihosoft/jcsg/Edge.java | 4 +-- src/main/java/eu/mihosoft/jcsg/Node.java | 4 +-- src/main/java/eu/mihosoft/jcsg/Plane.java | 2 +- src/main/java/eu/mihosoft/jcsg/Polygon.java | 34 +++++++++++++-------- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/main/java/eu/mihosoft/jcsg/Edge.java b/src/main/java/eu/mihosoft/jcsg/Edge.java index ac3d72ad..71e86bf2 100644 --- a/src/main/java/eu/mihosoft/jcsg/Edge.java +++ b/src/main/java/eu/mihosoft/jcsg/Edge.java @@ -716,8 +716,8 @@ private static List> searchPlaneGroups(List polygons) { continue; } - Vector3d nOuter = pOuter.plane.normal; - Vector3d nInner = pInner.plane.normal; + Vector3d nOuter = pOuter._csg_plane.normal; + Vector3d nInner = pInner._csg_plane.normal; // TODO do we need radians or degrees? double angle = nOuter.angle(nInner); diff --git a/src/main/java/eu/mihosoft/jcsg/Node.java b/src/main/java/eu/mihosoft/jcsg/Node.java index c88f2862..c5d91cb1 100644 --- a/src/main/java/eu/mihosoft/jcsg/Node.java +++ b/src/main/java/eu/mihosoft/jcsg/Node.java @@ -128,7 +128,7 @@ public void invert() { }); if (this.plane == null && !polygons.isEmpty()) { - this.plane = polygons.get(0).plane.clone(); + this.plane = polygons.get(0)._csg_plane.clone(); } else if (this.plane == null && polygons.isEmpty()) { System.err.println("Please fix me! I don't know what to do?"); @@ -238,7 +238,7 @@ public final void build(List polygons) { if (polygons.isEmpty()) return; if (this.plane == null) { - this.plane = polygons.get(0).plane.clone(); + this.plane = polygons.get(0)._csg_plane.clone(); } polygons = polygons.stream().filter(p->p.isValid()).distinct().collect(Collectors.toList()); diff --git a/src/main/java/eu/mihosoft/jcsg/Plane.java b/src/main/java/eu/mihosoft/jcsg/Plane.java index 3ef82165..609cde4c 100644 --- a/src/main/java/eu/mihosoft/jcsg/Plane.java +++ b/src/main/java/eu/mihosoft/jcsg/Plane.java @@ -152,7 +152,7 @@ public void splitPolygon( switch (polygonType) { case COPLANAR: //System.out.println(" -> coplanar"); - (this.normal.dot(polygon.plane.normal) > 0 ? coplanarFront : coplanarBack).add(polygon); + (this.normal.dot(polygon._csg_plane.normal) > 0 ? coplanarFront : coplanarBack).add(polygon); break; case FRONT: //System.out.println(" -> front"); diff --git a/src/main/java/eu/mihosoft/jcsg/Polygon.java b/src/main/java/eu/mihosoft/jcsg/Polygon.java index e820a0db..58c12e60 100644 --- a/src/main/java/eu/mihosoft/jcsg/Polygon.java +++ b/src/main/java/eu/mihosoft/jcsg/Polygon.java @@ -64,8 +64,10 @@ public final class Polygon { * * Note: uses first three vertices to define the plane. */ - public final Plane plane; - + public final Plane _csg_plane; + public final eu.mihosoft.vvecmath.Plane plane; + + void setStorage(PropertyStorage storage) { this.shared = storage; } @@ -117,7 +119,11 @@ public boolean isValid() { public Polygon(List vertices, PropertyStorage shared) { this.vertices = vertices; this.shared = shared; - this.plane = Plane.createFromPoints( + this._csg_plane = Plane.createFromPoints( + vertices.get(0).pos, + vertices.get(1).pos, + vertices.get(2).pos); + this.plane = eu.mihosoft.vvecmath.Plane.fromPoints( vertices.get(0).pos, vertices.get(1).pos, vertices.get(2).pos); @@ -127,9 +133,9 @@ public Polygon(List vertices, PropertyStorage shared) { private void validateAndInit(List vertices1) { for (Vertex v : vertices1) { - v.normal = plane.normal; + v.normal = _csg_plane.normal; } - if (Vector3d.ZERO.equals(plane.normal)) { + if (Vector3d.ZERO.equals(_csg_plane.normal)) { valid = false; System.err.println( "Normal is zero! Probably, duplicate points have been specified!\n\n"+toStlString()); @@ -155,7 +161,12 @@ private void validateAndInit(List vertices1) { */ public Polygon(List vertices) { this.vertices = vertices; - this.plane = Plane.createFromPoints( + this._csg_plane = Plane.createFromPoints( + vertices.get(0).pos, + vertices.get(1).pos, + vertices.get(2).pos); + + this.plane = eu.mihosoft.vvecmath.Plane.fromPoints( vertices.get(0).pos, vertices.get(1).pos, vertices.get(2).pos); @@ -197,7 +208,7 @@ public Polygon flip() { }); Collections.reverse(vertices); - plane.flip(); + _csg_plane.flip(); return this; } @@ -241,8 +252,7 @@ public StringBuilder toStlString(StringBuilder sb) { String firstVertexStl = this.vertices.get(0).toStlString(); for (int i = 0; i < this.vertices.size() - 2; i++) { sb. - append(" facet normal ").append( - this.plane.normal.toStlString()).append("\n"). + append(" facet normal ").append(this._csg_plane.normal.toStlString()).append("\n"). append(" outer loop\n"). append(" ").append(firstVertexStl).append("\n"). append(" "); @@ -272,7 +282,7 @@ public Polygon translate(Vector3d v) { Vector3d b = this.vertices.get(1).pos; Vector3d c = this.vertices.get(2).pos; - this.plane.normal = b.minus(a).cross(c.minus(a)); + this._csg_plane.normal = b.minus(a).cross(c.minus(a)); return this; } @@ -312,8 +322,8 @@ public Polygon transform(Transform transform) { Vector3d b = this.vertices.get(1).pos; Vector3d c = this.vertices.get(2).pos; - this.plane.normal = b.minus(a).cross(c.minus(a)).normalized(); - this.plane.dist = this.plane.normal.dot(a); + this._csg_plane.normal = b.minus(a).cross(c.minus(a)).normalized(); + this._csg_plane.dist = this._csg_plane.normal.dot(a); if (transform.isMirror()) { // the transformation includes mirroring. flip polygon From 8a7a43958a186583fd122f63112aa1693c1a0985 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Tue, 7 Feb 2017 19:36:10 +0100 Subject: [PATCH 03/62] playground for new renderer --- .../eu/mihosoft/jcsg/playground/Main.java | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/main/java/eu/mihosoft/jcsg/playground/Main.java diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main.java b/src/main/java/eu/mihosoft/jcsg/playground/Main.java new file mode 100644 index 00000000..327b9319 --- /dev/null +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main.java @@ -0,0 +1,152 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package eu.mihosoft.jcsg.playground; + +import eu.mihosoft.jcsg.Bounds; +import eu.mihosoft.jcsg.CSG; +import eu.mihosoft.jcsg.Polygon; +import eu.mihosoft.jcsg.Sphere; +import eu.mihosoft.jcsg.Vertex; +import eu.mihosoft.vvecmath.Plane; +import eu.mihosoft.vvecmath.Vector3d; +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author Michael Hoffer (info@michaelhoffer.de) + */ +public class Main { + + public static final double EPS = 1e-8; + + public static void main(String[] args) { + + CSG c1 = new Sphere(Vector3d.zero(), 1.0, 32, 32).toCSG(); + CSG c2 = new Sphere(Vector3d.x(0.75), 1.0, 32, 32).toCSG(); + + SplitResult result = splitPolygons(c1.getPolygons(), c2.getPolygons()); + + } + + public static SplitResult splitPolygons(List ps1, List ps2, Bounds b1, Bounds b2) { + SplitResult result = new SplitResult(); + + List ps2WithCuts = new ArrayList<>(ps2); + + for (Polygon p1 : ps1) { + + // return early if polygon bounds do not intersect object bounds + if (p1.getBounds().intersects(b2)) { + continue; + } + + List cutsWithP1 = new ArrayList<>(); + for (Polygon p2 : ps2WithCuts) { + + // return early if polygon bounds do not intersect other polygon bound + if (!p1.getBounds().intersects(p2.getBounds())) { + continue; + } + +// // compute distance of polygon 1 vertices to polygon 2 plane +// int[] typesP1 = new int[p1.vertices.size()]; +// +// boolean typesEqual1 = true; +// int prevType1 = 0; +// for (int i = 0; i < p1.vertices.size(); i++) { +// typesP1[i] = p2.plane.compare(p1.vertices.get(i).pos, EPS); +// +// if (i > 0 && typesEqual1) { +// typesEqual1 &= prevType1 == typesP1[i]; +// } +// +// prevType1 = typesP1[i]; +// } +// +// // planes do not intersect, thus polygons do not intersect +// if (typesEqual1) { +// continue; +// } + + // compute distance of polygon 2 vertices to polygon 1 plane + int[] typesP2 = new int[p2.vertices.size()]; + + boolean typesEqual2 = true; + int prevType2 = 0; + + for (int i = 0; i < p2.vertices.size(); i++) { + typesP2[i] = p1.plane.compare(p2.vertices.get(i).pos, EPS); + + if (i > 0 && typesEqual2) { + typesEqual2 &= prevType2 == typesP2[i]; + } + + prevType2 = typesP2[i]; + } + + // planes do not intersect, thus polygons do not intersect + if (typesEqual2) { + continue; + } + + // cut p2 with plane1 + List frontPolygon = new ArrayList<>(); + List b = new ArrayList<>(); + for (int i = 0; i < p2.vertices.size(); i++) { + int j = (i + 1) % p2.vertices.size(); + int ti = typesP2[i]; + int tj = typesP2[j]; + Vertex vi = p2.vertices.get(i); + Vertex vj = p2.vertices.get(j); + if (ti != -1 /*front*/) { + frontPolygon.add(vi.pos); + } + if (ti != 1 /*back*/) { + b.add(ti != BACK ? vi.clone() : vi); + } + if (ti !=0 && tj != 1 && ti!=tj /*spanning*/) { + double t = (p1.plane.getDist() - p1.plane.normal.dot(vi.pos)) + / p1.plane.normal.dot(vj.pos.minus(vi.pos)); + Vertex v = vi.interpolate(vj, t); + frontPolygon.add(v); + b.add(v.clone()); + } + } + + cutsWithP1.add(Polygon.fromPoints(frontPolygon)); + + if (frontPolygon.size() >= 3) { + front.add(new Polygon(f, polygon.getStorage())); + } + if (b.size() >= 3) { + back.add(new Polygon(b, polygon.getStorage())); + } + + + + } + } + + return result; + } + + public static class SplitResult { + + List inside1 = new ArrayList<>(); + List inside2 = new ArrayList<>(); + List outside = new ArrayList<>(); + + } + + enum PolygonType { + UNKNOWN, + INSIDE_1, + INSIDE_2, + OUTSIDE, + OPPOSITE + } +} From ea8ca8647bee1ba1be94ac0754b9a27a0f7e0972 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Tue, 7 Feb 2017 19:41:17 +0100 Subject: [PATCH 04/62] use vvecmath snapshot --- build.gradle | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index bde50443..c094c281 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,8 @@ mainClassName = mainClass repositories { mavenCentral() jcenter() + + mavenLocal() } // javadoc is way too strict for my taste. @@ -62,8 +64,8 @@ dependencies { compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4' compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4', classifier: 'sources' - compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.2' - compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.2', classifier: 'sources' + compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.3-SNAPSHOT' + compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.3-SNAPSHOT', classifier: 'sources' compile 'org.slf4j:slf4j-simple:1.6.1' } From 938de1f857b416b1855bf86ed9cf4a904d6cba58 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Tue, 7 Feb 2017 23:11:41 +0100 Subject: [PATCH 05/62] splitting works (but more cases should be excluded) --- .../eu/mihosoft/jcsg/playground/Main.java | 104 ++++++++++-------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main.java b/src/main/java/eu/mihosoft/jcsg/playground/Main.java index 327b9319..ab1f9ee9 100644 --- a/src/main/java/eu/mihosoft/jcsg/playground/Main.java +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main.java @@ -10,8 +10,10 @@ import eu.mihosoft.jcsg.Polygon; import eu.mihosoft.jcsg.Sphere; import eu.mihosoft.jcsg.Vertex; -import eu.mihosoft.vvecmath.Plane; import eu.mihosoft.vvecmath.Vector3d; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; @@ -23,28 +25,41 @@ public class Main { public static final double EPS = 1e-8; - public static void main(String[] args) { + public static void main(String[] args) throws IOException { CSG c1 = new Sphere(Vector3d.zero(), 1.0, 32, 32).toCSG(); CSG c2 = new Sphere(Vector3d.x(0.75), 1.0, 32, 32).toCSG(); - SplitResult result = splitPolygons(c1.getPolygons(), c2.getPolygons()); + SplitResult result1 = splitPolygons(c1.getPolygons(), c2.getPolygons(), + c1.getBounds(), c2.getBounds()); + SplitResult result2 = splitPolygons(c2.getPolygons(), c1.getPolygons(), + c2.getBounds(), c1.getBounds()); + + result1.p2.addAll(result2.p2); + + CSG result = CSG.fromPolygons(result1.p2); + + Files.write(Paths.get("test.stl"), result.toStlString().getBytes()); } - public static SplitResult splitPolygons(List ps1, List ps2, Bounds b1, Bounds b2) { - SplitResult result = new SplitResult(); + public static SplitResult splitPolygons( + List ps1, + List ps2, + Bounds b1, Bounds b2) { + SplitResult result = new SplitResult(); List ps2WithCuts = new ArrayList<>(ps2); - + for (Polygon p1 : ps1) { // return early if polygon bounds do not intersect object bounds - if (p1.getBounds().intersects(b2)) { + if (!p1.getBounds().intersects(b2)) { continue; } - + List cutsWithP1 = new ArrayList<>(); + List p2ToDelete = new ArrayList<>(); for (Polygon p2 : ps2WithCuts) { // return early if polygon bounds do not intersect other polygon bound @@ -52,26 +67,25 @@ public static SplitResult splitPolygons(List ps1, List ps2, Bo continue; } -// // compute distance of polygon 1 vertices to polygon 2 plane -// int[] typesP1 = new int[p1.vertices.size()]; -// -// boolean typesEqual1 = true; -// int prevType1 = 0; -// for (int i = 0; i < p1.vertices.size(); i++) { -// typesP1[i] = p2.plane.compare(p1.vertices.get(i).pos, EPS); -// -// if (i > 0 && typesEqual1) { -// typesEqual1 &= prevType1 == typesP1[i]; -// } -// -// prevType1 = typesP1[i]; -// } -// -// // planes do not intersect, thus polygons do not intersect -// if (typesEqual1) { -// continue; -// } + // compute distance of polygon 1 vertices to polygon 2 plane + int[] typesP1 = new int[p1.vertices.size()]; + + boolean typesEqual1 = true; + int prevType1 = 0; + for (int i = 0; i < p1.vertices.size(); i++) { + typesP1[i] = p2.plane.compare(p1.vertices.get(i).pos, EPS); + + if (i > 0 && typesEqual1) { + typesEqual1 &= prevType1 == typesP1[i]; + } + + prevType1 = typesP1[i]; + } + // planes do not intersect, thus polygons do not intersect + if (typesEqual1) { + continue; + } // compute distance of polygon 2 vertices to polygon 1 plane int[] typesP2 = new int[p2.vertices.size()]; @@ -93,9 +107,11 @@ public static SplitResult splitPolygons(List ps1, List ps2, Bo continue; } + p2ToDelete.add(p2); + // cut p2 with plane1 List frontPolygon = new ArrayList<>(); - List b = new ArrayList<>(); + List backPolygon = new ArrayList<>(); for (int i = 0; i < p2.vertices.size(); i++) { int j = (i + 1) % p2.vertices.size(); int ti = typesP2[i]; @@ -106,39 +122,37 @@ public static SplitResult splitPolygons(List ps1, List ps2, Bo frontPolygon.add(vi.pos); } if (ti != 1 /*back*/) { - b.add(ti != BACK ? vi.clone() : vi); + backPolygon.add(vi.pos); } - if (ti !=0 && tj != 1 && ti!=tj /*spanning*/) { + if (ti != tj && (ti != 0 && tj != 0)/*spanning*/) { double t = (p1.plane.getDist() - p1.plane.normal.dot(vi.pos)) / p1.plane.normal.dot(vj.pos.minus(vi.pos)); Vertex v = vi.interpolate(vj, t); - frontPolygon.add(v); - b.add(v.clone()); + frontPolygon.add(v.pos); + backPolygon.add(v.pos); } } - - cutsWithP1.add(Polygon.fromPoints(frontPolygon)); - - if (frontPolygon.size() >= 3) { - front.add(new Polygon(f, polygon.getStorage())); + + if (frontPolygon.size() > 2) { + cutsWithP1.add(Polygon.fromPoints(frontPolygon, p1.getStorage())); } - if (b.size() >= 3) { - back.add(new Polygon(b, polygon.getStorage())); + if (backPolygon.size() > 2) { + cutsWithP1.add(Polygon.fromPoints(backPolygon, p1.getStorage())); } - - - } + ps2WithCuts.addAll(cutsWithP1); + ps2WithCuts.removeAll(p2ToDelete); } + result.p1 = ps1; + result.p2 = ps2WithCuts; return result; } public static class SplitResult { - List inside1 = new ArrayList<>(); - List inside2 = new ArrayList<>(); - List outside = new ArrayList<>(); + List p1 = new ArrayList<>(); + List p2 = new ArrayList<>(); } From 185b9716728d8d71cab1903dd907eaad5f38d365 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Wed, 8 Feb 2017 13:25:12 +0100 Subject: [PATCH 06/62] improved polygon splitting --- src/main/java/eu/mihosoft/jcsg/Polygon.java | 100 +++++++++++++----- .../eu/mihosoft/jcsg/playground/Main.java | 23 +++- 2 files changed, 92 insertions(+), 31 deletions(-) diff --git a/src/main/java/eu/mihosoft/jcsg/Polygon.java b/src/main/java/eu/mihosoft/jcsg/Polygon.java index 58c12e60..8069e3e2 100644 --- a/src/main/java/eu/mihosoft/jcsg/Polygon.java +++ b/src/main/java/eu/mihosoft/jcsg/Polygon.java @@ -66,8 +66,7 @@ public final class Polygon { */ public final Plane _csg_plane; public final eu.mihosoft.vvecmath.Plane plane; - - + void setStorage(PropertyStorage storage) { this.shared = storage; } @@ -97,15 +96,16 @@ public static List fromConcavePoints(List points) { } /** - * Indicates whether this polyon is valid, i.e., if it - * @return + * Indicates whether this polyon is valid, i.e., if it + * + * @return */ public boolean isValid() { return valid; } - + private boolean valid = true; - + /** * Constructor. Creates a new polygon that consists of the specified * vertices. @@ -127,7 +127,7 @@ public Polygon(List vertices, PropertyStorage shared) { vertices.get(0).pos, vertices.get(1).pos, vertices.get(2).pos); - + validateAndInit(vertices); } @@ -138,15 +138,15 @@ private void validateAndInit(List vertices1) { if (Vector3d.ZERO.equals(_csg_plane.normal)) { valid = false; System.err.println( - "Normal is zero! Probably, duplicate points have been specified!\n\n"+toStlString()); + "Normal is zero! Probably, duplicate points have been specified!\n\n" + toStlString()); // throw new RuntimeException( // "Normal is zero! Probably, duplicate points have been specified!\n\n"+toStlString()); - } - - if(vertices.size()<3) { + } + + if (vertices.size() < 3) { throw new RuntimeException( "Invalid polygon: at least 3 vertices expected, got: " - + vertices.size()); + + vertices.size()); } } @@ -165,12 +165,12 @@ public Polygon(List vertices) { vertices.get(0).pos, vertices.get(1).pos, vertices.get(2).pos); - + this.plane = eu.mihosoft.vvecmath.Plane.fromPoints( vertices.get(0).pos, vertices.get(1).pos, vertices.get(2).pos); - + validateAndInit(vertices); } @@ -459,25 +459,69 @@ public Bounds getBounds() { public boolean contains(Vector3d p) { // taken from http://www.java-gaming.org/index.php?topic=26013.0 + // http://alienryderflex.com/polygon/ // and http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html - double px = p.x(); - double py = p.y(); + + // P not on the plane + if (plane.distance(p) > Plane.EPSILON) { + return false; + } + + // if P is on the plane, we proceed with projection to XY plane + + // + // P1--P------P2 + // ^ + // | + // P is on the segment if( dist(P1,P) + dist(P2,P) - dist(P1,P2) < TOL) + boolean onASegment = false; + for (int i = 0; i < vertices.size() - 1; i++) { + + Vector3d p1 = vertices.get(i).pos; + Vector3d p2 = vertices.get(i + 1).pos; + + onASegment = p1.minus(p).magnitude() + p2.minus(p).magnitude() + - p1.minus(p2).magnitude() < Plane.EPSILON; + + if(onASegment) return true; + } + +// double px = p.x(); +// double py = p.y(); +// boolean oddNodes = false; +// double x2 = vertices.get(vertices.size() - 1).pos.x(); +// double y2 = vertices.get(vertices.size() - 1).pos.y(); +// double x1, y; +// for (int i = 0; i < vertices.size(); x2 = x1, y2 = y, ++i) { +// x1 = vertices.get(i).pos.x(); +// y = vertices.get(i).pos.y(); +// if (((y > py) && (y2 >= py)) +// || (y >= py) && (y2 < py)) { +// if ((py - y) / (y2 - y) +// * (x2 - x1) < (px - x1)) { +// oddNodes = !oddNodes; +// } +// } +// } +// return oddNodes; + int i, j = vertices.size() - 1; boolean oddNodes = false; - double x2 = vertices.get(vertices.size() - 1).pos.x(); - double y2 = vertices.get(vertices.size() - 1).pos.y(); - double x1, y1; - for (int i = 0; i < vertices.size(); x2 = x1, y2 = y1, ++i) { - x1 = vertices.get(i).pos.x(); - y1 = vertices.get(i).pos.y(); - if (((y1 < py) && (y2 >= py)) - || (y1 >= py) && (y2 < py)) { - if ((py - y1) / (y2 - y1) - * (x2 - x1) < (px - x1)) { - oddNodes = !oddNodes; - } + double x = p.x(); + double y = p.y(); + for (i = 0; i < vertices.size(); i++) { + double xi = vertices.get(i).pos.getX(); + double yi = vertices.get(i).pos.getY(); + double xj = vertices.get(j).pos.getX(); + double yj = vertices.get(j).pos.getY(); + if ((yi < y && yj >= y + || yj < y && yi >= y) + && (xi <= x || xj <= x)) { + oddNodes ^= (xi + (y - yi) / (yj - yi) * (xj - xi) < x); } + j = i; } return oddNodes; + } public boolean contains(Polygon p) { diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main.java b/src/main/java/eu/mihosoft/jcsg/playground/Main.java index ab1f9ee9..984543b3 100644 --- a/src/main/java/eu/mihosoft/jcsg/playground/Main.java +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main.java @@ -106,10 +106,9 @@ public static SplitResult splitPolygons( if (typesEqual2) { continue; } - - p2ToDelete.add(p2); // cut p2 with plane1 + List segmentPoints = new ArrayList<>(); List frontPolygon = new ArrayList<>(); List backPolygon = new ArrayList<>(); for (int i = 0; i < p2.vertices.size(); i++) { @@ -124,15 +123,33 @@ public static SplitResult splitPolygons( if (ti != 1 /*back*/) { backPolygon.add(vi.pos); } + if (ti != tj && (ti != 0 && tj != 0)/*spanning*/) { double t = (p1.plane.getDist() - p1.plane.normal.dot(vi.pos)) / p1.plane.normal.dot(vj.pos.minus(vi.pos)); Vertex v = vi.interpolate(vj, t); frontPolygon.add(v.pos); backPolygon.add(v.pos); + segmentPoints.add(v.pos); + } + } + + // check whether the segments are contained in the polygons + boolean p1ContainsIntersectionPointsOfP2WithPlane = true; + + for (Vector3d fv : segmentPoints) { + if (!p1.contains(fv)) { + p1ContainsIntersectionPointsOfP2WithPlane = false; + break; } } + if (!(p1ContainsIntersectionPointsOfP2WithPlane)) { + continue; + } + + p2ToDelete.add(p2); + if (frontPolygon.size() > 2) { cutsWithP1.add(Polygon.fromPoints(frontPolygon, p1.getStorage())); } @@ -141,7 +158,7 @@ public static SplitResult splitPolygons( } } ps2WithCuts.addAll(cutsWithP1); - ps2WithCuts.removeAll(p2ToDelete); + ps2WithCuts.removeAll(p2ToDelete); } result.p1 = ps1; result.p2 = ps2WithCuts; From be9068ccec858bc4c3239564539c43e3e8f0c8e3 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Thu, 9 Feb 2017 00:36:44 +0100 Subject: [PATCH 07/62] improved polygon splitting --- src/main/java/eu/mihosoft/jcsg/Polygon.java | 69 +-- .../eu/mihosoft/jcsg/playground/Main.java | 432 +++++++++++++++--- 2 files changed, 399 insertions(+), 102 deletions(-) diff --git a/src/main/java/eu/mihosoft/jcsg/Polygon.java b/src/main/java/eu/mihosoft/jcsg/Polygon.java index 8069e3e2..8f9afbcb 100644 --- a/src/main/java/eu/mihosoft/jcsg/Polygon.java +++ b/src/main/java/eu/mihosoft/jcsg/Polygon.java @@ -456,54 +456,60 @@ public Bounds getBounds() { Vector3d.xyz(minX, minY, minZ), Vector3d.xyz(maxX, maxY, maxZ)); } + + public Vector3d centroid() { + Vector3d sum = Vector3d.zero(); + + for(Vertex v : vertices) { + sum = sum.plus(v.pos); + } + + return sum.times(1.0/vertices.size()); + } + /** + * Indicates whether the specified point is contained within this polygon. + * + * @param p point + * @return {@code true} if the point is inside the polygon or on one of the + * edges; {@code false} otherwise + */ public boolean contains(Vector3d p) { - // taken from http://www.java-gaming.org/index.php?topic=26013.0 - // http://alienryderflex.com/polygon/ - // and http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html // P not on the plane if (plane.distance(p) > Plane.EPSILON) { return false; } - - // if P is on the plane, we proceed with projection to XY plane + // if P is on one of the vertices, return true + for (int i = 0; i < vertices.size() - 1; i++) { + if (p.minus(vertices.get(i).pos).magnitude() < Plane.EPSILON) { + return true; + } + } + + // if P is on the plane, we proceed with projection to XY plane // // P1--P------P2 // ^ // | // P is on the segment if( dist(P1,P) + dist(P2,P) - dist(P1,P2) < TOL) - boolean onASegment = false; for (int i = 0; i < vertices.size() - 1; i++) { Vector3d p1 = vertices.get(i).pos; Vector3d p2 = vertices.get(i + 1).pos; - onASegment = p1.minus(p).magnitude() + p2.minus(p).magnitude() + boolean onASegment = p1.minus(p).magnitude() + p2.minus(p).magnitude() - p1.minus(p2).magnitude() < Plane.EPSILON; - - if(onASegment) return true; + + if (onASegment) { + return true; + } } -// double px = p.x(); -// double py = p.y(); -// boolean oddNodes = false; -// double x2 = vertices.get(vertices.size() - 1).pos.x(); -// double y2 = vertices.get(vertices.size() - 1).pos.y(); -// double x1, y; -// for (int i = 0; i < vertices.size(); x2 = x1, y2 = y, ++i) { -// x1 = vertices.get(i).pos.x(); -// y = vertices.get(i).pos.y(); -// if (((y > py) && (y2 >= py)) -// || (y >= py) && (y2 < py)) { -// if ((py - y) / (y2 - y) -// * (x2 - x1) < (px - x1)) { -// oddNodes = !oddNodes; -// } -// } -// } -// return oddNodes; + // see from http://www.java-gaming.org/index.php?topic=26013.0 + // see http://alienryderflex.com/polygon/ + // see http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html int i, j = vertices.size() - 1; boolean oddNodes = false; double x = p.x(); @@ -524,6 +530,15 @@ public boolean contains(Vector3d p) { } + @Deprecated + public boolean intersects(Polygon p) { + if (!getBounds().intersects(p.getBounds())) { + return false; + } + + throw new UnsupportedOperationException("Not implemented"); + } + public boolean contains(Polygon p) { for (Vertex v : p.vertices) { diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main.java b/src/main/java/eu/mihosoft/jcsg/playground/Main.java index 984543b3..e0a891b5 100644 --- a/src/main/java/eu/mihosoft/jcsg/playground/Main.java +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main.java @@ -7,15 +7,19 @@ import eu.mihosoft.jcsg.Bounds; import eu.mihosoft.jcsg.CSG; +import eu.mihosoft.jcsg.Cube; import eu.mihosoft.jcsg.Polygon; import eu.mihosoft.jcsg.Sphere; import eu.mihosoft.jcsg.Vertex; +import eu.mihosoft.vvecmath.Plane; +import eu.mihosoft.vvecmath.Transform; import eu.mihosoft.vvecmath.Vector3d; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** * @@ -27,28 +31,278 @@ public class Main { public static void main(String[] args) throws IOException { - CSG c1 = new Sphere(Vector3d.zero(), 1.0, 32, 32).toCSG(); - CSG c2 = new Sphere(Vector3d.x(0.75), 1.0, 32, 32).toCSG(); + CSG c1 = new Cube(Vector3d.zero(), Vector3d.xyz(2, 2, 2)).toCSG(). + transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 45)); + CSG c2 = new Cube(Vector3d.xyz(1, 1, 1), Vector3d.xyz(2, 2, 2)).toCSG(); +// c1 = new Sphere(Vector3d.x(0.), 1.25, 10, 10).toCSG(); +// c2 = new Sphere(Vector3d.x(0.75), 1.25, 10, 10).toCSG(); + c2 = new Sphere(Vector3d.x(0.0), 1.25, 32, 32).toCSG(); - SplitResult result1 = splitPolygons(c1.getPolygons(), c2.getPolygons(), + List result1 = splitPolygons( + c1.getPolygons(), c2.getPolygons(), c1.getBounds(), c2.getBounds()); - SplitResult result2 = splitPolygons(c2.getPolygons(), c1.getPolygons(), + + List result2 = splitPolygons( + c2.getPolygons(), c1.getPolygons(), c2.getBounds(), c1.getBounds()); + + List splitted = new ArrayList<>(); + splitted.addAll(result1); + splitted.addAll(result2); + + Files.write(Paths.get("test-split1.stl"), + CSG.fromPolygons(splitted).toStlString().getBytes()); + + List inC2 = new ArrayList<>(); + List outC2 = new ArrayList<>(); + List sameC2 = new ArrayList<>(); + List oppositeC2 = new ArrayList<>(); + + List unknownOfC1 = new ArrayList<>(); + + for (Polygon p : result2) { + PolygonType pT = classifyPolygon(p, result1, c2.getBounds()); + + if (pT == PolygonType.INSIDE) { + inC2.add(p); + } + + if (pT == PolygonType.SAME) { + sameC2.add(p); + } + + if (pT == PolygonType.OPPOSITE) { + oppositeC2.add(p); + } + + if (pT == PolygonType.OUTSIDE) { + outC2.add(p); + } + + if (pT == PolygonType.UNKNOWN) { + unknownOfC1.add(p); + } + } + + List inC1 = new ArrayList<>(); + List outC1 = new ArrayList<>(); + List sameC1 = new ArrayList<>(); + List oppositeC1 = new ArrayList<>(); + + List unknownOfC2 = new ArrayList<>(); + + for (Polygon p : result1) { + PolygonType pT = classifyPolygon(p, result2, c1.getBounds()); + + if (pT == PolygonType.INSIDE) { + inC1.add(p); + } + + if (pT == PolygonType.OUTSIDE) { + outC1.add(p); + } + + if (pT == PolygonType.SAME) { + sameC1.add(p); + } + + if (pT == PolygonType.OPPOSITE) { + oppositeC1.add(p); + } + + if (pT == PolygonType.UNKNOWN) { + unknownOfC2.add(p); + } + } + + List difference = new ArrayList<>(); + difference.addAll(outC2); + difference.addAll(oppositeC2); + for (Polygon p : inC1) { + p.flip(); + } + for (Polygon p : inC2) { + p.flip(); + } + + difference.addAll(inC1); - result1.p2.addAll(result2.p2); + System.err.println(">> creating CSG"); - CSG result = CSG.fromPolygons(result1.p2); + CSG result = CSG.fromPolygons(difference); + + System.err.println(">> unknown polygons in C1: " + unknownOfC1.size()); + System.err.println(">> unknown polygons in C2: " + unknownOfC2.size()); + System.err.println(">> inside polygons in C1: " + inC1.size()); + System.err.println(">> inside polygons in C2: " + inC2.size()); Files.write(Paths.get("test.stl"), result.toStlString().getBytes()); } - public static SplitResult splitPolygons( + public static PolygonType classifyPolygon(Polygon p1, List polygons, Bounds b) { + + double TOL = 1e-8; + + // we are definitely outside if bounding boxes don't intersect + if (!p1.getBounds().intersects(b)) { + return PolygonType.OUTSIDE; + } + + Vector3d rayCenter = p1.centroid(); + Vector3d rayDirection = p1.plane.normal; + + List intersections = getPolygonsThatIntersectWithRay( + rayCenter, rayDirection, polygons, TOL); + + if (intersections.isEmpty()) { + return PolygonType.OUTSIDE; + } + + // find the closest polygon to the centroid of p1 which intersects thr + // ray + RayIntersection min = intersections.get(0); + double prevDist = min.polygon.centroid().minus(rayCenter).magnitude(); + for (RayIntersection ri : intersections) { + double dist = ri.polygon.centroid().minus(rayCenter).magnitude(); + if (dist < prevDist) { + prevDist = dist; + min = ri; + } + } + + int frontOrBack = p1.plane.compare(min.polygon.centroid(), TOL); + + Vector3d planePoint = p1.plane.normal.normalized().times(p1.plane.getDist()); + + int towardsOrAwayFrom = p1.plane.compare( + planePoint.plus(min.polygon.plane.normal), TOL); + + if (frontOrBack > 0 && towardsOrAwayFrom < 0) { + return PolygonType.INSIDE; + } + + if (frontOrBack < 0 && towardsOrAwayFrom > 0) { + return PolygonType.OUTSIDE; + } + + if (frontOrBack < 0 && towardsOrAwayFrom < 0) { + return PolygonType.OUTSIDE; + } + + if (frontOrBack > 0 && towardsOrAwayFrom > 0) { + return PolygonType.INSIDE; + } + + if (frontOrBack == 0 && towardsOrAwayFrom > 0) { + return PolygonType.SAME; + } + + if (frontOrBack == 0 && towardsOrAwayFrom < 0) { + return PolygonType.OPPOSITE; + } + + System.err.println("I need help (2) !"); + + return PolygonType.UNKNOWN; + } + + public static final class PlaneIntersection { + + public final IntersectionType type; + public Optional point; + + public PlaneIntersection( + IntersectionType type, Optional point) { + this.type = type; + this.point = point; + } + + public static enum IntersectionType { + ON, + PARALLEL, + NON_PARALLEL + } + } + + public static final class RayIntersection { + + public final Vector3d intersectionPoint; + public final Polygon polygon; + + public RayIntersection(Vector3d intersectionPoint, Polygon polygon) { + this.intersectionPoint = intersectionPoint; + this.polygon = polygon; + } + + } + + public static List getPolygonsThatIntersectWithRay( + Vector3d point, Vector3d direction, List polygons, double TOL) { + List intersection = new ArrayList<>(); + for (Polygon p : polygons) { + PlaneIntersection res = computePlaneIntersection(p.plane, point, direction, TOL); + if (res.type == PlaneIntersection.IntersectionType.NON_PARALLEL) { + if (p.contains(res.point.get())) { + intersection.add(new RayIntersection(res.point.get(), p)); + } + } + } + + return intersection; + } + + public static PlaneIntersection computePlaneIntersection( + Plane plane, Vector3d point, Vector3d direction, double TOL) { + + //Ax + By + Cz + D = 0 + //x = x0 + t(x1 x0) + //y = y0 + t(y1 y0) + //z = z0 + t(z1 z0) + //(x1 - x0) = dx, (y1 - y0) = dy, (z1 - z0) = dz + //t = -(A*x0 + B*y0 + C*z0 )/(A*dx + B*dy + C*dz) + Vector3d normal = plane.normal; + Vector3d planePoint = normal.normalized().times(plane.getDist()); + + double A = normal.x(); + double B = normal.y(); + double C = normal.z(); + double D = -(normal.x() * planePoint.x() + normal.y() * planePoint.y() + normal.z() * planePoint.z()); + + double numerator = A * point.x() + B * point.y() + C * point.z() + D; + double denominator = A * direction.x() + B * direction.y() + C * direction.z(); + + //if line is paralel to the plane... + if (Math.abs(denominator) < TOL) { + //if line is contained in the plane... + if (Math.abs(numerator) < TOL) { + return new PlaneIntersection( + PlaneIntersection.IntersectionType.ON, + Optional.of(point)); + } else { + return new PlaneIntersection( + PlaneIntersection.IntersectionType.PARALLEL, + Optional.empty()); + } + } //if line intercepts the plane... + else { + double t = -numerator / denominator; + Vector3d resultPoint = Vector3d.xyz( + point.x() + t * direction.x(), + point.y() + t * direction.y(), + point.z() + t * direction.z()); + + return new PlaneIntersection( + PlaneIntersection.IntersectionType.NON_PARALLEL, + Optional.of(resultPoint)); + } + } + + public static List splitPolygons( List ps1, List ps2, Bounds b1, Bounds b2) { - SplitResult result = new SplitResult(); List ps2WithCuts = new ArrayList<>(ps2); for (Polygon p1 : ps1) { @@ -67,25 +321,25 @@ public static SplitResult splitPolygons( continue; } - // compute distance of polygon 1 vertices to polygon 2 plane - int[] typesP1 = new int[p1.vertices.size()]; - - boolean typesEqual1 = true; - int prevType1 = 0; - for (int i = 0; i < p1.vertices.size(); i++) { - typesP1[i] = p2.plane.compare(p1.vertices.get(i).pos, EPS); - - if (i > 0 && typesEqual1) { - typesEqual1 &= prevType1 == typesP1[i]; - } - - prevType1 = typesP1[i]; - } - - // planes do not intersect, thus polygons do not intersect - if (typesEqual1) { - continue; - } +// // compute distance of polygon 1 vertices to polygon 2 plane +// int[] typesP1 = new int[p1.vertices.size()]; +// +// boolean typesEqual1 = true; +// int prevType1 = 0; +// for (int i = 0; i < p1.vertices.size(); i++) { +// typesP1[i] = p2.plane.compare(p1.vertices.get(i).pos, EPS); +// +// if (i > 0 && typesEqual1) { +// typesEqual1 &= prevType1 == typesP1[i]; +// } +// +// prevType1 = typesP1[i]; +// } +// +// // planes do not intersect, thus polygons do not intersect +// if (typesEqual1) { +// continue; +// } // compute distance of polygon 2 vertices to polygon 1 plane int[] typesP2 = new int[p2.vertices.size()]; @@ -96,7 +350,7 @@ public static SplitResult splitPolygons( typesP2[i] = p1.plane.compare(p2.vertices.get(i).pos, EPS); if (i > 0 && typesEqual2) { - typesEqual2 &= prevType2 == typesP2[i]; + typesEqual2 = typesEqual2 && (prevType2 == typesP2[i]); } prevType2 = typesP2[i]; @@ -107,77 +361,105 @@ public static SplitResult splitPolygons( continue; } - // cut p2 with plane1 - List segmentPoints = new ArrayList<>(); - List frontPolygon = new ArrayList<>(); - List backPolygon = new ArrayList<>(); - for (int i = 0; i < p2.vertices.size(); i++) { - int j = (i + 1) % p2.vertices.size(); - int ti = typesP2[i]; - int tj = typesP2[j]; - Vertex vi = p2.vertices.get(i); - Vertex vj = p2.vertices.get(j); - if (ti != -1 /*front*/) { - frontPolygon.add(vi.pos); - } - if (ti != 1 /*back*/) { - backPolygon.add(vi.pos); - } - - if (ti != tj && (ti != 0 && tj != 0)/*spanning*/) { - double t = (p1.plane.getDist() - p1.plane.normal.dot(vi.pos)) - / p1.plane.normal.dot(vj.pos.minus(vi.pos)); - Vertex v = vi.interpolate(vj, t); - frontPolygon.add(v.pos); - backPolygon.add(v.pos); - segmentPoints.add(v.pos); + // if at least two vertices of convex polygon are on the plane + // then there's no intersection + int countZeroTypes2 = 0; + for (int i = 0; i < typesP2.length; i++) { + if (typesP2[i] == 0) { + countZeroTypes2++; } } - - // check whether the segments are contained in the polygons - boolean p1ContainsIntersectionPointsOfP2WithPlane = true; - - for (Vector3d fv : segmentPoints) { - if (!p1.contains(fv)) { - p1ContainsIntersectionPointsOfP2WithPlane = false; - break; - } + if (countZeroTypes2 > 1) { + continue; } - if (!(p1ContainsIntersectionPointsOfP2WithPlane)) { + if (p1.plane.normal.angle(p2.plane.normal) < EPS + || Math.abs(p1.plane.normal.angle(p2.plane.normal) - 180) < EPS) { + System.err.println("HERE"); +// System.exit(-1); continue; + } else { + System.err.println("a: " + p1.plane.normal.angle(p2.plane.normal)); } + // cut p2 with plane1 + List segmentPointsP2 = new ArrayList<>(); + List frontPolygonP2 = new ArrayList<>(); + List backPolygonP2 = new ArrayList<>(); + cutPolygonWithPlane(p2, p1.plane, typesP2, + frontPolygonP2, backPolygonP2, segmentPointsP2); +// List segmentPointsP1 = new ArrayList<>(); +// List frontPolygonP1 = new ArrayList<>(); +// List backPolygonP1 = new ArrayList<>(); +// cutPolygonWithPlane(p1, p2.plane, typesP1, +// frontPolygonP1, backPolygonP1, segmentPointsP1); + + // TODO check segment intersection for accurate results + // now, we might have more cuts than necessary p2ToDelete.add(p2); - if (frontPolygon.size() > 2) { - cutsWithP1.add(Polygon.fromPoints(frontPolygon, p1.getStorage())); + if (frontPolygonP2.size() > 2) { + + Polygon p = Polygon.fromPoints( + frontPolygonP2, p2.getStorage()); + if (p.isValid()) { + cutsWithP1.add(p); + } } - if (backPolygon.size() > 2) { - cutsWithP1.add(Polygon.fromPoints(backPolygon, p1.getStorage())); + if (backPolygonP2.size() > 2) { + Polygon p = Polygon.fromPoints( + backPolygonP2, p2.getStorage()); + if (p.isValid()) { + cutsWithP1.add(p); + } } } ps2WithCuts.addAll(cutsWithP1); ps2WithCuts.removeAll(p2ToDelete); } - result.p1 = ps1; - result.p2 = ps2WithCuts; - return result; + return ps2WithCuts; } - public static class SplitResult { + private static void cutPolygonWithPlane(Polygon polygon, Plane cutPlane, + int[] vertexTypes, List frontPolygon, + List backPolygon, List segmentPoints) { + + for (int i = 0; i < polygon.vertices.size(); i++) { + int j = (i + 1) % polygon.vertices.size(); + int ti = vertexTypes[i]; + int tj = vertexTypes[j]; + Vertex vi = polygon.vertices.get(i); + Vertex vj = polygon.vertices.get(j); + if (ti == 1 /*front*/) { + frontPolygon.add(vi.pos); + } + if (ti == -1 /*back*/) { + backPolygon.add(vi.pos); + } - List p1 = new ArrayList<>(); - List p2 = new ArrayList<>(); + if (ti == 0) { + frontPolygon.add(vi.pos); + backPolygon.add(vi.pos); +// segmentPoints.add(vi.pos); + } + if (ti != tj && (ti != 0 && tj != 0)/*spanning*/) { + double t = (cutPlane.getDist() - cutPlane.normal.dot(vi.pos)) + / cutPlane.normal.dot(vj.pos.minus(vi.pos)); + Vertex v = vi.interpolate(vj, t); + frontPolygon.add(v.pos); + backPolygon.add(v.pos); + segmentPoints.add(v.pos); + } + } } enum PolygonType { UNKNOWN, - INSIDE_1, - INSIDE_2, + INSIDE, OUTSIDE, - OPPOSITE + OPPOSITE, + SAME } } From 7f87b762ecc618c3355c3c8992669ed1346daa9f Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Thu, 9 Feb 2017 19:18:35 +0100 Subject: [PATCH 08/62] fixed issues with plane intersection in new algorithm --- src/main/java/eu/mihosoft/jcsg/CSG.java | 67 ++++--- src/main/java/eu/mihosoft/jcsg/Polygon.java | 6 +- .../eu/mihosoft/jcsg/playground/Main.java | 178 +++++++++++++----- 3 files changed, 177 insertions(+), 74 deletions(-) diff --git a/src/main/java/eu/mihosoft/jcsg/CSG.java b/src/main/java/eu/mihosoft/jcsg/CSG.java index f0e9fd31..0733ad9e 100644 --- a/src/main/java/eu/mihosoft/jcsg/CSG.java +++ b/src/main/java/eu/mihosoft/jcsg/CSG.java @@ -59,12 +59,11 @@ * Implementation Details * * All CSG operations are implemented in terms of two functions, - * {@link Node#clipTo(Node)} and {@link Node#invert()}, - * which remove parts of a BSP tree inside another BSP tree and swap solid and - * empty space, respectively. To find the union of {@code a} and {@code b}, we - * want to remove everything in {@code a} inside {@code b} and everything in - * {@code b} inside {@code a}, then combine polygons from {@code a} and - * {@code b} into one solid: + * {@link Node#clipTo(Node)} and {@link Node#invert()}, which remove parts of a + * BSP tree inside another BSP tree and swap solid and empty space, + * respectively. To find the union of {@code a} and {@code b}, we want to remove + * everything in {@code a} inside {@code b} and everything in {@code b} inside + * {@code a}, then combine polygons from {@code a} and {@code b} into one solid: * *
  *     a.clipTo(b);
@@ -239,27 +238,30 @@ public CSG union(CSG csg) {
                 return _unionNoOpt(csg);
         }
     }
-    
+
     /**
-     * Returns a csg consisting of the polygons of this csg and the specified csg.
-     * 
+     * Returns a csg consisting of the polygons of this csg and the specified
+     * csg.
+     *
      * The purpose of this method is to allow fast union operations for objects
-     * that do not intersect. 
-     * 
-     * 

WARNING: this method does not apply the csg algorithms. Therefore, + * that do not intersect. + * + *

+ * WARNING: this method does not apply the csg algorithms. Therefore, * please ensure that this csg and the specified csg do not intersect. - * + * * @param csg csg - * - * @return a csg consisting of the polygons of this csg and the specified csg + * + * @return a csg consisting of the polygons of this csg and the specified + * csg */ public CSG dumbUnion(CSG csg) { - + CSG result = this.clone(); CSG other = csg.clone(); - + result.polygons.addAll(other.polygons); - + return result; } @@ -747,6 +749,12 @@ public CSG color(Color c) { } public ObjFile toObj() { + // we triangulate the polygon to ensure + // compatibility with 3d printer software + return toObj(3); + } + + public ObjFile toObj(int maxNumberOfVerts) { StringBuilder objSb = new StringBuilder(); @@ -809,19 +817,23 @@ public PolygonStruct(PropertyStorage storage, List indices, String mate ps.storage.getValue("material:color").ifPresent( (v) -> objSb.append("usemtl ").append(ps.materialName).append("\n")); - // we triangulate the polygon to ensure - // compatibility with 3d printer software List pVerts = ps.indices; int index1 = pVerts.get(0); - for (int i = 0; i < pVerts.size() - 2; i++) { - int index2 = pVerts.get(i + 1); - int index3 = pVerts.get(i + 2); + for (int i = 1; i < pVerts.size() - maxNumberOfVerts; i++) { + objSb.append("f "); + for (int j = 1; j < maxNumberOfVerts; j++) { + objSb.append(index1).append(" "); + int indexJ = pVerts.get(j); + objSb.append(indexJ).append(" "); + } + objSb.append("\n"); + } - objSb.append("f "). - append(index1).append(" "). - append(index2).append(" "). - append(index3).append("\n"); + objSb.append("f "); + for (int i = 0; i < pVerts.size(); i++) { + objSb.append(pVerts.get(i)).append(" "); } + objSb.append("\n"); } objSb.append("\n# End Group v3d.csg").append("\n"); @@ -941,7 +953,6 @@ public CSG transformed(Transform transform) { return result; } - // TODO finish experiment (20.7.2014) public MeshContainer toJavaFXMesh() { diff --git a/src/main/java/eu/mihosoft/jcsg/Polygon.java b/src/main/java/eu/mihosoft/jcsg/Polygon.java index 8f9afbcb..6b8c91fa 100644 --- a/src/main/java/eu/mihosoft/jcsg/Polygon.java +++ b/src/main/java/eu/mihosoft/jcsg/Polygon.java @@ -123,10 +123,8 @@ public Polygon(List vertices, PropertyStorage shared) { vertices.get(0).pos, vertices.get(1).pos, vertices.get(2).pos); - this.plane = eu.mihosoft.vvecmath.Plane.fromPoints( - vertices.get(0).pos, - vertices.get(1).pos, - vertices.get(2).pos); + this.plane = eu.mihosoft.vvecmath.Plane. + fromPointAndNormal(centroid(), _csg_plane.normal); validateAndInit(vertices); } diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main.java b/src/main/java/eu/mihosoft/jcsg/playground/Main.java index e0a891b5..e089fe33 100644 --- a/src/main/java/eu/mihosoft/jcsg/playground/Main.java +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main.java @@ -8,7 +8,9 @@ import eu.mihosoft.jcsg.Bounds; import eu.mihosoft.jcsg.CSG; import eu.mihosoft.jcsg.Cube; +import eu.mihosoft.jcsg.ObjFile; import eu.mihosoft.jcsg.Polygon; +import eu.mihosoft.jcsg.STL; import eu.mihosoft.jcsg.Sphere; import eu.mihosoft.jcsg.Vertex; import eu.mihosoft.vvecmath.Plane; @@ -20,6 +22,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; /** * @@ -31,13 +35,20 @@ public class Main { public static void main(String[] args) throws IOException { - CSG c1 = new Cube(Vector3d.zero(), Vector3d.xyz(2, 2, 2)).toCSG(). - transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 45)); - CSG c2 = new Cube(Vector3d.xyz(1, 1, 1), Vector3d.xyz(2, 2, 2)).toCSG(); -// c1 = new Sphere(Vector3d.x(0.), 1.25, 10, 10).toCSG(); -// c2 = new Sphere(Vector3d.x(0.75), 1.25, 10, 10).toCSG(); - c2 = new Sphere(Vector3d.x(0.0), 1.25, 32, 32).toCSG(); + testCut(); + CSG c1 = new Cube(Vector3d.zero(), Vector3d.xyz(2, 2, 2)).toCSG(); + + CSG c2 = new Cube(Vector3d.xyz(1, 1, 1), Vector3d.xyz(2, 2, 2)).toCSG().transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 45)); + + Files.write(Paths.get("c1.stl"), c1.toStlString().getBytes()); + Files.write(Paths.get("c2.stl"), c2.toStlString().getBytes()); + +// c1 = STL.file(Paths.get("c1.stl")); +// c2 = STL.file(Paths.get("c2.stl")); + c1 = new Sphere(Vector3d.x(0.), 1.25, 10, 10).toCSG(); + c2 = new Sphere(Vector3d.x(1.0), 1.25, 10, 10).toCSG(); +// c2 = new Sphere(Vector3d.x(0.0), 1.25, 32, 32).toCSG(); List result1 = splitPolygons( c1.getPolygons(), c2.getPolygons(), c1.getBounds(), c2.getBounds()); @@ -45,11 +56,16 @@ public static void main(String[] args) throws IOException { List result2 = splitPolygons( c2.getPolygons(), c1.getPolygons(), c2.getBounds(), c1.getBounds()); - + +// result1 = splitPolygons( +// result2, c2.getPolygons(), +// c1.getBounds(), c2.getBounds()); List splitted = new ArrayList<>(); splitted.addAll(result1); splitted.addAll(result2); - + + CSG.fromPolygons(splitted).toObj(100).toFiles(Paths.get("test-split1.obj")); + Files.write(Paths.get("test-split1.stl"), CSG.fromPolygons(splitted).toStlString().getBytes()); @@ -131,10 +147,12 @@ public static void main(String[] args) throws IOException { CSG result = CSG.fromPolygons(difference); - System.err.println(">> unknown polygons in C1: " + unknownOfC1.size()); - System.err.println(">> unknown polygons in C2: " + unknownOfC2.size()); - System.err.println(">> inside polygons in C1: " + inC1.size()); - System.err.println(">> inside polygons in C2: " + inC2.size()); + System.err.println(">> unknown polygons in C1: " + unknownOfC1.size()); + System.err.println(">> unknown polygons in C2: " + unknownOfC2.size()); + System.err.println(">> opposite polygons in C1: " + oppositeC1.size()); + System.err.println(">> opposite polygons in C2: " + oppositeC2.size()); + System.err.println(">> inside polygons in C1: " + inC1.size()); + System.err.println(">> inside polygons in C2: " + inC2.size()); Files.write(Paths.get("test.stl"), result.toStlString().getBytes()); @@ -150,7 +168,7 @@ public static PolygonType classifyPolygon(Polygon p1, List polygons, Bo } Vector3d rayCenter = p1.centroid(); - Vector3d rayDirection = p1.plane.normal; + Vector3d rayDirection = p1.plane.getNormal(); List intersections = getPolygonsThatIntersectWithRay( rayCenter, rayDirection, polygons, TOL); @@ -173,10 +191,10 @@ public static PolygonType classifyPolygon(Polygon p1, List polygons, Bo int frontOrBack = p1.plane.compare(min.polygon.centroid(), TOL); - Vector3d planePoint = p1.plane.normal.normalized().times(p1.plane.getDist()); + Vector3d planePoint = p1.plane.getAnchor(); int towardsOrAwayFrom = p1.plane.compare( - planePoint.plus(min.polygon.plane.normal), TOL); + planePoint.plus(min.polygon.plane.getNormal()), TOL); if (frontOrBack > 0 && towardsOrAwayFrom < 0) { return PolygonType.INSIDE; @@ -261,8 +279,8 @@ public static PlaneIntersection computePlaneIntersection( //z = z0 + t(z1 z0) //(x1 - x0) = dx, (y1 - y0) = dy, (z1 - z0) = dz //t = -(A*x0 + B*y0 + C*z0 )/(A*dx + B*dy + C*dz) - Vector3d normal = plane.normal; - Vector3d planePoint = normal.normalized().times(plane.getDist()); + Vector3d normal = plane.getNormal(); + Vector3d planePoint = plane.getAnchor(); double A = normal.x(); double B = normal.y(); @@ -363,25 +381,23 @@ public static List splitPolygons( // if at least two vertices of convex polygon are on the plane // then there's no intersection - int countZeroTypes2 = 0; - for (int i = 0; i < typesP2.length; i++) { - if (typesP2[i] == 0) { - countZeroTypes2++; - } - } - if (countZeroTypes2 > 1) { - continue; - } - - if (p1.plane.normal.angle(p2.plane.normal) < EPS - || Math.abs(p1.plane.normal.angle(p2.plane.normal) - 180) < EPS) { - System.err.println("HERE"); +// int countZeroTypes2 = 0; +// for (int i = 0; i < typesP2.length; i++) { +// if (typesP2[i] == 0) { +// countZeroTypes2++; +// } +// } +// if (countZeroTypes2 > 1) { +// continue; +// } +// if (p1.plane.getNormal().angle(p2.plane.getNormal()) < EPS +// || Math.abs(p1.plane.getNormal().angle(p2.plane.getNormal()) - 180) < EPS) { +// System.err.println("HERE"); // System.exit(-1); - continue; - } else { - System.err.println("a: " + p1.plane.normal.angle(p2.plane.normal)); - } - +// continue; +// } else { +// System.err.println("a: " + p1.plane.getNormal().angle(p2.plane.getNormal())); +// } // cut p2 with plane1 List segmentPointsP2 = new ArrayList<>(); List frontPolygonP2 = new ArrayList<>(); @@ -423,7 +439,12 @@ public static List splitPolygons( private static void cutPolygonWithPlane(Polygon polygon, Plane cutPlane, int[] vertexTypes, List frontPolygon, - List backPolygon, List segmentPoints) { + List backPolygon, List onPlane) { + + System.out.println("polygon: \n" + polygon.toStlString()); + System.out.println("--------------------"); + System.out.println("plane: \n -> p: " + cutPlane.getAnchor() + "\n -> n: " + cutPlane.getNormal()); + System.out.println("--------------------"); for (int i = 0; i < polygon.vertices.size(); i++) { int j = (i + 1) % polygon.vertices.size(); @@ -445,14 +466,87 @@ private static void cutPolygonWithPlane(Polygon polygon, Plane cutPlane, } if (ti != tj && (ti != 0 && tj != 0)/*spanning*/) { - double t = (cutPlane.getDist() - cutPlane.normal.dot(vi.pos)) - / cutPlane.normal.dot(vj.pos.minus(vi.pos)); - Vertex v = vi.interpolate(vj, t); - frontPolygon.add(v.pos); - backPolygon.add(v.pos); - segmentPoints.add(v.pos); + PlaneIntersection pI = computePlaneIntersection(cutPlane, vi.pos, vj.pos.minus(vi.pos), EPS); + + if (pI.type != PlaneIntersection.IntersectionType.NON_PARALLEL) { + throw new RuntimeException("I need help (3)!"); + } + + Vector3d intersectionPoint = pI.point.get(); + + System.out.println("i: " + i + ", j: " + j + " : " + intersectionPoint); + +// double t = (cutPlane.distance(vi.pos))/vi.pos.minus(vj.pos).magnitude(); +// System.out.println("t: " + t + ", i: " + i + ", j: " + j); +// System.out.println(" : vi-plane-dist: " + cutPlane.distance(vi.pos) + ", vi-vj-dist: " + vi.pos.minus(vj.pos).magnitude()); +// System.out.println(" : vi: " + vi.pos.toStlString()); +// System.out.println(" : vj: " + vj.pos.toStlString()); +// System.out.println(" : " + cutPlane.distance(vi.pos) + " , " + cutPlane.distance(vj.pos)); +// Vertex v = vi.interpolate(vj, t); +// System.out.println(" : vs: " + v); + frontPolygon.add(intersectionPoint); + backPolygon.add(intersectionPoint); + onPlane.add(intersectionPoint); + } + } + } + + public static void testCut() { + + Polygon p = Polygon.fromPoints( + Vector3d.xyz(0, 0, 0), + Vector3d.xyz(1, 0, 0), + Vector3d.xyz(1, 0, 1), + Vector3d.xyz(0, 0, 1) + ); + Plane plane = Plane.fromPointAndNormal(Vector3d.xyz(0.5, 0, 0.5), Vector3d.xyz(0, 0, 1)); + + int types[] = new int[p.vertices.size()]; + for (int i = 0; i < p.vertices.size(); i++) { + types[i] = plane.compare(p.vertices.get(i).pos, EPS); + System.out.println("type " + i + ": " + types[i]); + } + + List front = new ArrayList<>(); + List back = new ArrayList<>(); + List on = new ArrayList<>(); + + cutPolygonWithPlane(p, plane, types, front, back, on); + + List cutsWithP1 = new ArrayList<>(); + if (front.size() > 2) { + + Polygon frontCut = Polygon.fromPoints( + front); + if (frontCut.isValid()) { + cutsWithP1.add(frontCut); + } + } + if (back.size() > 2) { + Polygon backCut = Polygon.fromPoints( + back); + if (backCut.isValid()) { + cutsWithP1.add(backCut); } } + + System.out.println("#polygons: " + cutsWithP1.size()); + + for (Polygon polygon : cutsWithP1) { + System.out.println(polygon.toStlString()); + } + + try { + + ObjFile objF = CSG.fromPolygons(cutsWithP1).toObj(); + objF.toFiles(Paths.get("test-split1.obj")); +// Files.write(Paths.get("test-split1.stl"), +// CSG.fromPolygons(cutsWithP1).toStlString().getBytes()); + } catch (IOException ex) { + Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + } + +// System.exit(0); } enum PolygonType { From 0a99a06bc329647803b54f6975376c774701cb56 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Fri, 10 Feb 2017 20:45:32 +0100 Subject: [PATCH 09/62] more debugging --- src/main/java/eu/mihosoft/jcsg/Bounds.java | 9 +- src/main/java/eu/mihosoft/jcsg/CSG.java | 45 +-- src/main/java/eu/mihosoft/jcsg/Cube.java | 63 ++++- src/main/java/eu/mihosoft/jcsg/Polygon.java | 76 ++++-- .../eu/mihosoft/jcsg/playground/Main.java | 257 ++++++++---------- 5 files changed, 272 insertions(+), 178 deletions(-) diff --git a/src/main/java/eu/mihosoft/jcsg/Bounds.java b/src/main/java/eu/mihosoft/jcsg/Bounds.java index f39ce50c..ad6da711 100644 --- a/src/main/java/eu/mihosoft/jcsg/Bounds.java +++ b/src/main/java/eu/mihosoft/jcsg/Bounds.java @@ -18,7 +18,7 @@ public class Bounds { private final Vector3d bounds; private final Vector3d min; private final Vector3d max; - private final CSG csg; + private CSG csg; private final Cube cube; /** @@ -42,7 +42,7 @@ public Bounds(Vector3d min, Vector3d max) { this.max = max.clone(); cube = new Cube(center, bounds); - csg = cube.toCSG(); + } @Override @@ -74,6 +74,11 @@ public Vector3d getBounds() { * @return this bounding box as csg */ public CSG toCSG() { + + if (csg == null) { + csg = cube.toCSG(); + } + return csg; } diff --git a/src/main/java/eu/mihosoft/jcsg/CSG.java b/src/main/java/eu/mihosoft/jcsg/CSG.java index 0733ad9e..e264d4a6 100644 --- a/src/main/java/eu/mihosoft/jcsg/CSG.java +++ b/src/main/java/eu/mihosoft/jcsg/CSG.java @@ -755,6 +755,11 @@ public ObjFile toObj() { } public ObjFile toObj(int maxNumberOfVerts) { + + if(maxNumberOfVerts != 3) { + throw new UnsupportedOperationException( + "maxNumberOfVerts > 3 not supported yet"); + } StringBuilder objSb = new StringBuilder(); @@ -817,22 +822,24 @@ public PolygonStruct(PropertyStorage storage, List indices, String mate ps.storage.getValue("material:color").ifPresent( (v) -> objSb.append("usemtl ").append(ps.materialName).append("\n")); + // we triangulate the polygon to ensure + // compatibility with 3d printer software List pVerts = ps.indices; int index1 = pVerts.get(0); - for (int i = 1; i < pVerts.size() - maxNumberOfVerts; i++) { - objSb.append("f "); - for (int j = 1; j < maxNumberOfVerts; j++) { - objSb.append(index1).append(" "); - int indexJ = pVerts.get(j); - objSb.append(indexJ).append(" "); - } - objSb.append("\n"); - } + for (int i = 0; i < pVerts.size() - 2; i++) { + int index2 = pVerts.get(i + 1); + int index3 = pVerts.get(i + 2); - objSb.append("f "); - for (int i = 0; i < pVerts.size(); i++) { - objSb.append(pVerts.get(i)).append(" "); + objSb.append("f "). + append(index1).append(" "). + append(index2).append(" "). + append(index3).append("\n"); } +// +// objSb.append("f "); +// for (int i = 0; i < pVerts.size(); i++) { +// objSb.append(pVerts.get(i)).append(" "); +// } objSb.append("\n"); } @@ -1119,14 +1126,16 @@ public Bounds getBounds() { if (polygons.isEmpty()) { return new Bounds(Vector3d.ZERO, Vector3d.ZERO); } + + Vector3d initial = polygons.get(0).vertices.get(0).pos; - double minX = Double.POSITIVE_INFINITY; - double minY = Double.POSITIVE_INFINITY; - double minZ = Double.POSITIVE_INFINITY; + double minX = initial.x(); + double minY = initial.y(); + double minZ = initial.z(); - double maxX = Double.NEGATIVE_INFINITY; - double maxY = Double.NEGATIVE_INFINITY; - double maxZ = Double.NEGATIVE_INFINITY; + double maxX = initial.x(); + double maxY = initial.y(); + double maxZ = initial.z(); for (Polygon p : getPolygons()) { diff --git a/src/main/java/eu/mihosoft/jcsg/Cube.java b/src/main/java/eu/mihosoft/jcsg/Cube.java index 3d314df4..53e755a1 100644 --- a/src/main/java/eu/mihosoft/jcsg/Cube.java +++ b/src/main/java/eu/mihosoft/jcsg/Cube.java @@ -36,6 +36,7 @@ import eu.mihosoft.vvecmath.Vector3d; import eu.mihosoft.vvecmath.Transform; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -103,7 +104,66 @@ public Cube(double w, double h, double d) { this(Vector3d.ZERO, Vector3d.xyz(w, h, d)); } - @Override +// public List toPolygons() { +// List result = new ArrayList<>(6); +// +// Vector3d centerOffset = dimensions.times(0.5); +// +// result.addAll(Arrays.asList(new Polygon[]{ +// Polygon.fromPoints( +// centerOffset.times(-1, -1, -1), +// centerOffset.times(1, -1, -1), +// centerOffset.times(1, -1, 1), +// centerOffset.times(-1, -1, 1) +// ), +// Polygon.fromPoints( +// centerOffset.times(1, -1, -1), +// centerOffset.times(1, 1, -1), +// centerOffset.times(1, 1, 1), +// centerOffset.times(1, -1, 1) +// ), +// Polygon.fromPoints( +// centerOffset.times(1, 1, -1), +// centerOffset.times(-1, 1, -1), +// centerOffset.times(-1, 1, 1), +// centerOffset.times(1, 1, 1) +// ), +// Polygon.fromPoints( +// centerOffset.times(1, 1, 1), +// centerOffset.times(-1, 1, 1), +// centerOffset.times(-1, -1, 1), +// centerOffset.times(1, -1, 1) +// ), +// Polygon.fromPoints( +// centerOffset.times(-1, 1, 1), +// centerOffset.times(-1, 1, -1), +// centerOffset.times(-1, -1, -1), +// centerOffset.times(-1, -1, 1) +// ), +// Polygon.fromPoints( +// centerOffset.times(-1, 1, -1), +// centerOffset.times(1, 1, -1), +// centerOffset.times(1, -1, -1), +// centerOffset.times(-1, -1, -1) +// ) +// } +// )); +// +// if(!centered) { +// Transform centerTransform = Transform.unity(). +// translate(dimensions.x() / 2.0, +// dimensions.y() / 2.0, +// dimensions.z() / 2.0); +// +// for (Polygon p : result) { +// p.transform(centerTransform); +// } +// } +// +// return result; +// } + + public List toPolygons() { int[][][] a = { @@ -183,6 +243,7 @@ public PropertyStorage getProperties() { /** * Defines that this cube will not be centered. + * * @return this cube */ public Cube noCenter() { diff --git a/src/main/java/eu/mihosoft/jcsg/Polygon.java b/src/main/java/eu/mihosoft/jcsg/Polygon.java index 6b8c91fa..792b5a77 100644 --- a/src/main/java/eu/mihosoft/jcsg/Polygon.java +++ b/src/main/java/eu/mihosoft/jcsg/Polygon.java @@ -65,7 +65,7 @@ public final class Polygon { * Note: uses first three vertices to define the plane. */ public final Plane _csg_plane; - public final eu.mihosoft.vvecmath.Plane plane; + public eu.mihosoft.vvecmath.Plane plane; void setStorage(PropertyStorage storage) { this.shared = storage; @@ -164,10 +164,8 @@ public Polygon(List vertices) { vertices.get(1).pos, vertices.get(2).pos); - this.plane = eu.mihosoft.vvecmath.Plane.fromPoints( - vertices.get(0).pos, - vertices.get(1).pos, - vertices.get(2).pos); + this.plane = eu.mihosoft.vvecmath.Plane. + fromPointAndNormal(centroid(), _csg_plane.normal); validateAndInit(vertices); } @@ -280,8 +278,12 @@ public Polygon translate(Vector3d v) { Vector3d b = this.vertices.get(1).pos; Vector3d c = this.vertices.get(2).pos; + // TODO plane update correct? this._csg_plane.normal = b.minus(a).cross(c.minus(a)); + this.plane = eu.mihosoft.vvecmath.Plane. + fromPointAndNormal(centroid(), _csg_plane.normal); + return this; } @@ -323,6 +325,13 @@ public Polygon transform(Transform transform) { this._csg_plane.normal = b.minus(a).cross(c.minus(a)).normalized(); this._csg_plane.dist = this._csg_plane.normal.dot(a); + this.plane = eu.mihosoft.vvecmath.Plane. + fromPointAndNormal(centroid(), _csg_plane.normal); + + vertices.forEach((vertex) -> { + vertex.normal = plane.getNormal(); + }); + if (transform.isMirror()) { // the transformation includes mirroring. flip polygon flip(); @@ -454,15 +463,15 @@ public Bounds getBounds() { Vector3d.xyz(minX, minY, minZ), Vector3d.xyz(maxX, maxY, maxZ)); } - - public Vector3d centroid() { + + public Vector3d centroid() { Vector3d sum = Vector3d.zero(); - - for(Vertex v : vertices) { + + for (Vertex v : vertices) { sum = sum.plus(v.pos); } - - return sum.times(1.0/vertices.size()); + + return sum.times(1.0 / vertices.size()); } /** @@ -505,18 +514,51 @@ public boolean contains(Vector3d p) { } } + // find projection plane + // we start with XY plane + int coordIndex1 = 0; + int coordIndex2 = 1; + + boolean orthogonalToXY = Math.abs(eu.mihosoft.vvecmath.Plane.XY_PLANE.getNormal() + .dot(plane.getNormal())) < Plane.EPSILON; + + boolean foundProjectionPlane = false; + if (!orthogonalToXY && !foundProjectionPlane) { + coordIndex1 = 0; + coordIndex2 = 1; + foundProjectionPlane = true; + } + + boolean orthogonalToXZ = Math.abs(eu.mihosoft.vvecmath.Plane.XZ_PLANE.getNormal() + .dot(plane.getNormal())) < Plane.EPSILON; + + if (!orthogonalToXZ && !foundProjectionPlane) { + coordIndex1 = 0; + coordIndex2 = 2; + foundProjectionPlane = true; + } + + boolean orthogonalToYZ = Math.abs(eu.mihosoft.vvecmath.Plane.YZ_PLANE.getNormal() + .dot(plane.getNormal())) < Plane.EPSILON; + + if (!orthogonalToYZ && !foundProjectionPlane) { + coordIndex1 = 1; + coordIndex2 = 2; + foundProjectionPlane = true; + } + // see from http://www.java-gaming.org/index.php?topic=26013.0 // see http://alienryderflex.com/polygon/ // see http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html int i, j = vertices.size() - 1; boolean oddNodes = false; - double x = p.x(); - double y = p.y(); + double x = p.get(coordIndex1); + double y = p.get(coordIndex2); for (i = 0; i < vertices.size(); i++) { - double xi = vertices.get(i).pos.getX(); - double yi = vertices.get(i).pos.getY(); - double xj = vertices.get(j).pos.getX(); - double yj = vertices.get(j).pos.getY(); + double xi = vertices.get(i).pos.get(coordIndex1); + double yi = vertices.get(i).pos.get(coordIndex2); + double xj = vertices.get(j).pos.get(coordIndex1); + double yj = vertices.get(j).pos.get(coordIndex2); if ((yi < y && yj >= y || yj < y && yi >= y) && (xi <= x || xj <= x)) { diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main.java b/src/main/java/eu/mihosoft/jcsg/playground/Main.java index e089fe33..68200d2f 100644 --- a/src/main/java/eu/mihosoft/jcsg/playground/Main.java +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main.java @@ -20,6 +20,8 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.logging.Level; @@ -37,38 +39,39 @@ public static void main(String[] args) throws IOException { testCut(); - CSG c1 = new Cube(Vector3d.zero(), Vector3d.xyz(2, 2, 2)).toCSG(); + CSG c1 = new Cube(Vector3d.zero(), Vector3d.xyz(1, 1, 1)).toCSG(); - CSG c2 = new Cube(Vector3d.xyz(1, 1, 1), Vector3d.xyz(2, 2, 2)).toCSG().transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 45)); - - Files.write(Paths.get("c1.stl"), c1.toStlString().getBytes()); - Files.write(Paths.get("c2.stl"), c2.toStlString().getBytes()); + CSG c2 = new Cube(Vector3d.xyz(1,1,1), Vector3d.xyz(2, 2, 2)).toCSG() + .transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 16)); +// Files.write(Paths.get("c1.stl"), c1.toStlString().getBytes()); +// Files.write(Paths.get("c2.stl"), c2.toStlString().getBytes()); // c1 = STL.file(Paths.get("c1.stl")); // c2 = STL.file(Paths.get("c2.stl")); - c1 = new Sphere(Vector3d.x(0.), 1.25, 10, 10).toCSG(); - c2 = new Sphere(Vector3d.x(1.0), 1.25, 10, 10).toCSG(); +// c1 = new Sphere(Vector3d.x(0.), 0.5, 32, 32).toCSG(); +// c2 = new Sphere(Vector3d.x(0.6), 0.5, 32, 32).toCSG(); // c2 = new Sphere(Vector3d.x(0.0), 1.25, 32, 32).toCSG(); List result1 = splitPolygons( c1.getPolygons(), c2.getPolygons(), - c1.getBounds(), c2.getBounds()); + c1.getBounds(), c2.getBounds() + ); List result2 = splitPolygons( - c2.getPolygons(), c1.getPolygons(), - c2.getBounds(), c1.getBounds()); + result1, c1.getPolygons(), + c2.getBounds(), c1.getBounds() + ); -// result1 = splitPolygons( -// result2, c2.getPolygons(), -// c1.getBounds(), c2.getBounds()); + result1 = splitPolygons( + result2, c2.getPolygons(), + c1.getBounds(), c2.getBounds()); List splitted = new ArrayList<>(); splitted.addAll(result1); splitted.addAll(result2); - CSG.fromPolygons(splitted).toObj(100).toFiles(Paths.get("test-split1.obj")); - +// CSG.fromPolygons(splitted).toObj(100).toFiles(Paths.get("test-split1.obj")); +// Files.write(Paths.get("test-split1.stl"), CSG.fromPolygons(splitted).toStlString().getBytes()); - List inC2 = new ArrayList<>(); List outC2 = new ArrayList<>(); List sameC2 = new ArrayList<>(); @@ -77,7 +80,7 @@ public static void main(String[] args) throws IOException { List unknownOfC1 = new ArrayList<>(); for (Polygon p : result2) { - PolygonType pT = classifyPolygon(p, result1, c2.getBounds()); + PolygonType pT = classifyPolygon(p, c2.getPolygons(), c2.getBounds()); if (pT == PolygonType.INSIDE) { inC2.add(p); @@ -108,7 +111,7 @@ public static void main(String[] args) throws IOException { List unknownOfC2 = new ArrayList<>(); for (Polygon p : result1) { - PolygonType pT = classifyPolygon(p, result2, c1.getBounds()); + PolygonType pT = classifyPolygon(p, c1.getPolygons(), c1.getBounds()); if (pT == PolygonType.INSIDE) { inC1.add(p); @@ -160,7 +163,7 @@ public static void main(String[] args) throws IOException { public static PolygonType classifyPolygon(Polygon p1, List polygons, Bounds b) { - double TOL = 1e-8; + double TOL = 1e-10; // we are definitely outside if bounding boxes don't intersect if (!p1.getBounds().intersects(b)) { @@ -247,10 +250,24 @@ public static final class RayIntersection { public final Vector3d intersectionPoint; public final Polygon polygon; + public final PlaneIntersection.IntersectionType type; - public RayIntersection(Vector3d intersectionPoint, Polygon polygon) { + public RayIntersection(Vector3d intersectionPoint, + Polygon polygon, PlaneIntersection.IntersectionType type) { this.intersectionPoint = intersectionPoint; this.polygon = polygon; + this.type = type; + } + + + @Override + public String toString() { + return "" + + "[\n" + + " -> point: " + intersectionPoint+"\n" + + " -> polygon-normal: " + polygon.plane.getNormal()+"\n" + + " -> type: " + type + "\n" + + "]"; } } @@ -260,9 +277,9 @@ public static List getPolygonsThatIntersectWithRay( List intersection = new ArrayList<>(); for (Polygon p : polygons) { PlaneIntersection res = computePlaneIntersection(p.plane, point, direction, TOL); - if (res.type == PlaneIntersection.IntersectionType.NON_PARALLEL) { + if (res.point.isPresent()) { if (p.contains(res.point.get())) { - intersection.add(new RayIntersection(res.point.get(), p)); + intersection.add(new RayIntersection(res.point.get(), p, res.type)); } } } @@ -316,10 +333,23 @@ public static PlaneIntersection computePlaneIntersection( } } + /** + * Splits polygons ps2 with planes from polygons ps1. + * + * @param ps1 + * @param ps2 + * @param b1 + * @param b2 + * @return + */ public static List splitPolygons( List ps1, List ps2, Bounds b1, Bounds b2) { + + System.out.println("#ps1: " + ps1.size() + ", #ps2: " + ps2.size()); + + if(ps1.isEmpty()||ps2.isEmpty()) return Collections.EMPTY_LIST; List ps2WithCuts = new ArrayList<>(ps2); @@ -339,95 +369,11 @@ public static List splitPolygons( continue; } -// // compute distance of polygon 1 vertices to polygon 2 plane -// int[] typesP1 = new int[p1.vertices.size()]; -// -// boolean typesEqual1 = true; -// int prevType1 = 0; -// for (int i = 0; i < p1.vertices.size(); i++) { -// typesP1[i] = p2.plane.compare(p1.vertices.get(i).pos, EPS); -// -// if (i > 0 && typesEqual1) { -// typesEqual1 &= prevType1 == typesP1[i]; -// } -// -// prevType1 = typesP1[i]; -// } -// -// // planes do not intersect, thus polygons do not intersect -// if (typesEqual1) { -// continue; -// } - // compute distance of polygon 2 vertices to polygon 1 plane - int[] typesP2 = new int[p2.vertices.size()]; - - boolean typesEqual2 = true; - int prevType2 = 0; - - for (int i = 0; i < p2.vertices.size(); i++) { - typesP2[i] = p1.plane.compare(p2.vertices.get(i).pos, EPS); - - if (i > 0 && typesEqual2) { - typesEqual2 = typesEqual2 && (prevType2 == typesP2[i]); - } - - prevType2 = typesP2[i]; - } - - // planes do not intersect, thus polygons do not intersect - if (typesEqual2) { - continue; - } + List cutsOfP2WithP1 = cutPolygonWithPlane(p2, p1.plane); - // if at least two vertices of convex polygon are on the plane - // then there's no intersection -// int countZeroTypes2 = 0; -// for (int i = 0; i < typesP2.length; i++) { -// if (typesP2[i] == 0) { -// countZeroTypes2++; -// } -// } -// if (countZeroTypes2 > 1) { -// continue; -// } -// if (p1.plane.getNormal().angle(p2.plane.getNormal()) < EPS -// || Math.abs(p1.plane.getNormal().angle(p2.plane.getNormal()) - 180) < EPS) { -// System.err.println("HERE"); -// System.exit(-1); -// continue; -// } else { -// System.err.println("a: " + p1.plane.getNormal().angle(p2.plane.getNormal())); -// } - // cut p2 with plane1 - List segmentPointsP2 = new ArrayList<>(); - List frontPolygonP2 = new ArrayList<>(); - List backPolygonP2 = new ArrayList<>(); - cutPolygonWithPlane(p2, p1.plane, typesP2, - frontPolygonP2, backPolygonP2, segmentPointsP2); -// List segmentPointsP1 = new ArrayList<>(); -// List frontPolygonP1 = new ArrayList<>(); -// List backPolygonP1 = new ArrayList<>(); -// cutPolygonWithPlane(p1, p2.plane, typesP1, -// frontPolygonP1, backPolygonP1, segmentPointsP1); - - // TODO check segment intersection for accurate results - // now, we might have more cuts than necessary - p2ToDelete.add(p2); - - if (frontPolygonP2.size() > 2) { - - Polygon p = Polygon.fromPoints( - frontPolygonP2, p2.getStorage()); - if (p.isValid()) { - cutsWithP1.add(p); - } - } - if (backPolygonP2.size() > 2) { - Polygon p = Polygon.fromPoints( - backPolygonP2, p2.getStorage()); - if (p.isValid()) { - cutsWithP1.add(p); - } + if (!cutsOfP2WithP1.isEmpty()) { + cutsWithP1.addAll(cutsOfP2WithP1); + p2ToDelete.add(p2); } } ps2WithCuts.addAll(cutsWithP1); @@ -437,15 +383,14 @@ public static List splitPolygons( return ps2WithCuts; } - private static void cutPolygonWithPlane(Polygon polygon, Plane cutPlane, + private static void cutPolygonWithPlaneAndTypes(Polygon polygon, Plane cutPlane, int[] vertexTypes, List frontPolygon, List backPolygon, List onPlane) { - System.out.println("polygon: \n" + polygon.toStlString()); - System.out.println("--------------------"); - System.out.println("plane: \n -> p: " + cutPlane.getAnchor() + "\n -> n: " + cutPlane.getNormal()); - System.out.println("--------------------"); - +// System.out.println("polygon: \n" + polygon.toStlString()); +// System.out.println("--------------------"); +// System.out.println("plane: \n -> p: " + cutPlane.getAnchor() + "\n -> n: " + cutPlane.getNormal()); +// System.out.println("--------------------"); for (int i = 0; i < polygon.vertices.size(); i++) { int j = (i + 1) % polygon.vertices.size(); int ti = vertexTypes[i]; @@ -499,23 +444,72 @@ public static void testCut() { Vector3d.xyz(1, 0, 1), Vector3d.xyz(0, 0, 1) ); - Plane plane = Plane.fromPointAndNormal(Vector3d.xyz(0.5, 0, 0.5), Vector3d.xyz(0, 0, 1)); + CSG cube = new Cube(1).toCSG().transformed( + Transform.unity().translate(0.5,-0.55,0.5).rot(Vector3d.ZERO, Vector3d.UNITY, 0)); + + int cubePolyFrom = 0; + int cubePolyTo = 6; + + List cubePolys = cube.getPolygons().subList(cubePolyFrom, cubePolyTo); + + List intersections = + getPolygonsThatIntersectWithRay(p.centroid(), p.plane.getNormal(), cubePolys, EPS); + + System.out.println("my normal: " + p.plane.getNormal()); + + System.out.println("#intersections: " + intersections.size()); + for(RayIntersection ri : intersections) { + System.out.println(ri); + } + + PolygonType pType = classifyPolygon(p, cubePolys, cube.getBounds()); + + System.out.println("#pType:"); + System.out.println(" -> "+pType); + + List cutsWithCube = splitPolygons(cubePolys, + Arrays.asList(p), p.getBounds(), cube.getBounds()); + + cutsWithCube.addAll(cube.getPolygons().subList(cubePolyFrom, cubePolyTo)); + + try { + ObjFile objF = CSG.fromPolygons(cutsWithCube).toObj(3); + objF.toFiles(Paths.get("test-split1.obj")); +// Files.write(Paths.get("test-split1.stl"), +// CSG.fromPolygons(cutsWithP1).toStlString().getBytes()); + } catch (IOException ex) { + Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + } + +// System.exit(0); + } + + private static List cutPolygonWithPlane(Polygon p, Plane plane) { + + boolean typesEqual = true; int types[] = new int[p.vertices.size()]; for (int i = 0; i < p.vertices.size(); i++) { types[i] = plane.compare(p.vertices.get(i).pos, EPS); - System.out.println("type " + i + ": " + types[i]); +// System.out.println("type " + i + ": " + types[i]); + + if (i > 0 && typesEqual) { + typesEqual = typesEqual && (types[i] == types[i - 1]); + } + } + + // planes are parallel, thus polygons do not intersect + if (typesEqual) { + return Collections.EMPTY_LIST; } List front = new ArrayList<>(); List back = new ArrayList<>(); List on = new ArrayList<>(); - - cutPolygonWithPlane(p, plane, types, front, back, on); - + cutPolygonWithPlaneAndTypes(p, plane, types, front, back, on); + List cutsWithP1 = new ArrayList<>(); if (front.size() > 2) { - Polygon frontCut = Polygon.fromPoints( front); if (frontCut.isValid()) { @@ -529,24 +523,7 @@ public static void testCut() { cutsWithP1.add(backCut); } } - - System.out.println("#polygons: " + cutsWithP1.size()); - - for (Polygon polygon : cutsWithP1) { - System.out.println(polygon.toStlString()); - } - - try { - - ObjFile objF = CSG.fromPolygons(cutsWithP1).toObj(); - objF.toFiles(Paths.get("test-split1.obj")); -// Files.write(Paths.get("test-split1.stl"), -// CSG.fromPolygons(cutsWithP1).toStlString().getBytes()); - } catch (IOException ex) { - Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); - } - -// System.exit(0); + return cutsWithP1; } enum PolygonType { From c59f3f97fe0e3421181dd6c10f8062e3ad337001 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sun, 12 Feb 2017 12:26:13 +0100 Subject: [PATCH 10/62] new algorithm seems to work! --- .../eu/mihosoft/jcsg/playground/Main.java | 118 +++++++++++++----- 1 file changed, 88 insertions(+), 30 deletions(-) diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main.java b/src/main/java/eu/mihosoft/jcsg/playground/Main.java index 68200d2f..3460eb08 100644 --- a/src/main/java/eu/mihosoft/jcsg/playground/Main.java +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main.java @@ -42,28 +42,28 @@ public static void main(String[] args) throws IOException { CSG c1 = new Cube(Vector3d.zero(), Vector3d.xyz(1, 1, 1)).toCSG(); CSG c2 = new Cube(Vector3d.xyz(1,1,1), Vector3d.xyz(2, 2, 2)).toCSG() - .transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 16)); + .transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 78)); // Files.write(Paths.get("c1.stl"), c1.toStlString().getBytes()); // Files.write(Paths.get("c2.stl"), c2.toStlString().getBytes()); // c1 = STL.file(Paths.get("c1.stl")); // c2 = STL.file(Paths.get("c2.stl")); -// c1 = new Sphere(Vector3d.x(0.), 0.5, 32, 32).toCSG(); -// c2 = new Sphere(Vector3d.x(0.6), 0.5, 32, 32).toCSG(); -// c2 = new Sphere(Vector3d.x(0.0), 1.25, 32, 32).toCSG(); +// c1 = new Sphere(Vector3d.x(0.), 0.5, 16, 16).toCSG(); +// c2 = new Sphere(Vector3d.x(0.6), 0.5, 16, 16).toCSG(); + c2 = new Sphere(Vector3d.x(0.0), 0.7, 32, 32).toCSG(); List result1 = splitPolygons( c1.getPolygons(), c2.getPolygons(), c1.getBounds(), c2.getBounds() ); List result2 = splitPolygons( - result1, c1.getPolygons(), + c2.getPolygons(), c1.getPolygons(), c2.getBounds(), c1.getBounds() ); - result1 = splitPolygons( - result2, c2.getPolygons(), - c1.getBounds(), c2.getBounds()); + // result1 = splitPolygons( + // result2, c2.getPolygons(), + // c1.getBounds(), c2.getBounds()); List splitted = new ArrayList<>(); splitted.addAll(result1); splitted.addAll(result2); @@ -180,46 +180,84 @@ public static PolygonType classifyPolygon(Polygon p1, List polygons, Bo return PolygonType.OUTSIDE; } - // find the closest polygon to the centroid of p1 which intersects thr + // find the closest polygon to the centroid of p1 which intersects the // ray - RayIntersection min = intersections.get(0); - double prevDist = min.polygon.centroid().minus(rayCenter).magnitude(); + RayIntersection min = null; //intersections.get(0); + double dist = 0; + double prevDist = Double.MAX_VALUE; // min.polygon.centroid().minus(rayCenter).magnitude(); + int i = 0; for (RayIntersection ri : intersections) { - double dist = ri.polygon.centroid().minus(rayCenter).magnitude(); + + int frontOrBack = p1.plane.compare(ri.intersectionPoint, TOL); + + if(frontOrBack < 0) { + // System.out.println(" -> skipping intersection behind ray " + i); + continue; + } + + //try { + // ObjFile objF = CSG.fromPolygons(ri.polygon).toObj(3); + // objF.toFiles(Paths.get("test-intersection-" + i + ".obj")); + //} catch (IOException ex) { + // Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + //} + + dist = ri.polygon.centroid().minus(rayCenter).magnitude(); + + //System.out.println("dist-"+i+": " + dist); + + if(dist < TOL && ri.polygon.plane.getNormal().dot(rayDirection) < TOL ) { + // System.out.println(" -> skipping intersection " + i); + continue; + } + if (dist < prevDist) { prevDist = dist; min = ri; } + + i++; + } + + if (min==null) { + return PolygonType.OUTSIDE; } - int frontOrBack = p1.plane.compare(min.polygon.centroid(), TOL); + // try { + // ObjFile objF = CSG.fromPolygons(min.polygon).toObj(); + // objF.toFiles(Paths.get("test-intersection-min.obj")); + //} catch (IOException ex) { + // Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + //} + + int frontOrBack = p1.plane.compare(min.intersectionPoint, TOL); Vector3d planePoint = p1.plane.getAnchor(); - int towardsOrAwayFrom = p1.plane.compare( + int sameOrOpposite = p1.plane.compare( planePoint.plus(min.polygon.plane.getNormal()), TOL); - if (frontOrBack > 0 && towardsOrAwayFrom < 0) { + if (frontOrBack > 0 && sameOrOpposite > 0) { return PolygonType.INSIDE; } - if (frontOrBack < 0 && towardsOrAwayFrom > 0) { + if (frontOrBack > 0 && sameOrOpposite < 0) { return PolygonType.OUTSIDE; } - if (frontOrBack < 0 && towardsOrAwayFrom < 0) { - return PolygonType.OUTSIDE; + if (frontOrBack < 0 && sameOrOpposite < 0) { + return PolygonType.INSIDE; } - if (frontOrBack > 0 && towardsOrAwayFrom > 0) { - return PolygonType.INSIDE; + if (frontOrBack < 0 && sameOrOpposite > 0) { + return PolygonType.OUTSIDE; } - if (frontOrBack == 0 && towardsOrAwayFrom > 0) { + if (frontOrBack == 0 && sameOrOpposite > 0) { return PolygonType.SAME; } - if (frontOrBack == 0 && towardsOrAwayFrom < 0) { + if (frontOrBack == 0 && sameOrOpposite < 0) { return PolygonType.OPPOSITE; } @@ -419,7 +457,7 @@ private static void cutPolygonWithPlaneAndTypes(Polygon polygon, Plane cutPlane, Vector3d intersectionPoint = pI.point.get(); - System.out.println("i: " + i + ", j: " + j + " : " + intersectionPoint); + // System.out.println("i: " + i + ", j: " + j + " : " + intersectionPoint); // double t = (cutPlane.distance(vi.pos))/vi.pos.minus(vj.pos).magnitude(); // System.out.println("t: " + t + ", i: " + i + ", j: " + j); @@ -445,16 +483,36 @@ public static void testCut() { Vector3d.xyz(0, 0, 1) ); - CSG cube = new Cube(1).toCSG().transformed( - Transform.unity().translate(0.5,-0.55,0.5).rot(Vector3d.ZERO, Vector3d.UNITY, 0)); + try { + CSG pCSG = STL.file(Paths.get("sphere-test-01.stl")); + + p = pCSG.getPolygons().get(0); + } catch(Exception ex) { + // + } + + CSG cube = new Cube(Vector3d.xyz(1,1,1), Vector3d.xyz(2, 2, 2)).toCSG() + .transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 17)); + + cube = new Sphere(Vector3d.x(0.), 0.5, 16, 16).toCSG(); + +// CSG cube = new Cube(1).toCSG().transformed( +// Transform.unity().translate(0.5,-0.55,0.5).rot(Vector3d.ZERO, Vector3d.UNITY, 0) +// ); int cubePolyFrom = 0; int cubePolyTo = 6; - List cubePolys = cube.getPolygons().subList(cubePolyFrom, cubePolyTo); + List cubePolys = cube.getPolygons();//.subList(cubePolyFrom, cubePolyTo); + + System.out.println("p: " + p.toStlString()); + System.out.println("p-centroid: " + p.centroid()); List intersections = - getPolygonsThatIntersectWithRay(p.centroid(), p.plane.getNormal(), cubePolys, EPS); + getPolygonsThatIntersectWithRay( + p.centroid(), + p.plane.getNormal(), + cubePolys, EPS); System.out.println("my normal: " + p.plane.getNormal()); @@ -464,14 +522,14 @@ public static void testCut() { } PolygonType pType = classifyPolygon(p, cubePolys, cube.getBounds()); - + System.out.println("#pType:"); System.out.println(" -> "+pType); List cutsWithCube = splitPolygons(cubePolys, Arrays.asList(p), p.getBounds(), cube.getBounds()); - cutsWithCube.addAll(cube.getPolygons().subList(cubePolyFrom, cubePolyTo)); + cutsWithCube.addAll(cube.getPolygons()/*.subList(cubePolyFrom, cubePolyTo)*/); try { ObjFile objF = CSG.fromPolygons(cutsWithCube).toObj(3); @@ -482,7 +540,7 @@ public static void testCut() { Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); } -// System.exit(0); + // System.exit(0); } private static List cutPolygonWithPlane(Polygon p, Plane plane) { From 95101ecd8c671e413d53bdd2082f1d9a29eaaa05 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sun, 12 Feb 2017 17:57:04 +0100 Subject: [PATCH 11/62] added some additional checks to the new algorithm. --- .../eu/mihosoft/jcsg/playground/Main.java | 226 ++++++++++++++---- 1 file changed, 184 insertions(+), 42 deletions(-) diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main.java b/src/main/java/eu/mihosoft/jcsg/playground/Main.java index 3460eb08..23d4ac66 100644 --- a/src/main/java/eu/mihosoft/jcsg/playground/Main.java +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main.java @@ -13,9 +13,12 @@ import eu.mihosoft.jcsg.STL; import eu.mihosoft.jcsg.Sphere; import eu.mihosoft.jcsg.Vertex; +import eu.mihosoft.vvecmath.ModifiableVector3d; import eu.mihosoft.vvecmath.Plane; import eu.mihosoft.vvecmath.Transform; import eu.mihosoft.vvecmath.Vector3d; + +import javax.lang.model.type.IntersectionType; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; @@ -24,11 +27,11 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; /** - * * @author Michael Hoffer (info@michaelhoffer.de) */ public class Main { @@ -41,7 +44,7 @@ public static void main(String[] args) throws IOException { CSG c1 = new Cube(Vector3d.zero(), Vector3d.xyz(1, 1, 1)).toCSG(); - CSG c2 = new Cube(Vector3d.xyz(1,1,1), Vector3d.xyz(2, 2, 2)).toCSG() + CSG c2 = new Cube(Vector3d.xyz(1, 1, 1), Vector3d.xyz(2, 2, 2)).toCSG() .transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 78)); // Files.write(Paths.get("c1.stl"), c1.toStlString().getBytes()); @@ -50,7 +53,7 @@ public static void main(String[] args) throws IOException { // c2 = STL.file(Paths.get("c2.stl")); // c1 = new Sphere(Vector3d.x(0.), 0.5, 16, 16).toCSG(); // c2 = new Sphere(Vector3d.x(0.6), 0.5, 16, 16).toCSG(); - c2 = new Sphere(Vector3d.x(0.0), 0.7, 32, 32).toCSG(); + c2 = new Sphere(Vector3d.x(0.0), 0.65, 16, 16).toCSG(); List result1 = splitPolygons( c1.getPolygons(), c2.getPolygons(), c1.getBounds(), c2.getBounds() @@ -61,9 +64,10 @@ public static void main(String[] args) throws IOException { c2.getBounds(), c1.getBounds() ); - // result1 = splitPolygons( - // result2, c2.getPolygons(), - // c1.getBounds(), c2.getBounds()); + /* result1 = splitPolygons( + result2, c2.getPolygons(), + c1.getBounds(), c2.getBounds());*/ + List splitted = new ArrayList<>(); splitted.addAll(result1); splitted.addAll(result2); @@ -190,7 +194,7 @@ public static PolygonType classifyPolygon(Polygon p1, List polygons, Bo int frontOrBack = p1.plane.compare(ri.intersectionPoint, TOL); - if(frontOrBack < 0) { + if (frontOrBack < 0) { // System.out.println(" -> skipping intersection behind ray " + i); continue; } @@ -206,7 +210,7 @@ public static PolygonType classifyPolygon(Polygon p1, List polygons, Bo //System.out.println("dist-"+i+": " + dist); - if(dist < TOL && ri.polygon.plane.getNormal().dot(rayDirection) < TOL ) { + if (dist < TOL && ri.polygon.plane.getNormal().dot(rayDirection) < TOL) { // System.out.println(" -> skipping intersection " + i); continue; } @@ -219,7 +223,7 @@ public static PolygonType classifyPolygon(Polygon p1, List polygons, Bo i++; } - if (min==null) { + if (min == null) { return PolygonType.OUTSIDE; } @@ -235,7 +239,8 @@ public static PolygonType classifyPolygon(Polygon p1, List polygons, Bo Vector3d planePoint = p1.plane.getAnchor(); int sameOrOpposite = p1.plane.compare( - planePoint.plus(min.polygon.plane.getNormal()), TOL); + planePoint.plus(min.polygon.plane.getNormal()), TOL + ); if (frontOrBack > 0 && sameOrOpposite > 0) { return PolygonType.INSIDE; @@ -291,19 +296,19 @@ public static final class RayIntersection { public final PlaneIntersection.IntersectionType type; public RayIntersection(Vector3d intersectionPoint, - Polygon polygon, PlaneIntersection.IntersectionType type) { + Polygon polygon, PlaneIntersection.IntersectionType type) { this.intersectionPoint = intersectionPoint; this.polygon = polygon; this.type = type; } - - + + @Override public String toString() { return "" + "[\n" - + " -> point: " + intersectionPoint+"\n" - + " -> polygon-normal: " + polygon.plane.getNormal()+"\n" + + " -> point: " + intersectionPoint + "\n" + + " -> polygon-normal: " + polygon.plane.getNormal() + "\n" + " -> type: " + type + "\n" + "]"; } @@ -384,10 +389,10 @@ public static List splitPolygons( List ps1, List ps2, Bounds b1, Bounds b2) { - + System.out.println("#ps1: " + ps1.size() + ", #ps2: " + ps2.size()); - - if(ps1.isEmpty()||ps2.isEmpty()) return Collections.EMPTY_LIST; + + if (ps1.isEmpty() || ps2.isEmpty()) return Collections.EMPTY_LIST; List ps2WithCuts = new ArrayList<>(ps2); @@ -407,7 +412,40 @@ public static List splitPolygons( continue; } - List cutsOfP2WithP1 = cutPolygonWithPlane(p2, p1.plane); + List cutsOfP2WithP1 = cutPolygonWithPlaneIf(p2, p1.plane, + (Predicate>) segments -> { + + //if(true)return true; + if(segments.size()!=2) return true; + + Vector3d s1 = segments.get(0); + Vector3d s2 = segments.get(1); + + int numIntersectionsPoly1 = 0; + for(int i = 0; i< p1.vertices.size()-1;i++) { + //System.out.println("i,j : " + i + ", " + (i+1%p1.vertices.size())); + Vector3d e1 = p1.vertices.get(i).pos; + Vector3d e2 = p1.vertices.get(i+1%p1.vertices.size()).pos; + LineIntersectionResult iRes = calculateLineLineIntersection(e1,e2,s1,s2); + if(iRes.type == LineIntersectionResult.IntersectionType.INTERSECTING && + p1.contains(iRes.segmentPoint1.get())) { + numIntersectionsPoly1++; + } + } + + int numIntersectionsPoly2 = 0; + for(int i = 0; i< p2.vertices.size()-1;i++) { + Vector3d e1 = p2.vertices.get(i).pos; + Vector3d e2 = p2.vertices.get(i+1%p2.vertices.size()).pos; + LineIntersectionResult iRes = calculateLineLineIntersection(e1,e2,s1,s2); + if(iRes.type == LineIntersectionResult.IntersectionType.INTERSECTING && + p2.contains(iRes.segmentPoint1.get())) { + numIntersectionsPoly2++; + } + } + + return numIntersectionsPoly1 > 0 && numIntersectionsPoly2 > 0; + }); if (!cutsOfP2WithP1.isEmpty()) { cutsWithP1.addAll(cutsOfP2WithP1); @@ -421,9 +459,10 @@ public static List splitPolygons( return ps2WithCuts; } + private static void cutPolygonWithPlaneAndTypes(Polygon polygon, Plane cutPlane, - int[] vertexTypes, List frontPolygon, - List backPolygon, List onPlane) { + int[] vertexTypes, List frontPolygon, + List backPolygon, List onPlane) { // System.out.println("polygon: \n" + polygon.toStlString()); // System.out.println("--------------------"); @@ -457,16 +496,6 @@ private static void cutPolygonWithPlaneAndTypes(Polygon polygon, Plane cutPlane, Vector3d intersectionPoint = pI.point.get(); - // System.out.println("i: " + i + ", j: " + j + " : " + intersectionPoint); - -// double t = (cutPlane.distance(vi.pos))/vi.pos.minus(vj.pos).magnitude(); -// System.out.println("t: " + t + ", i: " + i + ", j: " + j); -// System.out.println(" : vi-plane-dist: " + cutPlane.distance(vi.pos) + ", vi-vj-dist: " + vi.pos.minus(vj.pos).magnitude()); -// System.out.println(" : vi: " + vi.pos.toStlString()); -// System.out.println(" : vj: " + vj.pos.toStlString()); -// System.out.println(" : " + cutPlane.distance(vi.pos) + " , " + cutPlane.distance(vj.pos)); -// Vertex v = vi.interpolate(vj, t); -// System.out.println(" : vs: " + v); frontPolygon.add(intersectionPoint); backPolygon.add(intersectionPoint); onPlane.add(intersectionPoint); @@ -487,11 +516,11 @@ public static void testCut() { CSG pCSG = STL.file(Paths.get("sphere-test-01.stl")); p = pCSG.getPolygons().get(0); - } catch(Exception ex) { + } catch (Exception ex) { // } - CSG cube = new Cube(Vector3d.xyz(1,1,1), Vector3d.xyz(2, 2, 2)).toCSG() + CSG cube = new Cube(Vector3d.xyz(1, 1, 1), Vector3d.xyz(2, 2, 2)).toCSG() .transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 17)); cube = new Sphere(Vector3d.x(0.), 0.5, 16, 16).toCSG(); @@ -508,24 +537,24 @@ public static void testCut() { System.out.println("p: " + p.toStlString()); System.out.println("p-centroid: " + p.centroid()); - List intersections = + List intersections = getPolygonsThatIntersectWithRay( p.centroid(), p.plane.getNormal(), cubePolys, EPS); - + System.out.println("my normal: " + p.plane.getNormal()); - + System.out.println("#intersections: " + intersections.size()); - for(RayIntersection ri : intersections) { + for (RayIntersection ri : intersections) { System.out.println(ri); } - + PolygonType pType = classifyPolygon(p, cubePolys, cube.getBounds()); System.out.println("#pType:"); - System.out.println(" -> "+pType); - + System.out.println(" -> " + pType); + List cutsWithCube = splitPolygons(cubePolys, Arrays.asList(p), p.getBounds(), cube.getBounds()); @@ -540,10 +569,19 @@ public static void testCut() { Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); } + ModifiableVector3d segmentP1 = Vector3d.zero().asModifiable(); + ModifiableVector3d segmentP2 = Vector3d.zero().asModifiable(); + LineIntersectionResult lineRes = calculateLineLineIntersection( + Vector3d.xyz(-1, 0, 0), Vector3d.xyz(1, 0, 0), + Vector3d.xyz(0, -1, 0), Vector3d.xyz(0, 1, 0)); + + System.out.println("l1 intersect l2: "); + System.out.println(lineRes); + // System.exit(0); } - private static List cutPolygonWithPlane(Polygon p, Plane plane) { + private static List cutPolygonWithPlaneIf(Polygon p, Plane plane, Predicate> check) { boolean typesEqual = true; int types[] = new int[p.vertices.size()]; @@ -565,7 +603,15 @@ private static List cutPolygonWithPlane(Polygon p, Plane plane) { List back = new ArrayList<>(); List on = new ArrayList<>(); cutPolygonWithPlaneAndTypes(p, plane, types, front, back, on); - + + boolean checkResult = check == null; + + if (check != null) { + checkResult = check.test(on); + } + + if (!checkResult) return Collections.EMPTY_LIST; + List cutsWithP1 = new ArrayList<>(); if (front.size() > 2) { Polygon frontCut = Polygon.fromPoints( @@ -591,4 +637,100 @@ enum PolygonType { OPPOSITE, SAME } + + + static class LineIntersectionResult { + + public final IntersectionType type; + + public final Optional segmentPoint1; + public final Optional segmentPoint2; + + LineIntersectionResult(IntersectionType type, Vector3d segmentPoint1, Vector3d segmentPoint2) { + this.type = type; + this.segmentPoint1 = Optional.ofNullable(segmentPoint1); + this.segmentPoint2 = Optional.ofNullable(segmentPoint2); + } + + static enum IntersectionType { + PARALLEL, + NON_PARALLEL, + INTERSECTING + } + + static final LineIntersectionResult PARALLEL = + new LineIntersectionResult(IntersectionType.PARALLEL, null, null); + + @Override + public String toString() { + return "[\n -> type: " + type + + "\n -> segmentP1: " + (segmentPoint1.isPresent() ? segmentPoint1.get() : "none") + + "\n -> segmentP2: " + (segmentPoint2.isPresent() ? segmentPoint2.get() : "none") + + "\n]"; + } + } + + /** + * Calculates the intersection line segment between two lines. + * + * @param line1Point1 + * @param line1Point2 + * @param line2Point1 + * @param line2Point2 + * @return {@code true} if the intersection line segment exists; {@code false} otherwise + */ + public static LineIntersectionResult calculateLineLineIntersection(Vector3d line1Point1, Vector3d line1Point2, + Vector3d line2Point1, Vector3d line2Point2) { + // Algorithm is ported from the C algorithm of + // Paul Bourke at http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline3d/ + + Vector3d p1 = line1Point1; + Vector3d p2 = line1Point2; + Vector3d p3 = line2Point1; + Vector3d p4 = line2Point2; + Vector3d p13 = p1.minus(p3); + Vector3d p43 = p4.minus(p3); + + if (p43.magnitudeSq() < EPS) { + return LineIntersectionResult.PARALLEL; + } + Vector3d p21 = p2.minus(p1); + if (p21.magnitudeSq() < EPS) { + return LineIntersectionResult.PARALLEL; + } + + double d1343 = p13.x() * (double) p43.x() + (double) p13.y() * p43.y() + (double) p13.z() * p43.z(); + double d4321 = p43.x() * (double) p21.x() + (double) p43.y() * p21.y() + (double) p43.z() * p21.z(); + double d1321 = p13.x() * (double) p21.x() + (double) p13.y() * p21.y() + (double) p13.z() * p21.z(); + double d4343 = p43.x() * (double) p43.x() + (double) p43.y() * p43.y() + (double) p43.z() * p43.z(); + double d2121 = p21.x() * (double) p21.x() + (double) p21.y() * p21.y() + (double) p21.z() * p21.z(); + + double denom = d2121 * d4343 - d4321 * d4321; + if (Math.abs(denom) < EPS) { + return LineIntersectionResult.PARALLEL; + } + double numer = d1343 * d4321 - d1321 * d4343; + + double mua = numer / denom; + double mub = (d1343 + d4321 * (mua)) / d4343; + + ModifiableVector3d resultSegmentPoint1 = Vector3d.zero().asModifiable(); + ModifiableVector3d resultSegmentPoint2 = Vector3d.zero().asModifiable(); + + resultSegmentPoint1.setX(p1.x() + mua * p21.x()); + resultSegmentPoint1.setY(p1.y() + mua * p21.y()); + resultSegmentPoint1.setZ(p1.z() + mua * p21.z()); + resultSegmentPoint2.setX(p3.x() + mub * p43.x()); + resultSegmentPoint2.setY(p3.y() + mub * p43.y()); + resultSegmentPoint2.setZ(p3.z() + mub * p43.z()); + + if (resultSegmentPoint1.equals(resultSegmentPoint2)) { + return new LineIntersectionResult(LineIntersectionResult.IntersectionType.INTERSECTING, + resultSegmentPoint1, resultSegmentPoint2); + } else { + return new LineIntersectionResult(LineIntersectionResult.IntersectionType.NON_PARALLEL, + resultSegmentPoint1, resultSegmentPoint2); + } + } + } From 3800f3dea6c4b17ca44943bc3de6e6b839ea2af1 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sun, 12 Feb 2017 23:15:37 +0100 Subject: [PATCH 12/62] uses new vvecmath (should compile again) --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index c094c281..981fd22e 100644 --- a/build.gradle +++ b/build.gradle @@ -64,8 +64,8 @@ dependencies { compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4' compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4', classifier: 'sources' - compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.3-SNAPSHOT' - compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.3-SNAPSHOT', classifier: 'sources' + compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.3' + compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.3', classifier: 'sources' compile 'org.slf4j:slf4j-simple:1.6.1' } From dadf4f00707160a15caf654cb5471b3bee003b2b Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Tue, 14 Feb 2017 00:25:59 +0100 Subject: [PATCH 13/62] works with newest vvecmath library (TODO improve usage of stored/mutable vectors) --- build.gradle | 4 ++-- src/main/java/eu/mihosoft/jcsg/Cylinder.java | 4 ++-- src/main/java/eu/mihosoft/jcsg/Extrude.java | 2 +- src/main/java/eu/mihosoft/jcsg/Plane.java | 2 +- src/main/java/eu/mihosoft/jcsg/Polygon.java | 4 ++-- .../java/eu/mihosoft/jcsg/samples/FractalStructure.java | 6 +++--- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 981fd22e..e990cf17 100644 --- a/build.gradle +++ b/build.gradle @@ -64,8 +64,8 @@ dependencies { compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4' compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4', classifier: 'sources' - compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.3' - compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.3', classifier: 'sources' + compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.6' + compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.6', classifier: 'sources' compile 'org.slf4j:slf4j-simple:1.6.1' } diff --git a/src/main/java/eu/mihosoft/jcsg/Cylinder.java b/src/main/java/eu/mihosoft/jcsg/Cylinder.java index 80ebfad7..122a12da 100644 --- a/src/main/java/eu/mihosoft/jcsg/Cylinder.java +++ b/src/main/java/eu/mihosoft/jcsg/Cylinder.java @@ -150,8 +150,8 @@ public List toPolygons() { final Vector3d axisZ = ray.normalized(); boolean isY = (Math.abs(axisZ.y()) > 0.5); final Vector3d axisX = Vector3d.xyz(isY ? 1 : 0, !isY ? 1 : 0, 0). - cross(axisZ).normalized(); - final Vector3d axisY = axisX.cross(axisZ).normalized(); + crossed(axisZ).normalized(); + final Vector3d axisY = axisX.crossed(axisZ).normalized(); Vertex startV = new Vertex(s, axisZ.negated()); Vertex endV = new Vertex(e, axisZ.normalized()); List polygons = new ArrayList<>(); diff --git a/src/main/java/eu/mihosoft/jcsg/Extrude.java b/src/main/java/eu/mihosoft/jcsg/Extrude.java index 1b0a32ba..a18b612f 100644 --- a/src/main/java/eu/mihosoft/jcsg/Extrude.java +++ b/src/main/java/eu/mihosoft/jcsg/Extrude.java @@ -205,7 +205,7 @@ public static boolean isCCW(Polygon polygon) { private static double normalizedX(Vector3d v1, Vector3d v2) { Vector3d v2MinusV1 = v2.minus(v1); - return v2MinusV1.dividedBy(v2MinusV1.magnitude()).times(Vector3d.X_ONE).x(); + return v2MinusV1.divided(v2MinusV1.magnitude()).times(Vector3d.X_ONE).x(); } // public static void main(String[] args) { diff --git a/src/main/java/eu/mihosoft/jcsg/Plane.java b/src/main/java/eu/mihosoft/jcsg/Plane.java index 609cde4c..37f1407c 100644 --- a/src/main/java/eu/mihosoft/jcsg/Plane.java +++ b/src/main/java/eu/mihosoft/jcsg/Plane.java @@ -94,7 +94,7 @@ public Plane(Vector3d normal, double dist) { * @return a plane */ public static Plane createFromPoints(Vector3d a, Vector3d b, Vector3d c) { - Vector3d n = b.minus(a).cross(c.minus(a)).normalized(); + Vector3d n = b.minus(a).crossed(c.minus(a)).normalized(); return new Plane(n, n.dot(a)); } diff --git a/src/main/java/eu/mihosoft/jcsg/Polygon.java b/src/main/java/eu/mihosoft/jcsg/Polygon.java index 792b5a77..fca3d233 100644 --- a/src/main/java/eu/mihosoft/jcsg/Polygon.java +++ b/src/main/java/eu/mihosoft/jcsg/Polygon.java @@ -279,7 +279,7 @@ public Polygon translate(Vector3d v) { Vector3d c = this.vertices.get(2).pos; // TODO plane update correct? - this._csg_plane.normal = b.minus(a).cross(c.minus(a)); + this._csg_plane.normal = b.minus(a).crossed(c.minus(a)); this.plane = eu.mihosoft.vvecmath.Plane. fromPointAndNormal(centroid(), _csg_plane.normal); @@ -322,7 +322,7 @@ public Polygon transform(Transform transform) { Vector3d b = this.vertices.get(1).pos; Vector3d c = this.vertices.get(2).pos; - this._csg_plane.normal = b.minus(a).cross(c.minus(a)).normalized(); + this._csg_plane.normal = b.minus(a).crossed(c.minus(a)).normalized(); this._csg_plane.dist = this._csg_plane.normal.dot(a); this.plane = eu.mihosoft.vvecmath.Plane. diff --git a/src/main/java/eu/mihosoft/jcsg/samples/FractalStructure.java b/src/main/java/eu/mihosoft/jcsg/samples/FractalStructure.java index 6191837c..21fb9d28 100644 --- a/src/main/java/eu/mihosoft/jcsg/samples/FractalStructure.java +++ b/src/main/java/eu/mihosoft/jcsg/samples/FractalStructure.java @@ -148,10 +148,10 @@ public FractalStructure(Vector3d groundCenter, Vector3d topCenter, && Math.abs(orthoVecToRotAxis2.dot(rotationAxis)) < orthoThreshhold) { this.orthoVecToRotAxis2 = orthoVecToRotAxis2.normalized(); } else { - this.orthoVecToRotAxis2 = rotationAxis.cross(this.orthoVecToRotAxis1).normalized(); + this.orthoVecToRotAxis2 = rotationAxis.crossed(this.orthoVecToRotAxis1).normalized(); } } else { - this.orthoVecToRotAxis2 = rotationAxis.cross(this.orthoVecToRotAxis1).normalized(); + this.orthoVecToRotAxis2 = rotationAxis.crossed(this.orthoVecToRotAxis1).normalized(); } // x, y, z @@ -445,7 +445,7 @@ private ArrayList createSubStructures() { helpCenterPoint = connectionLineVectorNormalized.times(j).plus(connectionLineVectorNormalized.times(stepSizeOnConnectionLineHalf)).plus(centerGroundPoint).plus(correctionInRotationAxisDirection); if (secondOrthoVec == null) { - secondOrthoVec = connectionLineVectorNormalized.cross(helpCenterPoint.minus(helpEdgePoint)); + secondOrthoVec = connectionLineVectorNormalized.crossed(helpCenterPoint.minus(helpEdgePoint)); } // prevent that the last cross connactions from bottom left to top right has a to above end point in the top plane From cf8b8dae0070faefd5e779a16e5099a9ab91966d Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Tue, 14 Feb 2017 22:41:14 +0100 Subject: [PATCH 14/62] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c3c02ae..6f9f8b08 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Navigate to the [Gradle](http://www.gradle.org/) project (e.g., `path/to/JCSG`) #### Bash (Linux/OS X/Cygwin/other Unix-like shell) - sh gradlew assemble + bash gradlew assemble #### Windows (CMD) From 98d1dbc8c85d8b739a5f6934845438912a0c74a9 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Wed, 21 Jun 2017 10:18:09 +0200 Subject: [PATCH 15/62] Version 0.5.3 with support for vvecmath 0.3.6 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 576bd381..5757c283 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group = eu.mihosoft.vrl.jcsg -version = 0.5.3-SNAPSHOT +version = 0.5.3 From 10aecbf588a1b98006f2702a77c6d489058b078f Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Wed, 21 Jun 2017 10:19:35 +0200 Subject: [PATCH 16/62] we are on snapshot again --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5757c283..09fe08de 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group = eu.mihosoft.vrl.jcsg -version = 0.5.3 +version = 0.5.4-SNAPSHOT From fb47ebd13b54a8f91e8e1dda0b5e6bd0a49d4769 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Wed, 21 Jun 2017 10:26:25 +0200 Subject: [PATCH 17/62] license header was missing in some files --- build.gradle | 2 +- src/main/java/eu/mihosoft/jcsg/Bounds.java | 36 +++++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index e990cf17..5b96d635 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ sourceCompatibility = '1.8' //repairHeaders.licenseHeaderText = new File(projectDir,'./license-template.txt') task wrapper(type: Wrapper, description: 'Creates and deploys the Gradle wrapper to the current directory.') { - gradleVersion = '3.3' + gradleVersion = '4.0' } if (!hasProperty('mainClass')) { diff --git a/src/main/java/eu/mihosoft/jcsg/Bounds.java b/src/main/java/eu/mihosoft/jcsg/Bounds.java index ad6da711..2f6efcb1 100644 --- a/src/main/java/eu/mihosoft/jcsg/Bounds.java +++ b/src/main/java/eu/mihosoft/jcsg/Bounds.java @@ -1,7 +1,35 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. +/** + * Bounds.java + * + * Copyright 2014-2017 Michael Hoffer . All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY Michael Hoffer "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL Michael Hoffer OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are + * those of the authors and should not be interpreted as representing official + * policies, either expressed or implied, of Michael Hoffer + * . */ package eu.mihosoft.jcsg; From 61922aa700605b18dfb9c3d03034b4cc2e51eb84 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Wed, 21 Jun 2017 10:42:15 +0200 Subject: [PATCH 18/62] uses gradle 4.0 --- gradle/wrapper/gradle-wrapper.jar | Bin 52928 -> 54706 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 23 ++-- gradlew.bat | 168 +++++++++++------------ 4 files changed, 99 insertions(+), 96 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 6ffa237849ef3607e39c3b334a92a65367962071..51ef3dbf64e5112650805cbdfb5141198ff31212 100644 GIT binary patch delta 24210 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 y>p>p~0xEqh_zY?G+DvhONNUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From 7aafea64f66a9131366878eb838bf3eeb5a3c404 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Thu, 22 Jun 2017 12:42:39 +0200 Subject: [PATCH 19/62] uses new vvecmath version with improved api for creating vectors --- build.gradle | 4 ++-- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 5b96d635..35f82874 100644 --- a/build.gradle +++ b/build.gradle @@ -64,8 +64,8 @@ dependencies { compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4' compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4', classifier: 'sources' - compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.6' - compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.6', classifier: 'sources' + compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.7' + compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.7', classifier: 'sources' compile 'org.slf4j:slf4j-simple:1.6.1' } diff --git a/gradle.properties b/gradle.properties index 09fe08de..410cc2dc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group = eu.mihosoft.vrl.jcsg -version = 0.5.4-SNAPSHOT +version = 0.5.4 From e3d2df99fb885f98c046df3e55f0ff5c0fe77a2d Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Thu, 22 Jun 2017 12:44:19 +0200 Subject: [PATCH 20/62] snapshot --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 410cc2dc..09fe08de 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group = eu.mihosoft.vrl.jcsg -version = 0.5.4 +version = 0.5.4-SNAPSHOT From b0c8a59feae9d4a6a4ccc7cd1d61578af55acb95 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Thu, 22 Jun 2017 14:12:22 +0200 Subject: [PATCH 21/62] uses new vvecmath version --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 35f82874..6d331ab6 100644 --- a/build.gradle +++ b/build.gradle @@ -64,8 +64,8 @@ dependencies { compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4' compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4', classifier: 'sources' - compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.7' - compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.7', classifier: 'sources' + compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.8' + compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.8', classifier: 'sources' compile 'org.slf4j:slf4j-simple:1.6.1' } From 4c8e76828ff613e1e8f61aa885232d36ab2b0daa Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Thu, 22 Jun 2017 14:13:30 +0200 Subject: [PATCH 22/62] uses new vvecmath version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 09fe08de..71e3393e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group = eu.mihosoft.vrl.jcsg -version = 0.5.4-SNAPSHOT +version = 0.5.5 From 44d50fbd344d3b86dae5667e07f3d26093a4fab9 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Thu, 22 Jun 2017 14:15:23 +0200 Subject: [PATCH 23/62] snapshot --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 71e3393e..e24bc5f2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group = eu.mihosoft.vrl.jcsg -version = 0.5.5 +version = 0.5.6-SNAPSHOT From 28979c24b1d253205454e7c05a482d018d15f757 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Mon, 26 Jun 2017 12:40:40 +0200 Subject: [PATCH 24/62] Extrusion API improved. This will make extruding along paths much easier to implement. --- gradle.properties | 2 +- src/main/java/eu/mihosoft/jcsg/Extrude.java | 174 +++++++++++++++++++- 2 files changed, 174 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index e24bc5f2..85ed38e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group = eu.mihosoft.vrl.jcsg -version = 0.5.6-SNAPSHOT +version = 0.5.6 diff --git a/src/main/java/eu/mihosoft/jcsg/Extrude.java b/src/main/java/eu/mihosoft/jcsg/Extrude.java index a18b612f..93a4691d 100644 --- a/src/main/java/eu/mihosoft/jcsg/Extrude.java +++ b/src/main/java/eu/mihosoft/jcsg/Extrude.java @@ -1,7 +1,7 @@ /** * Extrude.java * - * Copyright 2014-2014 Michael Hoffer . All rights + * Copyright 2014-2017 Michael Hoffer . All rights * reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,6 +33,7 @@ */ package eu.mihosoft.jcsg; +import eu.mihosoft.vvecmath.Transform; import eu.mihosoft.vvecmath.Vector3d; import eu.mihosoft.jcsg.ext.org.poly2tri.PolygonUtil; import java.util.ArrayList; @@ -82,6 +83,105 @@ public static CSG points(Vector3d dir, List points) { return extrude(dir, Polygon.fromPoints(toCCW(newList))); } + + /** + * Extrudes the specified path (convex or concave polygon without holes or + * intersections, specified in CCW) into the specified direction. + * + * @param dir direction + * @param points path (convex or concave polygon without holes or + * intersections) + * + * @return a list containing the extruded polygon + */ + public static List points(Vector3d dir, boolean top, boolean bottom, Vector3d... points) { + + return extrude(dir, Polygon.fromPoints(toCCW(Arrays.asList(points))), top, bottom); + } + + /** + * Extrudes the specified path (convex or concave polygon without holes or + * intersections, specified in CCW) into the specified direction. + * + * @param dir direction + * @param points1 path (convex or concave polygon without holes or + * intersections) + * @param points1 path (convex or concave polygon without holes or + * intersections) + * + * @return a list containing the extruded polygon + */ + public static List points(Vector3d dir, boolean top, boolean bottom, List points1) { + + List newList1 = new ArrayList<>(points1); + + return extrude(dir, Polygon.fromPoints(toCCW(newList1)), top, bottom); + } + + /** + * Combines two polygons into one CSG object. Polygons p1 and p2 are treated as top and + * bottom of a tube segment with p1 and p2 as the profile. Note: both polygons must have the + * same number of vertices. This method does not guarantee intersection-free CSGs. It is in the + * responsibility of the caller to ensure that the orientation of p1 and p2 allow for + * intersection-free combination of both. + * + * @param p1 first polygon + * @param p2 second polygon + * @return List of polygons + */ + public static CSG combine(Polygon p1, Polygon p2) { + return CSG.fromPolygons(combine(p1,p2,true,true)); + } + + /** + * Combines two polygons into one CSG object. Polygons p1 and p2 are treated as top and + * bottom of a tube segment with p1 and p2 as the profile. Note: both polygons must have the + * same number of vertices. This method does not guarantee intersection-free CSGs. It is in the + * responsibility of the caller to ensure that the orientation of p1 and p2 allow for + * intersection-free combination of both. + * + * @param p1 first polygon + * @param p2 second polygon + * @param bottom defines whether to close the bottom of the tube + * @param top defines whether to close the top of the tube + * @return List of polygons + */ + public static List combine(Polygon p1, Polygon p2, boolean bottom, boolean top) { + List newPolygons = new ArrayList<>(); + + if (p1.vertices.size() != p2.vertices.size()) { + throw new RuntimeException("Polygons must have the same number of vertices"); + } + + int numVertices = p1.vertices.size(); + + if (bottom) { + newPolygons.add(p1.flipped()); + } + + for (int i = 0; i < numVertices; i++) { + + int nexti = (i + 1) % numVertices; + + Vector3d bottomV1 = p1.vertices.get(i).pos; + Vector3d topV1 = p2.vertices.get(i).pos; + Vector3d bottomV2 = p1.vertices.get(nexti).pos; + Vector3d topV2 = p2.vertices.get(nexti).pos; + + List pPoints; + + pPoints = Arrays.asList(bottomV2, topV2, topV1); + newPolygons.add(Polygon.fromPoints(pPoints, p1.getStorage())); + pPoints = Arrays.asList(bottomV2, topV1, bottomV1); + newPolygons.add(Polygon.fromPoints(pPoints, p1.getStorage())); + } + + if (top) { + newPolygons.add(p2); + } + + return newPolygons; + } private static CSG extrude(Vector3d dir, Polygon polygon1) { List newPolygons = new ArrayList<>(); @@ -118,6 +218,78 @@ private static CSG extrude(Vector3d dir, Polygon polygon1) { } + + private static List extrude(Vector3d dir, Polygon polygon1, boolean top, boolean bottom) { + List newPolygons = new ArrayList<>(); + + + if (bottom) { + newPolygons.addAll(PolygonUtil.concaveToConvex(polygon1)); + } + + Polygon polygon2 = polygon1.translated(dir); + + Transform rot = Transform.unity(); + + Vector3d a = polygon2.plane.getNormal().normalized(); + Vector3d b = dir.normalized(); + + Vector3d c = a.crossed(b); + + double l = c.magnitude(); // sine of angle + + if (l > 1e-9) { + + Vector3d axis = c.times(1.0 / l); + double angle = a.angle(b); + + double sx = 0; + double sy = 0; + double sz = 0; + + int n = polygon2.vertices.size(); + + for (Vertex v : polygon2.vertices) { + sx += v.pos.x(); + sy += v.pos.y(); + sz += v.pos.z(); + } + + Vector3d center = Vector3d.xyz(sx / n, sy / n, sz / n); + + rot = rot.rot(center, axis, angle * Math.PI / 180.0); + + for (Vertex v : polygon2.vertices) { + v.pos = rot.transform(v.pos); + } + } + + int numvertices = polygon1.vertices.size(); + for (int i = 0; i < numvertices; i++) { + + int nexti = (i + 1) % numvertices; + + Vector3d bottomV1 = polygon1.vertices.get(i).pos; + Vector3d topV1 = polygon2.vertices.get(i).pos; + Vector3d bottomV2 = polygon1.vertices.get(nexti).pos; + Vector3d topV2 = polygon2.vertices.get(nexti).pos; + + List pPoints = Arrays.asList(bottomV2, topV2, topV1, bottomV1); + + newPolygons.add(Polygon.fromPoints(pPoints, polygon1.getStorage())); + } + + polygon2 = polygon2.flipped(); + List topPolygons = PolygonUtil.concaveToConvex(polygon2); + if (top) { + newPolygons.addAll(topPolygons); + } + + return newPolygons; + + } + + static List toCCW(List points) { List result = new ArrayList<>(points); From 668f1d90cbfcdd9b24e389d5e94fb197805af88d Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Mon, 26 Jun 2017 12:42:16 +0200 Subject: [PATCH 25/62] snapshot --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 85ed38e0..d6c234ff 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group = eu.mihosoft.vrl.jcsg -version = 0.5.6 +version = 0.5.7-SNAPSHOT From bf4ea99e379eaa8a99ec99410e982325cb84b754 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Fri, 7 Jul 2017 00:02:45 +0200 Subject: [PATCH 26/62] documentation improved --- src/main/java/eu/mihosoft/jcsg/Extrude.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/eu/mihosoft/jcsg/Extrude.java b/src/main/java/eu/mihosoft/jcsg/Extrude.java index a18b612f..5cfe47e0 100644 --- a/src/main/java/eu/mihosoft/jcsg/Extrude.java +++ b/src/main/java/eu/mihosoft/jcsg/Extrude.java @@ -140,6 +140,12 @@ static List toCW(List points) { return result; } + /** + * Indicates whether the specified polygon is defined counter-clockwise. + * @param polygon polygon + * @return {@code true} if the specified polygon is defined counter-clockwise; + * {@code false} otherwise + */ public static boolean isCCW(Polygon polygon) { // thanks to Sepp Reiter for explaining me the algorithm! From fb0ebd822eca487c10deee85cf51b40e5aa677aa Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Wed, 12 Jul 2017 09:04:59 +0800 Subject: [PATCH 27/62] encapsulated polygon plane --- src/main/java/eu/mihosoft/jcsg/Extrude.java | 2 +- src/main/java/eu/mihosoft/jcsg/Polygon.java | 11 ++++++++- .../eu/mihosoft/jcsg/playground/Main.java | 24 +++++++++---------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/main/java/eu/mihosoft/jcsg/Extrude.java b/src/main/java/eu/mihosoft/jcsg/Extrude.java index ae2d94a8..c0072b03 100644 --- a/src/main/java/eu/mihosoft/jcsg/Extrude.java +++ b/src/main/java/eu/mihosoft/jcsg/Extrude.java @@ -231,7 +231,7 @@ private static List extrude(Vector3d dir, Polygon polygon1, boolean top Transform rot = Transform.unity(); - Vector3d a = polygon2.plane.getNormal().normalized(); + Vector3d a = polygon2.getPlane().getNormal().normalized(); Vector3d b = dir.normalized(); Vector3d c = a.crossed(b); diff --git a/src/main/java/eu/mihosoft/jcsg/Polygon.java b/src/main/java/eu/mihosoft/jcsg/Polygon.java index fca3d233..32a41866 100644 --- a/src/main/java/eu/mihosoft/jcsg/Polygon.java +++ b/src/main/java/eu/mihosoft/jcsg/Polygon.java @@ -65,7 +65,16 @@ public final class Polygon { * Note: uses first three vertices to define the plane. */ public final Plane _csg_plane; - public eu.mihosoft.vvecmath.Plane plane; + private eu.mihosoft.vvecmath.Plane plane; + + /** + * Returns the plane defined by this triangle. + * + * @return plane + */ + public eu.mihosoft.vvecmath.Plane getPlane() { + return plane; + } void setStorage(PropertyStorage storage) { this.shared = storage; diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main.java b/src/main/java/eu/mihosoft/jcsg/playground/Main.java index 23d4ac66..0b122874 100644 --- a/src/main/java/eu/mihosoft/jcsg/playground/Main.java +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main.java @@ -175,7 +175,7 @@ public static PolygonType classifyPolygon(Polygon p1, List polygons, Bo } Vector3d rayCenter = p1.centroid(); - Vector3d rayDirection = p1.plane.getNormal(); + Vector3d rayDirection = p1.getPlane().getNormal(); List intersections = getPolygonsThatIntersectWithRay( rayCenter, rayDirection, polygons, TOL); @@ -192,7 +192,7 @@ public static PolygonType classifyPolygon(Polygon p1, List polygons, Bo int i = 0; for (RayIntersection ri : intersections) { - int frontOrBack = p1.plane.compare(ri.intersectionPoint, TOL); + int frontOrBack = p1.getPlane().compare(ri.intersectionPoint, TOL); if (frontOrBack < 0) { // System.out.println(" -> skipping intersection behind ray " + i); @@ -210,7 +210,7 @@ public static PolygonType classifyPolygon(Polygon p1, List polygons, Bo //System.out.println("dist-"+i+": " + dist); - if (dist < TOL && ri.polygon.plane.getNormal().dot(rayDirection) < TOL) { + if (dist < TOL && ri.polygon.getPlane().getNormal().dot(rayDirection) < TOL) { // System.out.println(" -> skipping intersection " + i); continue; } @@ -234,12 +234,12 @@ public static PolygonType classifyPolygon(Polygon p1, List polygons, Bo // Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); //} - int frontOrBack = p1.plane.compare(min.intersectionPoint, TOL); + int frontOrBack = p1.getPlane().compare(min.intersectionPoint, TOL); - Vector3d planePoint = p1.plane.getAnchor(); + Vector3d planePoint = p1.getPlane().getAnchor(); - int sameOrOpposite = p1.plane.compare( - planePoint.plus(min.polygon.plane.getNormal()), TOL + int sameOrOpposite = p1.getPlane().compare( + planePoint.plus(min.polygon.getPlane().getNormal()), TOL ); if (frontOrBack > 0 && sameOrOpposite > 0) { @@ -308,7 +308,7 @@ public String toString() { return "" + "[\n" + " -> point: " + intersectionPoint + "\n" - + " -> polygon-normal: " + polygon.plane.getNormal() + "\n" + + " -> polygon-normal: " + polygon.getPlane().getNormal() + "\n" + " -> type: " + type + "\n" + "]"; } @@ -319,7 +319,7 @@ public static List getPolygonsThatIntersectWithRay( Vector3d point, Vector3d direction, List polygons, double TOL) { List intersection = new ArrayList<>(); for (Polygon p : polygons) { - PlaneIntersection res = computePlaneIntersection(p.plane, point, direction, TOL); + PlaneIntersection res = computePlaneIntersection(p.getPlane(), point, direction, TOL); if (res.point.isPresent()) { if (p.contains(res.point.get())) { intersection.add(new RayIntersection(res.point.get(), p, res.type)); @@ -412,7 +412,7 @@ public static List splitPolygons( continue; } - List cutsOfP2WithP1 = cutPolygonWithPlaneIf(p2, p1.plane, + List cutsOfP2WithP1 = cutPolygonWithPlaneIf(p2, p1.getPlane(), (Predicate>) segments -> { //if(true)return true; @@ -540,10 +540,10 @@ public static void testCut() { List intersections = getPolygonsThatIntersectWithRay( p.centroid(), - p.plane.getNormal(), + p.getPlane().getNormal(), cubePolys, EPS); - System.out.println("my normal: " + p.plane.getNormal()); + System.out.println("my normal: " + p.getPlane().getNormal()); System.out.println("#intersections: " + intersections.size()); for (RayIntersection ri : intersections) { From 041b3be1b88d7108f190f79ead2f2157360f39a6 Mon Sep 17 00:00:00 2001 From: Kevin Harrington Date: Fri, 21 Jul 2017 13:10:34 -0400 Subject: [PATCH 28/62] ignore eclipse files --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 39caed47..43e2f40d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ /build/ /.gradle/ /.idea/ -/.nb-gradle/ \ No newline at end of file +/.nb-gradle/ +/bin/ +/.settings/ +/.classpath +/.project From 2240d779a0af8023278f146a594bdd974e266145 Mon Sep 17 00:00:00 2001 From: Kevin Harrington Date: Fri, 21 Jul 2017 13:51:15 -0400 Subject: [PATCH 29/62] removing unused depdancy that causes a circular inclusion of an older release of JCSG --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 6d331ab6..959deb75 100644 --- a/build.gradle +++ b/build.gradle @@ -62,8 +62,8 @@ dependencies { testCompile group: 'junit', name: 'junit', version: '4.+' - compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4' - compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4', classifier: 'sources' + // compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4' + //compile group: 'eu.mihosoft.ext.org.fxyz', name: 'extfxyz', version: '0.4', classifier: 'sources' compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.8' compile group: 'eu.mihosoft.vvecmath', name: 'vvecmath', version: '0.3.8', classifier: 'sources' compile 'org.slf4j:slf4j-simple:1.6.1' From dcce1e3e79f43bf6beeae77bc770027999821141 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Fri, 18 Aug 2017 14:23:02 +0200 Subject: [PATCH 30/62] fixes issue #42 for STL files : https://github.com/miho/JCSG/issues/42 --- src/main/java/eu/mihosoft/jcsg/CSG.java | 125 ++++++++----------- src/main/java/eu/mihosoft/jcsg/FileUtil.java | 28 +++++ 2 files changed, 82 insertions(+), 71 deletions(-) diff --git a/src/main/java/eu/mihosoft/jcsg/CSG.java b/src/main/java/eu/mihosoft/jcsg/CSG.java index e264d4a6..dbad3592 100644 --- a/src/main/java/eu/mihosoft/jcsg/CSG.java +++ b/src/main/java/eu/mihosoft/jcsg/CSG.java @@ -1,47 +1,47 @@ /** * CSG.java * - * Copyright 2014-2014 Michael Hoffer . All rights - * reserved. + * Copyright 2014-2014 Michael Hoffer . All rights reserved. * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following + * disclaimer. * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other materials provided with the distribution. * - * THIS SOFTWARE IS PROVIDED BY Michael Hoffer "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL Michael Hoffer OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * THIS SOFTWARE IS PROVIDED BY Michael Hoffer "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL Michael Hoffer OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * - * The views and conclusions contained in the software and documentation are - * those of the authors and should not be interpreted as representing official - * policies, either expressed or implied, of Michael Hoffer + * The views and conclusions contained in the software and documentation are those of the authors and should not be + * interpreted as representing official policies, either expressed or implied, of Michael Hoffer * . */ package eu.mihosoft.jcsg; +import static eu.mihosoft.jcsg.STL.file; import eu.mihosoft.vvecmath.Vector3d; import eu.mihosoft.vvecmath.Transform; import eu.mihosoft.jcsg.ext.quickhull3d.HullUtil; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.scene.paint.Color; @@ -53,17 +53,15 @@ * This implementation is a Java port of * https://github.com/evanw/csg.js/ - * with some additional features like polygon extrude, transformations etc. - * Thanks to the author for creating the CSG.js library.

+ * with some additional features like polygon extrude, transformations etc. Thanks to the author for creating the CSG.js + * library.

* * Implementation Details * - * All CSG operations are implemented in terms of two functions, - * {@link Node#clipTo(Node)} and {@link Node#invert()}, which remove parts of a - * BSP tree inside another BSP tree and swap solid and empty space, - * respectively. To find the union of {@code a} and {@code b}, we want to remove - * everything in {@code a} inside {@code b} and everything in {@code b} inside - * {@code a}, then combine polygons from {@code a} and {@code b} into one solid: + * All CSG operations are implemented in terms of two functions, {@link Node#clipTo(Node)} and {@link Node#invert()}, + * which remove parts of a BSP tree inside another BSP tree and swap solid and empty space, respectively. To find the + * union of {@code a} and {@code b}, we want to remove everything in {@code a} inside {@code b} and everything in + * {@code b} inside {@code a}, then combine polygons from {@code a} and {@code b} into one solid: * *

  *     a.clipTo(b);
@@ -71,11 +69,9 @@
  *     a.build(b.allPolygons());
  * 
* - * The only tricky part is handling overlapping coplanar polygons in both trees. - * The code above keeps both copies, but we need to keep them in one tree and - * remove them in the other tree. To remove them from {@code b} we can clip the - * inverse of {@code b} against {@code a}. The code for union now looks like - * this: + * The only tricky part is handling overlapping coplanar polygons in both trees. The code above keeps both copies, but + * we need to keep them in one tree and remove them in the other tree. To remove them from {@code b} we can clip the + * inverse of {@code b} against {@code a}. The code for union now looks like this: * *
  *     a.clipTo(b);
@@ -86,9 +82,8 @@
  *     a.build(b.allPolygons());
  * 
* - * Subtraction and intersection naturally follow from set operations. If union - * is {@code A | B}, differenceion is {@code A - B = ~(~A | B)} and intersection - * is {@code A & B = + * Subtraction and intersection naturally follow from set operations. If union is {@code A | B}, differenceion is + * {@code A - B = ~(~A | B)} and intersection is {@code A & B = * ~(~A | ~B)} where {@code ~} is the complement operator. */ public class CSG { @@ -203,8 +198,7 @@ public CSG optimization(OptType type) { } /** - * Return a new CSG solid representing the union of this csg and the - * specified csg. + * Return a new CSG solid representing the union of this csg and the specified csg. * * Note: Neither this csg nor the specified csg are weighted. * @@ -240,20 +234,17 @@ public CSG union(CSG csg) { } /** - * Returns a csg consisting of the polygons of this csg and the specified - * csg. + * Returns a csg consisting of the polygons of this csg and the specified csg. * - * The purpose of this method is to allow fast union operations for objects - * that do not intersect. + * The purpose of this method is to allow fast union operations for objects that do not intersect. * *

- * WARNING: this method does not apply the csg algorithms. Therefore, - * please ensure that this csg and the specified csg do not intersect. + * WARNING: this method does not apply the csg algorithms. Therefore, please ensure that this csg and the + * specified csg do not intersect. * * @param csg csg * - * @return a csg consisting of the polygons of this csg and the specified - * csg + * @return a csg consisting of the polygons of this csg and the specified csg */ public CSG dumbUnion(CSG csg) { @@ -266,8 +257,7 @@ public CSG dumbUnion(CSG csg) { } /** - * Return a new CSG solid representing the union of this csg and the - * specified csgs. + * Return a new CSG solid representing the union of this csg and the specified csgs. * * Note: Neither this csg nor the specified csg are weighted. * @@ -301,8 +291,7 @@ public CSG union(List csgs) { } /** - * Return a new CSG solid representing the union of this csg and the - * specified csgs. + * Return a new CSG solid representing the union of this csg and the specified csgs. * * Note: Neither this csg nor the specified csg are weighted. * @@ -414,9 +403,8 @@ private CSG _unionPolygonBoundsOpt(CSG csg) { } /** - * Optimizes for intersection. If csgs do not intersect create a new csg - * that consists of the polygon lists of this csg and the specified csg. In - * this case no further space partitioning is performed. + * Optimizes for intersection. If csgs do not intersect create a new csg that consists of the polygon lists of this + * csg and the specified csg. In this case no further space partitioning is performed. * * @param csg csg * @return the union of this csg and the specified csg @@ -458,8 +446,7 @@ private CSG _unionNoOpt(CSG csg) { } /** - * Return a new CSG solid representing the difference of this csg and the - * specified csgs. + * Return a new CSG solid representing the difference of this csg and the specified csgs. * * Note: Neither this csg nor the specified csgs are weighted. * @@ -495,8 +482,7 @@ public CSG difference(List csgs) { } /** - * Return a new CSG solid representing the difference of this csg and the - * specified csgs. + * Return a new CSG solid representing the difference of this csg and the specified csgs. * * Note: Neither this csg nor the specified csgs are weighted. * @@ -522,8 +508,7 @@ public CSG difference(CSG... csgs) { } /** - * Return a new CSG solid representing the difference of this csg and the - * specified csg. + * Return a new CSG solid representing the difference of this csg and the specified csg. * * Note: Neither this csg nor the specified csg are weighted. * @@ -606,8 +591,7 @@ private CSG _differenceNoOpt(CSG csg) { } /** - * Return a new CSG solid representing the intersection of this csg and the - * specified csg. + * Return a new CSG solid representing the intersection of this csg and the specified csg. * * Note: Neither this csg nor the specified csg are weighted. * @@ -643,8 +627,7 @@ public CSG intersect(CSG csg) { } /** - * Return a new CSG solid representing the intersection of this csg and the - * specified csgs. + * Return a new CSG solid representing the intersection of this csg and the specified csgs. * * Note: Neither this csg nor the specified csgs are weighted. * @@ -681,8 +664,7 @@ public CSG intersect(List csgs) { } /** - * Return a new CSG solid representing the intersection of this csg and the - * specified csgs. + * Return a new CSG solid representing the intersection of this csg and the specified csgs. * * Note: Neither this csg nor the specified csgs are weighted. * @@ -719,6 +701,7 @@ public String toStlString() { return sb.toString(); } + /** * Returns this csg in STL string format. * @@ -755,8 +738,8 @@ public ObjFile toObj() { } public ObjFile toObj(int maxNumberOfVerts) { - - if(maxNumberOfVerts != 3) { + + if (maxNumberOfVerts != 3) { throw new UnsupportedOperationException( "maxNumberOfVerts > 3 not supported yet"); } @@ -1126,7 +1109,7 @@ public Bounds getBounds() { if (polygons.isEmpty()) { return new Bounds(Vector3d.ZERO, Vector3d.ZERO); } - + Vector3d initial = polygons.get(0).vertices.get(0).pos; double minX = initial.x(); diff --git a/src/main/java/eu/mihosoft/jcsg/FileUtil.java b/src/main/java/eu/mihosoft/jcsg/FileUtil.java index 44c180d9..e3c00490 100644 --- a/src/main/java/eu/mihosoft/jcsg/FileUtil.java +++ b/src/main/java/eu/mihosoft/jcsg/FileUtil.java @@ -36,6 +36,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.logging.Level; +import java.util.logging.Logger; /** * File util class. @@ -74,4 +76,30 @@ public static void write(Path p, String s) throws IOException { public static String read(Path p) throws IOException { return new String(Files.readAllBytes(p), Charset.forName("UTF-8")); } + + + /** + * Saves the specified csg using STL ASCII format. + * + * @param path destination path + * @param csg csg to save + * @throws java.io.IOException + */ + public static void toStlFile(Path path, CSG csg) throws IOException { + try (BufferedWriter out = Files.newBufferedWriter(path, Charset.forName("UTF-8"), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + + out.append("solid v3d.csg\n"); + csg.getPolygons().stream().forEach( + (Polygon p) -> { + try { + out.append(p.toStlString()); + } catch (IOException ex) { + Logger.getLogger(CSG.class.getName()).log(Level.SEVERE, null, ex); + throw new RuntimeException(ex); + } + }); + out.append("endsolid v3d.csg\n"); + } + } } From cf00df368264fdf71ce3fae1cd2a987d648499ff Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 7 Apr 2018 23:11:13 +0200 Subject: [PATCH 31/62] triangulation of polygons possible via public api --- build.gradle | 6 ++-- gradle.properties | 2 +- src/main/java/eu/mihosoft/jcsg/Polygon.java | 32 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 959deb75..883988c3 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ plugins { id 'application' id 'java' id 'maven-publish' - id 'net.nemerosa.versioning' version '1.5.0' - id 'com.jfrog.bintray' version '1.6' + id 'net.nemerosa.versioning' version '2.6.1' + id 'com.jfrog.bintray' version '1.7.2' } sourceCompatibility = '1.8' @@ -13,7 +13,7 @@ sourceCompatibility = '1.8' //repairHeaders.licenseHeaderText = new File(projectDir,'./license-template.txt') task wrapper(type: Wrapper, description: 'Creates and deploys the Gradle wrapper to the current directory.') { - gradleVersion = '4.0' + gradleVersion = '4.6' } if (!hasProperty('mainClass')) { diff --git a/gradle.properties b/gradle.properties index d6c234ff..5e990126 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group = eu.mihosoft.vrl.jcsg -version = 0.5.7-SNAPSHOT +version = 0.5.7 diff --git a/src/main/java/eu/mihosoft/jcsg/Polygon.java b/src/main/java/eu/mihosoft/jcsg/Polygon.java index 32a41866..22170b44 100644 --- a/src/main/java/eu/mihosoft/jcsg/Polygon.java +++ b/src/main/java/eu/mihosoft/jcsg/Polygon.java @@ -272,6 +272,38 @@ public StringBuilder toStlString(StringBuilder sb) { return sb; } + /** + * Returns a triangulated version of this polygon. + * + * @return triangles + */ + public List toTriangles() { + + List result = new ArrayList<>(); + + if (this.vertices.size() >= 3) { + + // TODO: improve the triangulation? + // + // If our polygon has more vertices, create + // multiple triangles: + Vertex firstVertexStl = this.vertices.get(0); + for (int i = 0; i < this.vertices.size() - 2; i++) { + + // create triangle + Polygon polygon = Polygon.fromPoints( + firstVertexStl.pos, + this.vertices.get(i + 1).pos, + this.vertices.get(i + 2).pos + ); + + result.add(polygon); + } + } + + return result; + } + /** * Translates this polygon. * From 436f848f47ef37f7a358a4149db366012b91f31b Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 7 Apr 2018 23:13:13 +0200 Subject: [PATCH 32/62] moved to snapshot --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5e990126..54491709 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group = eu.mihosoft.vrl.jcsg -version = 0.5.7 +version = 0.5.8-SNAPSHOT From 09a5583f5dae803cf6f39942bd52cfad558da820 Mon Sep 17 00:00:00 2001 From: Peter De Roovere Date: Mon, 28 May 2018 16:10:12 +0200 Subject: [PATCH 33/62] Update plane when flipping polygon When the flip()-method is called on Polygon objects, update the plane field accordingly. --- src/main/java/eu/mihosoft/jcsg/Polygon.java | 1 + .../eu/mihosoft/jcsg/PolygonFlipTest.java | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/test/java/eu/mihosoft/jcsg/PolygonFlipTest.java diff --git a/src/main/java/eu/mihosoft/jcsg/Polygon.java b/src/main/java/eu/mihosoft/jcsg/Polygon.java index 22170b44..faf8f56c 100644 --- a/src/main/java/eu/mihosoft/jcsg/Polygon.java +++ b/src/main/java/eu/mihosoft/jcsg/Polygon.java @@ -214,6 +214,7 @@ public Polygon flip() { Collections.reverse(vertices); _csg_plane.flip(); + this.plane = plane.flipped(); return this; } diff --git a/src/test/java/eu/mihosoft/jcsg/PolygonFlipTest.java b/src/test/java/eu/mihosoft/jcsg/PolygonFlipTest.java new file mode 100644 index 00000000..67d83838 --- /dev/null +++ b/src/test/java/eu/mihosoft/jcsg/PolygonFlipTest.java @@ -0,0 +1,28 @@ +package eu.mihosoft.jcsg; + +import eu.mihosoft.vvecmath.Vector3d; +import org.junit.Assert; +import org.junit.Test; + +public class PolygonFlipTest { + + private static final double EPSILON = 1e-8; + + @Test + public void flipPolygonTest() { + Polygon polygon = Polygon.fromPoints( + Vector3d.xy(1, 1), + Vector3d.xy(2, 1), + Vector3d.xy(1, 2) + ); + assertEquals(Vector3d.z(1), polygon.getPlane().getNormal()); + polygon.flip(); + assertEquals(Vector3d.z(-1), polygon.getPlane().getNormal()); + } + + private void assertEquals(final Vector3d expected, final Vector3d actual) { + Assert.assertEquals(expected.getX(), actual.getX(), EPSILON); + Assert.assertEquals(expected.getY(), actual.getY(), EPSILON); + Assert.assertEquals(expected.getZ(), actual.getZ(), EPSILON); + } +} From 1fea35a3fe3ee48d91499849a483ab0c5d45220e Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Tue, 24 Jul 2018 19:48:51 +0200 Subject: [PATCH 34/62] Adding JDK 9 and 10 to the build matrix --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8de9f2e7..54243dd9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ sudo: false language: java jdk: - oraclejdk8 + - oraclejdk9 + - oraclejdk10 before_install: - "export DISPLAY=:99.0" From e7ee8b0f021c9798e2b0f10614edb3b08f5ddbd9 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Tue, 24 Jul 2018 19:54:19 +0200 Subject: [PATCH 35/62] preparing jdk10 support --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 54706 -> 54413 bytes gradle/wrapper/gradle-wrapper.properties | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 883988c3..12303a4a 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ sourceCompatibility = '1.8' //repairHeaders.licenseHeaderText = new File(projectDir,'./license-template.txt') task wrapper(type: Wrapper, description: 'Creates and deploys the Gradle wrapper to the current directory.') { - gradleVersion = '4.6' + gradleVersion = '4.9' } if (!hasProperty('mainClass')) { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 51ef3dbf64e5112650805cbdfb5141198ff31212..0d4a9516871afd710a9d84d89e31ba77745607bd 100644 GIT binary patch delta 23122 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 Date: Tue, 24 Jul 2018 20:22:41 +0200 Subject: [PATCH 36/62] Compiles with JDK 10 --- .../openjfx/importers/SmoothingGroups.java | 1 - .../jcsg/ext/openjfx/importers/Vec3f.java | 220 ++++++++++++++++++ 2 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/Vec3f.java diff --git a/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/SmoothingGroups.java b/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/SmoothingGroups.java index 827227da..a0a0dbf6 100644 --- a/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/SmoothingGroups.java +++ b/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/SmoothingGroups.java @@ -40,7 +40,6 @@ import java.util.List; import java.util.Map; import java.util.Queue; -import com.sun.javafx.geom.Vec3f; import javafx.scene.shape.TriangleMesh; /** Util for converting Normals to Smoothing Groups */ diff --git a/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/Vec3f.java b/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/Vec3f.java new file mode 100644 index 00000000..8de9acb9 --- /dev/null +++ b/src/main/java/eu/mihosoft/jcsg/ext/openjfx/importers/Vec3f.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2014, Oracle and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package eu.mihosoft.jcsg.ext.openjfx.importers; + +/** + * A 3-dimensional, single-precision, floating-point vector. + * + */ +public class Vec3f { + /** + * The x coordinate. + */ + public float x; + + /** + * The y coordinate. + */ + public float y; + + /** + * The z coordinate. + */ + public float z; + + public Vec3f() { } + + public Vec3f(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vec3f(Vec3f v) { + this.x = v.x; + this.y = v.y; + this.z = v.z; + } + + public void set(Vec3f v) { + this.x = v.x; + this.y = v.y; + this.z = v.z; + } + + public void set(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public final void mul(float s) { + this.x *= s; + this.y *= s; + this.z *= s; + } + + /** + * Sets the value of this vector to the difference + * of vectors t1 and t2 (this = t1 - t2). + * @param t1 the first vector + * @param t2 the second vector + */ + public void sub(Vec3f t1, Vec3f t2) { + this.x = t1.x - t2.x; + this.y = t1.y - t2.y; + this.z = t1.z - t2.z; + } + + /** + * Sets the value of this vector to the difference of + * itself and vector t1 (this = this - t1) . + * @param t1 the other vector + */ + public void sub(Vec3f t1) { + this.x -= t1.x; + this.y -= t1.y; + this.z -= t1.z; + } + + /** + * Sets the value of this vector to the sum + * of vectors t1 and t2 (this = t1 + t2). + * @param t1 the first vector + * @param t2 the second vector + */ + public void add(Vec3f t1, Vec3f t2) { + this.x = t1.x + t2.x; + this.y = t1.y + t2.y; + this.z = t1.z + t2.z; + } + + /** + * Sets the value of this vector to the sum of + * itself and vector t1 (this = this + t1) . + * @param t1 the other vector + */ + public void add(Vec3f t1) { + this.x += t1.x; + this.y += t1.y; + this.z += t1.z; + } + + /** + * Returns the length of this vector. + * @return the length of this vector + */ + public float length() { + return (float) Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z); + } + + /** + * Normalize this vector. + */ + public void normalize() { + float norm = 1.0f / length(); + this.x = this.x * norm; + this.y = this.y * norm; + this.z = this.z * norm; + } + + /** + * Sets this vector to be the vector cross product of vectors v1 and v2. + * @param v1 the first vector + * @param v2 the second vector + */ + public void cross(Vec3f v1, Vec3f v2) { + float tmpX; + float tmpY; + + tmpX = v1.y * v2.z - v1.z * v2.y; + tmpY = v2.x * v1.z - v2.z * v1.x; + this.z = v1.x * v2.y - v1.y * v2.x; + this.x = tmpX; + this.y = tmpY; + } + + /** + * Computes the dot product of this vector and vector v1. + * @param v1 the other vector + * @return the dot product of this vector and v1 + */ + public float dot(Vec3f v1) { + return this.x * v1.x + this.y * v1.y + this.z * v1.z; + } + + /** + * Returns the hashcode for this Vec3f. + * @return a hash code for this Vec3f. + */ + @Override + public int hashCode() { + int bits = 7; + bits = 31 * bits + Float.floatToIntBits(x); + bits = 31 * bits + Float.floatToIntBits(y); + bits = 31 * bits + Float.floatToIntBits(z); + return bits; + } + + /** + * Determines whether or not two 3D points or vectors are equal. + * Two instances of Vec3f are equal if the values of their + * x, y and z member fields, + * representing their position in the coordinate space, are the same. + * @param obj an object to be compared with this Vec3f + * @return true if the object to be compared is + * an instance of Vec3f and has + * the same values; false otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Vec3f) { + Vec3f v = (Vec3f) obj; + return (x == v.x) && (y == v.y) && (z == v.z); + } + return false; + } + + /** + * Returns a String that represents the value + * of this Vec3f. + * @return a string representation of this Vec3f. + */ + @Override + public String toString() { + return "Vec3f[" + x + ", " + y + ", " + z + "]"; + } +} \ No newline at end of file From 370898f8c008a0ddc4cd2be34307dff13f4a4a93 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 27 Oct 2018 22:24:22 +0200 Subject: [PATCH 37/62] Volume computation implemented: - https://github.com/miho/JCSG/issues/51 --- .gitignore | 1 + src/main/java/eu/mihosoft/jcsg/CSG.java | 42 +++++++++++++ .../java/eu/mihosoft/jcsg/VolumeTest.java | 61 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 src/test/java/eu/mihosoft/jcsg/VolumeTest.java diff --git a/.gitignore b/.gitignore index 43e2f40d..3a8c92a2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /.settings/ /.classpath /.project +/.vscode/ diff --git a/src/main/java/eu/mihosoft/jcsg/CSG.java b/src/main/java/eu/mihosoft/jcsg/CSG.java index dbad3592..8cf0d296 100644 --- a/src/main/java/eu/mihosoft/jcsg/CSG.java +++ b/src/main/java/eu/mihosoft/jcsg/CSG.java @@ -1183,4 +1183,46 @@ public static enum OptType { NONE } + /** + * Computes and returns the volume of this CSG based on a triangulated version + * of the internal mesh. + * @return volume of this csg + */ + public double computeVolume() { + if(getPolygons().isEmpty()) return 0; + + // triangulate polygons (parallel for larger meshes) + Stream polyStream; + if(getPolygons().size()>200) { + polyStream = getPolygons().parallelStream(); + } else { + polyStream = getPolygons().stream(); + } + List triangles = polyStream. + flatMap(poly->poly.toTriangles().stream()). + collect(Collectors.toList()); + + // compute sum over signed volumes of triangles + // we use parallel streams for larger meshes + // see http://chenlab.ece.cornell.edu/Publication/Cha/icip01_Cha.pdf + Stream triangleStream; + if(triangles.size()>200) { + triangleStream = triangles.parallelStream(); + } else { + triangleStream = triangles.stream(); + } + + double volume = triangleStream.mapToDouble(tri-> { + Vector3d p1 = tri.vertices.get(0).pos; + Vector3d p2 = tri.vertices.get(1).pos; + Vector3d p3 = tri.vertices.get(2).pos; + + return p1.dot(p2.crossed(p3)) / 6.0; + }).sum(); + + volume = Math.abs(volume); + + return volume; + } + } diff --git a/src/test/java/eu/mihosoft/jcsg/VolumeTest.java b/src/test/java/eu/mihosoft/jcsg/VolumeTest.java new file mode 100644 index 00000000..c1343d1e --- /dev/null +++ b/src/test/java/eu/mihosoft/jcsg/VolumeTest.java @@ -0,0 +1,61 @@ +package eu.mihosoft.jcsg; +import org.junit.Test; +import static org.junit.Assert.*; + +import java.nio.file.Paths; + +public class VolumeTest { + + @Test + public void vlumeTest() { + + { + // volume of empty CSG object is 0 + double emptyVolume = CSG.fromPolygons(new Polygon[0]).computeVolume(); + assertEquals(emptyVolume, 0, 1e-16); + } + + { + // volume of unit cube is 1 unit^3 + double volumeUnitCube = new Cube(1.0).toCSG().computeVolume(); + assertEquals(1.0, volumeUnitCube, 1e-16); + } + + { + // volume of cube is w*h*d unit^3 + double w = 30.65; + double h = 24.17; + double d = 75.3; + double volumeBox = new Cube(w,h,d).toCSG().computeVolume(); + assertEquals(w*h*d, volumeBox, 1e-16); + } + + { + // volume of sphere is (4*PI*r^3)/3.0 unit^3 + double r = 3.4; + + // bad approximation + double volumeSphere1 = new Sphere(r, 32,16).toCSG().computeVolume(); + assertEquals((4.0*Math.PI*r*r*r)/3.0, volumeSphere1, 10.0); + + // better approximation + double volumeSphere2 = new Sphere(r, 1024, 512).toCSG().computeVolume(); + assertEquals((4.0*Math.PI*r*r*r)/3.0, volumeSphere2, 1e-2); + } + + { + // volume of cylinder is PI*r^2*h unit^3 + double r = 5.9; + double h = 2.1; + + // bad approximation + double volumeCylinder1 = new Cylinder(r, h, 16).toCSG().computeVolume(); + assertEquals(Math.PI*r*r*h, volumeCylinder1, 10); + + // better approximation + double volumeCylinder2 = new Cylinder(r, h, 1024).toCSG().computeVolume(); + assertEquals(Math.PI*r*r*h, volumeCylinder2, 1e-2); + } + + } +} \ No newline at end of file From 727eb889e8dac5966da0df2edfbf53de5b610cfb Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 27 Oct 2018 22:56:11 +0200 Subject: [PATCH 38/62] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 54243dd9..09e0367d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ language: java jdk: - oraclejdk8 - oraclejdk9 - - oraclejdk10 + # - oraclejdk10 openjdk 10 is broken on travis (maybe they use the wrong download link?) before_install: - "export DISPLAY=:99.0" From ab3f96c7486e5155f4b0377d557db86e6392303c Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Thu, 20 Dec 2018 16:59:32 +0100 Subject: [PATCH 39/62] mention VCSG and extension libraries --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6f9f8b08..977d256a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ JCSG Java implementation of BSP based CSG (Constructive Solid Geometry). It is the only simple and free Java implementation I am aware of. This implementation uses an optimized CSG algorithm based on [csg.js](https://github.com/evanw/csg.js) (see `CSG` and `Node` classes). Thanks to the author for creating the [csg.js](https://github.com/evanw/csg.js) library. +There's also the related [VCSG](https://github.com/miho/VCSG) library, a [plugin for VRL-Studio](https://github.com/VRL-Studio/VRL-JCSG) and two extension libraries: [JCSG-MeshExtension](https://github.com/miho/JCSG-MeshExtensions) and [JCSG-PathExtension](https://github.com/miho/JCSG-PathExtensions). + In addition to CSG this library provides the following features: - optimized `difference()` and `union()` operations (many thanks to Sebastian Reiter) From 5b1aa99f7f1087333592ca73acc49233a04e7d0e Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Thu, 20 Dec 2018 17:04:01 +0100 Subject: [PATCH 40/62] Fixed bintray plugin version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 12303a4a..cc012dc3 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { id 'java' id 'maven-publish' id 'net.nemerosa.versioning' version '2.6.1' - id 'com.jfrog.bintray' version '1.7.2' + id 'com.jfrog.bintray' version '1.8.4' } sourceCompatibility = '1.8' From 93b869e756a47326031fd368ab1676f6e7430208 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Thu, 20 Dec 2018 17:07:10 +0100 Subject: [PATCH 41/62] Update build.gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cc012dc3..bc6b76c0 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'application' id 'java' id 'maven-publish' - id 'net.nemerosa.versioning' version '2.6.1' + id 'net.nemerosa.versioning' version '2.8.2' id 'com.jfrog.bintray' version '1.8.4' } From 13f9a9d6b9c8cb7ff2acba818f1472ecb6b20126 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Thu, 20 Dec 2018 17:09:25 +0100 Subject: [PATCH 42/62] Update build.gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bc6b76c0..dfca4986 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'application' id 'java' id 'maven-publish' - id 'net.nemerosa.versioning' version '2.8.2' + //id 'net.nemerosa.versioning' version '2.8.2' id 'com.jfrog.bintray' version '1.8.4' } From bca7f9c67bd8a63499b7e79055b1634dda8dc49e Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Thu, 20 Dec 2018 17:12:59 +0100 Subject: [PATCH 43/62] Update build.gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dfca4986..c08e6a97 100644 --- a/build.gradle +++ b/build.gradle @@ -96,7 +96,7 @@ jar { 'Created-By': System.properties['java.version'] + " (" + System.properties['java.vendor'] + " " + System.properties['java.vm.version'] + ")", 'Build-Date': project.buildDate, 'Build-Time': project.buildTime, - 'Build-Revision': versioning.info.commit, + //'Build-Revision': versioning.info.commit, 'Specification-Title': project.name, 'Specification-Version': project.version, 'Implementation-Title': project.name, From b6d14a372328f98e68f14a64dda9acff679bcbf1 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 29 Dec 2018 12:34:47 +0100 Subject: [PATCH 44/62] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 977d256a..b62ad0b0 100644 --- a/README.md +++ b/README.md @@ -82,3 +82,5 @@ try { Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); } ``` + +Thanks to [JetBrains](https://www.jetbrains.com) for the [IntelliJ IDEA](https://www.jetbrains.com/idea/) licenses. From ed4ac1f7dcbe0bd0719d4bd55b00cd3d12e9cc52 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 29 Dec 2018 12:35:37 +0100 Subject: [PATCH 45/62] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b62ad0b0..3cac9551 100644 --- a/README.md +++ b/README.md @@ -83,4 +83,4 @@ try { } ``` -Thanks to [JetBrains](https://www.jetbrains.com) for the [IntelliJ IDEA](https://www.jetbrains.com/idea/) licenses. +Thanks to [JetBrains](https://www.jetbrains.com) for their [IntelliJ IDEA](https://www.jetbrains.com/idea/) license(s). From af6b7df9f6a7bde8430496cebc989ee9b7a8a9e7 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 29 Dec 2018 12:37:26 +0100 Subject: [PATCH 46/62] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3cac9551..f6ad6d7f 100644 --- a/README.md +++ b/README.md @@ -83,4 +83,8 @@ try { } ``` -Thanks to [JetBrains](https://www.jetbrains.com) for their [IntelliJ IDEA](https://www.jetbrains.com/idea/) license(s). +## Thanks to + + - [JetBrains](https://www.jetbrains.com) for their [IntelliJ IDEA](https://www.jetbrains.com/idea/) license(s). + + - the author for creating the [csg.js](https://github.com/evanw/csg.js) library From 47f4ae8617a969686b10649108b78361c6a20554 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 29 Dec 2018 12:39:34 +0100 Subject: [PATCH 47/62] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f6ad6d7f..9e2f2c9b 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,6 @@ try { ## Thanks to - - [JetBrains](https://www.jetbrains.com) for their [IntelliJ IDEA](https://www.jetbrains.com/idea/) license(s). + - [JetBrains](https://www.jetbrains.com) for their [IntelliJ IDEA](https://www.jetbrains.com/idea/) license(s) - - the author for creating the [csg.js](https://github.com/evanw/csg.js) library + - Evan Wallace for creating the Javascript library [csg.js](https://github.com/evanw/csg.js) From 25b055977b87746123d6ad9e8e805f7f9ebe7f66 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 29 Dec 2018 12:40:01 +0100 Subject: [PATCH 48/62] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e2f2c9b..707b10da 100644 --- a/README.md +++ b/README.md @@ -87,4 +87,4 @@ try { - [JetBrains](https://www.jetbrains.com) for their [IntelliJ IDEA](https://www.jetbrains.com/idea/) license(s) - - Evan Wallace for creating the Javascript library [csg.js](https://github.com/evanw/csg.js) + - Evan Wallace for creating the JavaScript library [csg.js](https://github.com/evanw/csg.js) From 52db0414ce2f40351c12304fbddbfc8745cbd273 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Mon, 28 Jan 2019 20:33:25 +0100 Subject: [PATCH 49/62] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 707b10da..2d9e45dc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ JCSG ======= -[![Build Status](https://travis-ci.org/miho/JCSG.png?branch=master)](https://travis-ci.org/miho/JCSG) [ ![Download](https://api.bintray.com/packages/miho/JCSG/jcsg/images/download.svg) ](https://bintray.com/miho/JCSG/jcsg/_latestVersion) +[![Build Status](https://travis-ci.org/miho/JCSG.png?branch=master)](https://travis-ci.org/miho/JCSG) [ ![Download](https://api.bintray.com/packages/miho/JCSG/jcsg/images/download.svg) ](https://bintray.com/miho/JCSG/jcsg/_latestVersion) [![Javadocs](https://www.javadoc.io/badge/eu.mihosoft.jcsg/jcsg.svg)](https://www.javadoc.io/doc/eu.mihosoft.jcsg/jcsg) + Java implementation of BSP based CSG (Constructive Solid Geometry). It is the only simple and free Java implementation I am aware of. This implementation uses an optimized CSG algorithm based on [csg.js](https://github.com/evanw/csg.js) (see `CSG` and `Node` classes). Thanks to the author for creating the [csg.js](https://github.com/evanw/csg.js) library. From 5801d917460012438412d0cd8e5fa2c2697c1bd4 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Mon, 28 Jan 2019 20:35:31 +0100 Subject: [PATCH 50/62] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d9e45dc..ebcdb86d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ JCSG ======= -[![Build Status](https://travis-ci.org/miho/JCSG.png?branch=master)](https://travis-ci.org/miho/JCSG) [ ![Download](https://api.bintray.com/packages/miho/JCSG/jcsg/images/download.svg) ](https://bintray.com/miho/JCSG/jcsg/_latestVersion) [![Javadocs](https://www.javadoc.io/badge/eu.mihosoft.jcsg/jcsg.svg)](https://www.javadoc.io/doc/eu.mihosoft.jcsg/jcsg) +[![Build Status](https://travis-ci.org/miho/JCSG.png?branch=master)](https://travis-ci.org/miho/JCSG) [ ![Download](https://api.bintray.com/packages/miho/JCSG/jcsg/images/download.svg) ](https://bintray.com/miho/JCSG/jcsg/_latestVersion) [![Javadocs](https://www.javadoc.io/badge/eu.mihosoft.vrl.jcsg/jcsg.svg)](https://www.javadoc.io/doc/eu.mihosoft.vrl.jcsg/jcsg) + Java implementation of BSP based CSG (Constructive Solid Geometry). It is the only simple and free Java implementation I am aware of. This implementation uses an optimized CSG algorithm based on [csg.js](https://github.com/evanw/csg.js) (see `CSG` and `Node` classes). Thanks to the author for creating the [csg.js](https://github.com/evanw/csg.js) library. From 3e29225815264e16e47d29e0b4b20db9e3655099 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 3 Aug 2019 14:40:36 +0200 Subject: [PATCH 51/62] Minimal supported version is JDK 11 now --- build.gradle | 6 +++--- gradle/wrapper/gradle-wrapper.jar | Bin 54413 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 18 +++++++++++++++++- gradlew.bat | 18 +++++++++++++++++- 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index c08e6a97..57217bf2 100644 --- a/build.gradle +++ b/build.gradle @@ -6,14 +6,14 @@ plugins { id 'com.jfrog.bintray' version '1.8.4' } -sourceCompatibility = '1.8' +sourceCompatibility = '11' [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' //apply from: 'http://gradle-plugins.mihosoft.eu/latest/vlicenseheader.gradle' //repairHeaders.licenseHeaderText = new File(projectDir,'./license-template.txt') -task wrapper(type: Wrapper, description: 'Creates and deploys the Gradle wrapper to the current directory.') { - gradleVersion = '4.9' +wrapper { + gradleVersion = '5.5' } if (!hasProperty('mainClass')) { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 0d4a9516871afd710a9d84d89e31ba77745607bd..5c2d1cf016b3885f6930543d57b744ea8c220a1a 100644 GIT binary patch literal 55616 zcmafaW0WS*vSoFbZJS-TZP!<}ZQEV8ZQHihW!tvx>6!c9%-lQoy;&DmfdT@8fB*sl68LLCKtKQ283+jS?^Q-bNq|NIAW8=eB==8_)^)r*{C^$z z{u;{v?IMYnO`JhmPq7|LA_@Iz75S9h~8`iX>QrjrmMeu{>hn4U;+$dor zz+`T8Q0f}p^Ao)LsYq74!W*)&dTnv}E8;7H*Zetclpo2zf_f>9>HT8;`O^F8;M%l@ z57Z8dk34kG-~Wg7n48qF2xwPp;SOUpd1}9Moir5$VSyf4gF)Mp-?`wO3;2x9gYj59oFwG>?Leva43@e(z{mjm0b*@OAYLC`O9q|s+FQLOE z!+*Y;%_0(6Sr<(cxE0c=lS&-FGBFGWd_R<5$vwHRJG=tB&Mi8@hq_U7@IMyVyKkOo6wgR(<% zQw1O!nnQl3T9QJ)Vh=(`cZM{nsEKChjbJhx@UQH+G>6p z;beBQ1L!3Zl>^&*?cSZjy$B3(1=Zyn~>@`!j%5v7IBRt6X`O)yDpVLS^9EqmHxBcisVG$TRwiip#ViN|4( zYn!Av841_Z@Ys=T7w#>RT&iXvNgDq3*d?$N(SznG^wR`x{%w<6^qj&|g})La;iD?`M=p>99p><39r9+e z`dNhQ&tol5)P#;x8{tT47i*blMHaDKqJs8!Pi*F{#)9%USFxTVMfMOy{mp2ZrLR40 z2a9?TJgFyqgx~|j0eA6SegKVk@|Pd|_6P$HvwTrLTK)Re`~%kg8o9`EAE1oAiY5Jgo=H}0*D?tSCn^=SIN~fvv453Ia(<1|s07aTVVtsRxY6+tT3589iQdi^ zC92D$ewm9O6FA*u*{Fe_=b`%q`pmFvAz@hfF@OC_${IPmD#QMpPNo0mE9U=Ch;k0L zZteokPG-h7PUeRCPPYG%H!WswC?cp7M|w42pbtwj!m_&4%hB6MdLQe&}@5-h~! zkOt;w0BbDc0H!RBw;1UeVckHpJ@^|j%FBZlC} zsm?nFOT$`F_i#1_gh4|n$rDe>0md6HvA=B%hlX*3Z%y@a&W>Rq`Fe(8smIgxTGb#8 zZ`->%h!?QCk>v*~{!qp=w?a*};Y**1uH`)OX`Gi+L%-d6{rV?@}MU#qfCU(!hLz;kWH=0A%W7E^pA zD;A%Jg5SsRe!O*0TyYkAHe&O9z*Ij-YA$%-rR?sc`xz_v{>x%xY39!8g#!Z0#03H( z{O=drKfb0cbx1F*5%q81xvTDy#rfUGw(fesh1!xiS2XT;7_wBi(Rh4i(!rR^9=C+- z+**b9;icxfq@<7}Y!PW-0rTW+A^$o*#ZKenSkxLB$Qi$%gJSL>x!jc86`GmGGhai9 zOHq~hxh}KqQHJeN$2U{M>qd*t8_e&lyCs69{bm1?KGTYoj=c0`rTg>pS6G&J4&)xp zLEGIHSTEjC0-s-@+e6o&w=h1sEWWvJUvezID1&exb$)ahF9`(6`?3KLyVL$|c)CjS zx(bsy87~n8TQNOKle(BM^>1I!2-CZ^{x6zdA}qeDBIdrfd-(n@Vjl^9zO1(%2pP9@ zKBc~ozr$+4ZfjmzEIzoth(k?pbI87=d5OfjVZ`Bn)J|urr8yJq`ol^>_VAl^P)>2r)s+*3z5d<3rP+-fniCkjmk=2hTYRa@t zCQcSxF&w%mHmA?!vaXnj7ZA$)te}ds+n8$2lH{NeD4mwk$>xZCBFhRy$8PE>q$wS`}8pI%45Y;Mg;HH+}Dp=PL)m77nKF68FggQ-l3iXlVZuM2BDrR8AQbK;bn1%jzahl0; zqz0(mNe;f~h8(fPzPKKf2qRsG8`+Ca)>|<&lw>KEqM&Lpnvig>69%YQpK6fx=8YFj zHKrfzy>(7h2OhUVasdwKY`praH?>qU0326-kiSyOU_Qh>ytIs^htlBA62xU6xg?*l z)&REdn*f9U3?u4$j-@ndD#D3l!viAUtw}i5*Vgd0Y6`^hHF5R=No7j8G-*$NWl%?t z`7Nilf_Yre@Oe}QT3z+jOUVgYtT_Ym3PS5(D>kDLLas8~F+5kW%~ZYppSrf1C$gL* zCVy}fWpZ3s%2rPL-E63^tA|8OdqKsZ4TH5fny47ENs1#^C`_NLg~H^uf3&bAj#fGV zDe&#Ot%_Vhj$}yBrC3J1Xqj>Y%&k{B?lhxKrtYy;^E9DkyNHk5#6`4cuP&V7S8ce9 zTUF5PQIRO7TT4P2a*4;M&hk;Q7&{(83hJe5BSm=9qt~;U)NTf=4uKUcnxC`;iPJeI zW#~w?HIOM+0j3ptB0{UU{^6_#B*Q2gs;1x^YFey(%DJHNWz@e_NEL?$fv?CDxG`jk zH|52WFdVsZR;n!Up;K;4E$|w4h>ZIN+@Z}EwFXI{w_`?5x+SJFY_e4J@|f8U08%dd z#Qsa9JLdO$jv)?4F@&z_^{Q($tG`?|9bzt8ZfH9P`epY`soPYqi1`oC3x&|@m{hc6 zs0R!t$g>sR@#SPfNV6Pf`a^E?q3QIaY30IO%yKjx#Njj@gro1YH2Q(0+7D7mM~c>C zk&_?9Ye>B%*MA+77$Pa!?G~5tm`=p{NaZsUsOgm6Yzclr_P^2)r(7r%n(0?4B#$e7 z!fP;+l)$)0kPbMk#WOjm07+e?{E)(v)2|Ijo{o1+Z8#8ET#=kcT*OwM#K68fSNo%< zvZFdHrOrr;>`zq!_welWh!X}=oN5+V01WJn7=;z5uo6l_$7wSNkXuh=8Y>`TjDbO< z!yF}c42&QWYXl}XaRr0uL?BNPXlGw=QpDUMo`v8pXzzG(=!G;t+mfCsg8 zJb9v&a)E!zg8|%9#U?SJqW!|oBHMsOu}U2Uwq8}RnWeUBJ>FtHKAhP~;&T4mn(9pB zu9jPnnnH0`8ywm-4OWV91y1GY$!qiQCOB04DzfDDFlNy}S{$Vg9o^AY!XHMueN<{y zYPo$cJZ6f7``tmlR5h8WUGm;G*i}ff!h`}L#ypFyV7iuca!J+C-4m@7*Pmj9>m+jh zlpWbud)8j9zvQ`8-oQF#u=4!uK4kMFh>qS_pZciyq3NC(dQ{577lr-!+HD*QO_zB9 z_Rv<#qB{AAEF8Gbr7xQly%nMA%oR`a-i7nJw95F3iH&IX5hhy3CCV5y>mK4)&5aC*12 zI`{(g%MHq<(ocY5+@OK-Qn-$%!Nl%AGCgHl>e8ogTgepIKOf3)WoaOkuRJQt%MN8W z=N-kW+FLw=1^}yN@*-_c>;0N{-B!aXy#O}`%_~Nk?{e|O=JmU8@+92Q-Y6h)>@omP=9i~ zi`krLQK^!=@2BH?-R83DyFkejZkhHJqV%^} zUa&K22zwz7b*@CQV6BQ9X*RB177VCVa{Z!Lf?*c~PwS~V3K{id1TB^WZh=aMqiws5)qWylK#^SG9!tqg3-)p_o(ABJsC!0;0v36;0tC= z!zMQ_@se(*`KkTxJ~$nIx$7ez&_2EI+{4=uI~dwKD$deb5?mwLJ~ema_0Z z6A8Q$1~=tY&l5_EBZ?nAvn$3hIExWo_ZH2R)tYPjxTH5mAw#3n-*sOMVjpUrdnj1DBm4G!J+Ke}a|oQN9f?!p-TcYej+(6FNh_A? zJ3C%AOjc<8%9SPJ)U(md`W5_pzYpLEMwK<_jgeg-VXSX1Nk1oX-{yHz z-;CW!^2ds%PH{L{#12WonyeK5A=`O@s0Uc%s!@22etgSZW!K<%0(FHC+5(BxsXW@e zAvMWiO~XSkmcz%-@s{|F76uFaBJ8L5H>nq6QM-8FsX08ug_=E)r#DC>d_!6Nr+rXe zzUt30Du_d0oSfX~u>qOVR*BmrPBwL@WhF^5+dHjWRB;kB$`m8|46efLBXLkiF|*W= zg|Hd(W}ZnlJLotYZCYKoL7YsQdLXZ!F`rLqLf8n$OZOyAzK`uKcbC-n0qoH!5-rh&k-`VADETKHxrhK<5C zhF0BB4azs%j~_q_HA#fYPO0r;YTlaa-eb)Le+!IeP>4S{b8&STp|Y0if*`-A&DQ$^ z-%=i73HvEMf_V6zSEF?G>G-Eqn+|k`0=q?(^|ZcqWsuLlMF2!E*8dDAx%)}y=lyMa z$Nn0_f8YN8g<4D>8IL3)GPf#dJYU@|NZqIX$;Lco?Qj=?W6J;D@pa`T=Yh z-ybpFyFr*3^gRt!9NnbSJWs2R-S?Y4+s~J8vfrPd_&_*)HBQ{&rW(2X>P-_CZU8Y9 z-32><7|wL*K+3{ZXE5}nn~t@NNT#Bc0F6kKI4pVwLrpU@C#T-&f{Vm}0h1N3#89@d zgcx3QyS;Pb?V*XAq;3(W&rjLBazm69XX;%^n6r}0!CR2zTU1!x#TypCr`yrII%wk8 z+g)fyQ!&xIX(*>?T}HYL^>wGC2E}euj{DD_RYKK@w=yF+44367X17)GP8DCmBK!xS zE{WRfQ(WB-v>DAr!{F2-cQKHIjIUnLk^D}7XcTI#HyjSiEX)BO^GBI9NjxojYfQza zWsX@GkLc7EqtP8(UM^cq5zP~{?j~*2T^Bb={@PV)DTkrP<9&hxDwN2@hEq~8(ZiF! z3FuQH_iHyQ_s-#EmAC5~K$j_$cw{+!T>dm#8`t%CYA+->rWp09jvXY`AJQ-l%C{SJ z1c~@<5*7$`1%b}n7ivSo(1(j8k+*Gek(m^rQ!+LPvb=xA@co<|(XDK+(tb46xJ4) zcw7w<0p3=Idb_FjQ@ttoyDmF?cT4JRGrX5xl&|ViA@Lg!vRR}p#$A?0=Qe+1)Mizl zn;!zhm`B&9t0GA67GF09t_ceE(bGdJ0mbXYrUoV2iuc3c69e;!%)xNOGG*?x*@5k( zh)snvm0s&gRq^{yyeE)>hk~w8)nTN`8HJRtY0~1f`f9ue%RV4~V(K*B;jFfJY4dBb z*BGFK`9M-tpWzayiD>p_`U(29f$R|V-qEB;+_4T939BPb=XRw~8n2cGiRi`o$2qm~ zN&5N7JU{L*QGM@lO8VI)fUA0D7bPrhV(GjJ$+@=dcE5vAVyCy6r&R#4D=GyoEVOnu z8``8q`PN-pEy>xiA_@+EN?EJpY<#}BhrsUJC0afQFx7-pBeLXR9Mr+#w@!wSNR7vxHy@r`!9MFecB4O zh9jye3iSzL0@t3)OZ=OxFjjyK#KSF|zz@K}-+HaY6gW+O{T6%Zky@gD$6SW)Jq;V0 zt&LAG*YFO^+=ULohZZW*=3>7YgND-!$2}2)Mt~c>JO3j6QiPC-*ayH2xBF)2m7+}# z`@m#q{J9r~Dr^eBgrF(l^#sOjlVNFgDs5NR*Xp;V*wr~HqBx7?qBUZ8w)%vIbhhe) zt4(#1S~c$Cq7b_A%wpuah1Qn(X9#obljoY)VUoK%OiQZ#Fa|@ZvGD0_oxR=vz{>U* znC(W7HaUDTc5F!T77GswL-jj7e0#83DH2+lS-T@_^SaWfROz9btt*5zDGck${}*njAwf}3hLqKGLTeV&5(8FC+IP>s;p{L@a~RyCu)MIa zs~vA?_JQ1^2Xc&^cjDq02tT_Z0gkElR0Aa$v@VHi+5*)1(@&}gEXxP5Xon?lxE@is z9sxd|h#w2&P5uHJxWgmtVZJv5w>cl2ALzri;r57qg){6`urTu(2}EI?D?##g=!Sbh z*L*>c9xN1a3CH$u7C~u_!g81`W|xp=54oZl9CM)&V9~ATCC-Q!yfKD@vp#2EKh0(S zgt~aJ^oq-TM0IBol!w1S2j7tJ8H7;SR7yn4-H}iz&U^*zW95HrHiT!H&E|rSlnCYr z7Y1|V7xebn=TFbkH;>WIH6H>8;0?HS#b6lCke9rSsH%3AM1#2U-^*NVhXEIDSFtE^ z=jOo1>j!c__Bub(R*dHyGa)@3h?!ls1&M)d2{?W5#1|M@6|ENYYa`X=2EA_oJUw=I zjQ)K6;C!@>^i7vdf`pBOjH>Ts$97}B=lkb07<&;&?f#cy3I0p5{1=?O*#8m$C_5TE zh}&8lOWWF7I@|pRC$G2;Sm#IJfhKW@^jk=jfM1MdJP(v2fIrYTc{;e5;5gsp`}X8-!{9{S1{h+)<@?+D13s^B zq9(1Pu(Dfl#&z|~qJGuGSWDT&u{sq|huEsbJhiqMUae}K*g+R(vG7P$p6g}w*eYWn zQ7luPl1@{vX?PMK%-IBt+N7TMn~GB z!Ldy^(2Mp{fw_0;<$dgHAv1gZgyJAx%}dA?jR=NPW1K`FkoY zNDgag#YWI6-a2#&_E9NMIE~gQ+*)i<>0c)dSRUMHpg!+AL;a;^u|M1jp#0b<+#14z z+#LuQ1jCyV_GNj#lHWG3e9P@H34~n0VgP#(SBX=v|RSuOiY>L87 z#KA{JDDj2EOBX^{`a;xQxHtY1?q5^B5?up1akjEPhi1-KUsK|J9XEBAbt%^F`t0I- zjRYYKI4OB7Zq3FqJFBZwbI=RuT~J|4tA8x)(v2yB^^+TYYJS>Et`_&yge##PuQ%0I z^|X!Vtof}`UuIxPjoH8kofw4u1pT5h`Ip}d8;l>WcG^qTe>@x63s#zoJiGmDM@_h= zo;8IZR`@AJRLnBNtatipUvL^(1P_a;q8P%&voqy#R!0(bNBTlV&*W9QU?kRV1B*~I zWvI?SNo2cB<7bgVY{F_CF$7z!02Qxfw-Ew#p!8PC#! z1sRfOl`d-Y@&=)l(Sl4CS=>fVvor5lYm61C!!iF3NMocKQHUYr0%QM}a4v2>rzPfM zUO}YRDb7-NEqW+p_;e0{Zi%0C$&B3CKx6|4BW`@`AwsxE?Vu}@Jm<3%T5O&05z+Yq zkK!QF(vlN}Rm}m_J+*W4`8i~R&`P0&5!;^@S#>7qkfb9wxFv@(wN@$k%2*sEwen$a zQnWymf+#Uyv)0lQVd?L1gpS}jMQZ(NHHCKRyu zjK|Zai0|N_)5iv)67(zDBCK4Ktm#ygP|0(m5tU`*AzR&{TSeSY8W=v5^=Ic`ahxM-LBWO+uoL~wxZmgcSJMUF9q%<%>jsvh9Dnp^_e>J_V=ySx4p?SF0Y zg4ZpZt@!h>WR76~P3_YchYOak7oOzR|`t+h!BbN}?zd zq+vMTt0!duALNWDwWVIA$O=%{lWJEj;5(QD()huhFL5=6x_=1h|5ESMW&S|*oxgF# z-0GRIb ziolwI13hJ-Rl(4Rj@*^=&Zz3vD$RX8bFWvBM{niz(%?z0gWNh_vUvpBDoa>-N=P4c zbw-XEJ@txIbc<`wC883;&yE4ayVh>+N($SJ01m}fumz!#!aOg*;y4Hl{V{b;&ux3& zBEmSq2jQ7#IbVm3TPBw?2vVN z0wzj|Y6EBS(V%Pb+@OPkMvEKHW~%DZk#u|A18pZMmCrjWh%7J4Ph>vG61 zRBgJ6w^8dNRg2*=K$Wvh$t>$Q^SMaIX*UpBG)0bqcvY%*by=$EfZAy{ZOA#^tB(D( zh}T(SZgdTj?bG9u+G{Avs5Yr1x=f3k7%K|eJp^>BHK#~dsG<&+=`mM@>kQ-cAJ2k) zT+Ht5liXdc^(aMi9su~{pJUhe)!^U&qn%mV6PS%lye+Iw5F@Xv8E zdR4#?iz+R4--iiHDQmQWfNre=iofAbF~1oGTa1Ce?hId~W^kPuN(5vhNx++ZLkn?l zUA7L~{0x|qA%%%P=8+-Ck{&2$UHn#OQncFS@uUVuE39c9o~#hl)v#!$X(X*4ban2c z{buYr9!`H2;6n73n^W3Vg(!gdBV7$e#v3qubWALaUEAf@`ava{UTx%2~VVQbEE(*Q8_ zv#me9i+0=QnY)$IT+@3vP1l9Wrne+MlZNGO6|zUVG+v&lm7Xw3P*+gS6e#6mVx~(w zyuaXogGTw4!!&P3oZ1|4oc_sGEa&m3Jsqy^lzUdJ^y8RlvUjDmbC^NZ0AmO-c*&m( zSI%4P9f|s!B#073b>Eet`T@J;3qY!NrABuUaED6M^=s-Q^2oZS`jVzuA z>g&g$!Tc>`u-Q9PmKu0SLu-X(tZeZ<%7F+$j3qOOftaoXO5=4!+P!%Cx0rNU+@E~{ zxCclYb~G(Ci%o{}4PC(Bu>TyX9slm5A^2Yi$$kCq-M#Jl)a2W9L-bq5%@Pw^ zh*iuuAz`x6N_rJ1LZ7J^MU9~}RYh+EVIVP+-62u+7IC%1p@;xmmQ`dGCx$QpnIUtK z0`++;Ddz7{_R^~KDh%_yo8WM$IQhcNOALCIGC$3_PtUs?Y44@Osw;OZ()Lk=(H&Vc zXjkHt+^1@M|J%Q&?4>;%T-i%#h|Tb1u;pO5rKst8(Cv2!3U{TRXdm&>fWTJG)n*q&wQPjRzg%pS1RO9}U0*C6fhUi&f#qoV`1{U<&mWKS<$oVFW>{&*$6)r6Rx)F4W zdUL8Mm_qNk6ycFVkI5F?V+cYFUch$92|8O^-Z1JC94GU+Nuk zA#n3Z1q4<6zRiv%W5`NGk*Ym{#0E~IA6*)H-=RmfWIY%mEC0? zSih7uchi`9-WkF2@z1ev6J_N~u;d$QfSNLMgPVpHZoh9oH-8D*;EhoCr~*kJ<|-VD z_jklPveOxWZq40E!SV@0XXy+~Vfn!7nZ1GXsn~U$>#u0d*f?RL9!NMlz^qxYmz|xt zz6A&MUAV#eD%^GcP#@5}QH5e7AV`}(N2#(3xpc!7dDmgu7C3TpgX5Z|$%Vu8=&SQI zdxUk*XS-#C^-cM*O>k}WD5K81e2ayyRA)R&5>KT1QL!T!%@}fw{>BsF+-pzu>;7{g z^CCSWfH;YtJGT@+An0Ded#zM9>UEFOdR_Xq zS~!5R*{p1Whq62ynHo|n$4p7&d|bal{iGsxAY?opi3R${)Zt*8YyOU!$TWMYXF?|i zPXYr}wJp#EH;keSG5WYJ*(~oiu#GDR>C4%-HpIWr7v`W`lzQN-lb?*vpoit z8FqJ)`LC4w8fO8Fu}AYV`awF2NLMS4$f+?=KisU4P6@#+_t)5WDz@f*qE|NG0*hwO z&gv^k^kC6Fg;5>Gr`Q46C{6>3F(p0QukG6NM07rxa&?)_C*eyU(jtli>9Zh#eUb(y zt9NbC-bp0>^m?i`?$aJUyBmF`N0zQ% zvF_;vLVI{tq%Ji%u*8s2p4iBirv*uD(?t~PEz$CfxVa=@R z^HQu6-+I9w>a35kX!P)TfnJDD!)j8!%38(vWNe9vK0{k*`FS$ABZ`rdwfQe@IGDki zssfXnsa6teKXCZUTd^qhhhUZ}>GG_>F0~LG7*<*x;8e39nb-0Bka(l)%+QZ_IVy3q zcmm2uKO0p)9|HGxk*e_$mX2?->&-MXe`=Fz3FRTFfM!$_y}G?{F9jmNgD+L%R`jM1 zIP-kb=3Hlsb35Q&qo(%Ja(LwQj>~!GI|Hgq65J9^A!ibChYB3kxLn@&=#pr}BwON0Q=e5;#sF8GGGuzx6O}z%u3l?jlKF&8Y#lUA)Cs6ZiW8DgOk|q z=YBPAMsO7AoAhWgnSKae2I7%7*Xk>#AyLX-InyBO?OD_^2^nI4#;G|tBvg3C0ldO0 z*`$g(q^es4VqXH2t~0-u^m5cfK8eECh3Rb2h1kW%%^8A!+ya3OHLw$8kHorx4(vJO zAlVu$nC>D{7i?7xDg3116Y2e+)Zb4FPAdZaX}qA!WW{$d?u+sK(iIKqOE-YM zH7y^hkny24==(1;qEacfFU{W{xSXhffC&DJV&oqw`u~WAl@=HIel>KC-mLs2ggFld zsSm-03=Jd^XNDA4i$vKqJ|e|TBc19bglw{)QL${Q(xlN?E;lPumO~;4w_McND6d+R zsc2p*&uRWd`wTDszTcWKiii1mNBrF7n&LQp$2Z<}zkv=8k2s6-^+#siy_K1`5R+n( z++5VOU^LDo(kt3ok?@$3drI`<%+SWcF*`CUWqAJxl3PAq!X|q{al;8%HfgxxM#2Vb zeBS756iU|BzB>bN2NP=AX&!{uZXS;|F`LLd9F^97UTMnNks_t7EPnjZF`2ocD2*u+ z?oKP{xXrD*AKGYGkZtlnvCuazg6g16ZAF{Nu%w+LCZ+v_*`0R$NK)tOh_c#cze;o$ z)kY(eZ5Viv<5zl1XfL(#GO|2FlXL#w3T?hpj3BZ&OAl^L!7@ zy;+iJWYQYP?$(`li_!|bfn!h~k#=v-#XXyjTLd+_txOqZZETqSEp>m+O0ji7MxZ*W zSdq+yqEmafrsLErZG8&;kH2kbCwluSa<@1yU3^Q#5HmW(hYVR0E6!4ZvH;Cr<$`qf zSvqRc`Pq_9b+xrtN3qLmds9;d7HdtlR!2NV$rZPCh6>(7f7M}>C^LeM_5^b$B~mn| z#)?`E=zeo9(9?{O_ko>51~h|c?8{F=2=_-o(-eRc z9p)o51krhCmff^U2oUi#$AG2p-*wSq8DZ(i!Jmu1wzD*)#%J&r)yZTq`3e|v4>EI- z=c|^$Qhv}lEyG@!{G~@}Wbx~vxTxwKoe9zn%5_Z^H$F1?JG_Kadc(G8#|@yaf2-4< zM1bdQF$b5R!W1f`j(S>Id;CHMzfpyjYEC_95VQ*$U3y5piVy=9Rdwg7g&)%#6;U%b2W}_VVdh}qPnM4FY9zFP(5eR zWuCEFox6e;COjs$1RV}IbpE0EV;}5IP}Oq|zcb*77PEDIZU{;@_;8*22{~JRvG~1t zc+ln^I+)Q*+Ha>(@=ra&L&a-kD;l$WEN;YL0q^GE8+})U_A_StHjX_gO{)N>tx4&F zRK?99!6JqktfeS-IsD@74yuq*aFJoV{5&K(W`6Oa2Qy0O5JG>O`zZ-p7vBGh!MxS;}}h6(96Wp`dci3DY?|B@1p8fVsDf$|0S zfE{WL5g3<9&{~yygYyR?jK!>;eZ2L#tpL2)H#89*b zycE?VViXbH7M}m33{#tI69PUPD=r)EVPTBku={Qh{ zKi*pht1jJ+yRhVE)1=Y()iS9j`FesMo$bjLSqPMF-i<42Hxl6%y7{#vw5YT(C}x0? z$rJU7fFmoiR&%b|Y*pG?7O&+Jb#Z%S8&%o~fc?S9c`Dwdnc4BJC7njo7?3bp#Yonz zPC>y`DVK~nzN^n}jB5RhE4N>LzhCZD#WQseohYXvqp5^%Ns!q^B z&8zQN(jgPS(2ty~g2t9!x9;Dao~lYVujG-QEq{vZp<1Nlp;oj#kFVsBnJssU^p-4% zKF_A?5sRmA>d*~^og-I95z$>T*K*33TGBPzs{OMoV2i+(P6K|95UwSj$Zn<@Rt(g%|iY z$SkSjYVJ)I<@S(kMQ6md{HxAa8S`^lXGV?ktLX!ngTVI~%WW+p#A#XTWaFWeBAl%U z&rVhve#Yse*h4BC4nrq7A1n>Rlf^ErbOceJC`o#fyCu@H;y)`E#a#)w)3eg^{Hw&E7);N5*6V+z%olvLj zp^aJ4`h*4L4ij)K+uYvdpil(Z{EO@u{BcMI&}5{ephilI%zCkBhBMCvOQT#zp|!18 zuNl=idd81|{FpGkt%ty=$fnZnWXxem!t4x{ zat@68CPmac(xYaOIeF}@O1j8O?2jbR!KkMSuix;L8x?m01}|bS2=&gsjg^t2O|+0{ zlzfu5r5_l4)py8uPb5~NHPG>!lYVynw;;T-gk1Pl6PQ39Mwgd2O+iHDB397H)2grN zHwbd>8i%GY>Pfy7;y5X7AN>qGLZVH>N_ZuJZ-`z9UA> zfyb$nbmPqxyF2F;UW}7`Cu>SS%0W6h^Wq5e{PWAjxlh=#Fq+6SiPa-L*551SZKX&w zc9TkPv4eao?kqomkZ#X%tA{`UIvf|_=Y7p~mHZKqO>i_;q4PrwVtUDTk?M7NCssa?Y4uxYrsXj!+k@`Cxl;&{NLs*6!R<6k9$Bq z%grLhxJ#G_j~ytJpiND8neLfvD0+xu>wa$-%5v;4;RYYM66PUab)c9ruUm%d{^s{# zTBBY??@^foRv9H}iEf{w_J%rV<%T1wv^`)Jm#snLTIifjgRkX``x2wV(D6(=VTLL4 zI-o}&5WuwBl~(XSLIn5~{cGWorl#z+=(vXuBXC#lp}SdW=_)~8Z(Vv!#3h2@pdA3d z{cIPYK@Ojc9(ph=H3T7;aY>(S3~iuIn05Puh^32WObj%hVN(Y{Ty?n?Cm#!kGNZFa zW6Ybz!tq|@erhtMo4xAus|H8V_c+XfE5mu|lYe|{$V3mKnb1~fqoFim;&_ZHN_=?t zysQwC4qO}rTi}k8_f=R&i27RdBB)@bTeV9Wcd}Rysvod}7I%ujwYbTI*cN7Kbp_hO z=eU521!#cx$0O@k9b$;pnCTRtLIzv){nVW6Ux1<0@te6`S5%Ew3{Z^9=lbL5$NFvd4eUtK?%zgmB;_I&p`)YtpN`2Im(?jPN<(7Ua_ZWJRF(CChv`(gHfWodK%+joy>8Vaa;H1w zIJ?!kA|x7V;4U1BNr(UrhfvjPii7YENLIm`LtnL9Sx z5E9TYaILoB2nSwDe|BVmrpLT43*dJ8;T@1l zJE)4LEzIE{IN}+Nvpo3=ZtV!U#D;rB@9OXYw^4QH+(52&pQEcZq&~u9bTg63ikW9! z=!_RjN2xO=F+bk>fSPhsjQA;)%M1My#34T`I7tUf>Q_L>DRa=>Eo(sapm>}}LUsN% zVw!C~a)xcca`G#g*Xqo>_uCJTz>LoWGSKOwp-tv`yvfqw{17t`9Z}U4o+q2JGP^&9 z(m}|d13XhYSnEm$_8vH-Lq$A^>oWUz1)bnv|AVn_0FwM$vYu&8+qUg$+qP}nwrykD zwmIF?wr$()X@33oz1@B9zi+?Th^nZnsES)rb@O*K^JL~ZH|pRRk$i0+ohh?Il)y&~ zQaq{}9YxPt5~_2|+r#{k#~SUhO6yFq)uBGtYMMg4h1qddg!`TGHocYROyNFJtYjNe z3oezNpq6%TP5V1g(?^5DMeKV|i6vdBq)aGJ)BRv;K(EL0_q7$h@s?BV$)w31*c(jd z{@hDGl3QdXxS=#?0y3KmPd4JL(q(>0ikTk6nt98ptq$6_M|qrPi)N>HY>wKFbnCKY z%0`~`9p)MDESQJ#A`_>@iL7qOCmCJ(p^>f+zqaMuDRk!z01Nd2A_W^D%~M73jTqC* zKu8u$$r({vP~TE8rPk?8RSjlRvG*BLF}ye~Su%s~rivmjg2F z24dhh6-1EQF(c>Z1E8DWY)Jw#9U#wR<@6J)3hjA&2qN$X%piJ4s={|>d-|Gzl~RNu z##iR(m;9TN3|zh+>HgTI&82iR>$YVoOq$a(2%l*2mNP(AsV=lR^>=tIP-R9Tw!BYnZROx`PN*JiNH>8bG}&@h0_v$yOTk#@1;Mh;-={ZU7e@JE(~@@y0AuETvsqQV@7hbKe2wiWk@QvV=Kz`%@$rN z_0Hadkl?7oEdp5eaaMqBm;#Xj^`fxNO^GQ9S3|Fb#%{lN;1b`~yxLGEcy8~!cz{!! z=7tS!I)Qq%w(t9sTSMWNhoV#f=l5+a{a=}--?S!rA0w}QF!_Eq>V4NbmYKV&^OndM z4WiLbqeC5+P@g_!_rs01AY6HwF7)$~%Ok^(NPD9I@fn5I?f$(rcOQjP+z?_|V0DiN zb}l0fy*el9E3Q7fVRKw$EIlb&T0fG~fDJZL7Qn8*a5{)vUblM)*)NTLf1ll$ zpQ^(0pkSTol`|t~`Y4wzl;%NRn>689mpQrW=SJ*rB;7}w zVHB?&sVa2%-q@ANA~v)FXb`?Nz8M1rHKiZB4xC9<{Q3T!XaS#fEk=sXI4IFMnlRqG+yaFw< zF{}7tcMjV04!-_FFD8(FtuOZx+|CjF@-xl6-{qSFF!r7L3yD()=*Ss6fT?lDhy(h$ zt#%F575$U(3-e2LsJd>ksuUZZ%=c}2dWvu8f!V%>z3gajZ!Dlk zm=0|(wKY`c?r$|pX6XVo6padb9{EH}px)jIsdHoqG^(XH(7}r^bRa8BC(%M+wtcB? z6G2%tui|Tx6C3*#RFgNZi9emm*v~txI}~xV4C`Ns)qEoczZ>j*r zqQCa5k90Gntl?EX!{iWh=1t$~jVoXjs&*jKu0Ay`^k)hC^v_y0xU~brMZ6PPcmt5$ z@_h`f#qnI$6BD(`#IR0PrITIV^~O{uo=)+Bi$oHA$G* zH0a^PRoeYD3jU_k%!rTFh)v#@cq`P3_y=6D(M~GBud;4 zCk$LuxPgJ5=8OEDlnU!R^4QDM4jGni}~C zy;t2E%Qy;A^bz_5HSb5pq{x{g59U!ReE?6ULOw58DJcJy;H?g*ofr(X7+8wF;*3{rx>j&27Syl6A~{|w{pHb zeFgu0E>OC81~6a9(2F13r7NZDGdQxR8T68&t`-BK zE>ZV0*0Ba9HkF_(AwfAds-r=|dA&p`G&B_zn5f9Zfrz9n#Rvso`x%u~SwE4SzYj!G zVQ0@jrLwbYP=awX$21Aq!I%M{x?|C`narFWhp4n;=>Sj!0_J!k7|A0;N4!+z%Oqlk z1>l=MHhw3bi1vT}1!}zR=6JOIYSm==qEN#7_fVsht?7SFCj=*2+Ro}B4}HR=D%%)F z?eHy=I#Qx(vvx)@Fc3?MT_@D))w@oOCRR5zRw7614#?(-nC?RH`r(bb{Zzn+VV0bm zJ93!(bfrDH;^p=IZkCH73f*GR8nDKoBo|!}($3^s*hV$c45Zu>6QCV(JhBW=3(Tpf z=4PT6@|s1Uz+U=zJXil3K(N6;ePhAJhCIo`%XDJYW@x#7Za);~`ANTvi$N4(Fy!K- z?CQ3KeEK64F0@ykv$-0oWCWhYI-5ZC1pDqui@B|+LVJmU`WJ=&C|{I_))TlREOc4* zSd%N=pJ_5$G5d^3XK+yj2UZasg2) zXMLtMp<5XWWfh-o@ywb*nCnGdK{&S{YI54Wh2|h}yZ})+NCM;~i9H@1GMCgYf`d5n zwOR(*EEkE4-V#R2+Rc>@cAEho+GAS2L!tzisLl${42Y=A7v}h;#@71_Gh2MV=hPr0_a% z0!={Fcv5^GwuEU^5rD|sP;+y<%5o9;#m>ssbtVR2g<420(I-@fSqfBVMv z?`>61-^q;M(b3r2z{=QxSjyH=-%99fpvb}8z}d;%_8$$J$qJg1Sp3KzlO_!nCn|g8 zzg8skdHNsfgkf8A7PWs;YBz_S$S%!hWQ@G>guCgS--P!!Ui9#%GQ#Jh?s!U-4)7ozR?i>JXHU$| zg0^vuti{!=N|kWorZNFX`dJgdphgic#(8sOBHQdBkY}Qzp3V%T{DFb{nGPgS;QwnH9B9;-Xhy{? z(QVwtzkn9I)vHEmjY!T3ifk1l5B?%%TgP#;CqG-?16lTz;S_mHOzu#MY0w}XuF{lk z*dt`2?&plYn(B>FFXo+fd&CS3q^hquSLVEn6TMAZ6e*WC{Q2e&U7l|)*W;^4l~|Q= zt+yFlLVqPz!I40}NHv zE2t1meCuGH%<`5iJ(~8ji#VD{?uhP%F(TnG#uRZW-V}1=N%ev&+Gd4v!0(f`2Ar-Y z)GO6eYj7S{T_vxV?5^%l6TF{ygS_9e2DXT>9caP~xq*~oE<5KkngGtsv)sdCC zaQH#kSL%c*gLj6tV)zE6SGq|0iX*DPV|I`byc9kn_tNQkPU%y<`rj zMC}lD<93=Oj+D6Y2GNMZb|m$^)RVdi`&0*}mxNy0BW#0iq!GGN2BGx5I0LS>I|4op z(6^xWULBr=QRpbxIJDK~?h;K#>LwQI4N<8V?%3>9I5l+e*yG zFOZTIM0c3(q?y9f7qDHKX|%zsUF%2zN9jDa7%AK*qrI5@z~IruFP+IJy7!s~TE%V3 z_PSSxXlr!FU|Za>G_JL>DD3KVZ7u&}6VWbwWmSg?5;MabycEB)JT(eK8wg`^wvw!Q zH5h24_E$2cuib&9>Ue&@%Cly}6YZN-oO_ei5#33VvqV%L*~ZehqMe;)m;$9)$HBsM zfJ96Hk8GJyWwQ0$iiGjwhxGgQX$sN8ij%XJzW`pxqgwW=79hgMOMnC|0Q@ed%Y~=_ z?OnjUB|5rS+R$Q-p)vvM(eFS+Qr{_w$?#Y;0Iknw3u(+wA=2?gPyl~NyYa3me{-Su zhH#8;01jEm%r#5g5oy-f&F>VA5TE_9=a0aO4!|gJpu470WIrfGo~v}HkF91m6qEG2 zK4j=7C?wWUMG$kYbIp^+@)<#ArZ$3k^EQxraLk0qav9TynuE7T79%MsBxl3|nRn?L zD&8kt6*RJB6*a7=5c57wp!pg)p6O?WHQarI{o9@3a32zQ3FH8cK@P!DZ?CPN_LtmC6U4F zlv8T2?sau&+(i@EL6+tvP^&=|aq3@QgL4 zOu6S3wSWeYtgCnKqg*H4ifIQlR4hd^n{F+3>h3;u_q~qw-Sh;4dYtp^VYymX12$`? z;V2_NiRt82RC=yC+aG?=t&a81!gso$hQUb)LM2D4Z{)S zI1S9f020mSm(Dn$&Rlj0UX}H@ zv={G+fFC>Sad0~8yB%62V(NB4Z|b%6%Co8j!>D(VyAvjFBP%gB+`b*&KnJ zU8s}&F+?iFKE(AT913mq;57|)q?ZrA&8YD3Hw*$yhkm;p5G6PNiO3VdFlnH-&U#JH zEX+y>hB(4$R<6k|pt0?$?8l@zeWk&1Y5tlbgs3540F>A@@rfvY;KdnVncEh@N6Mfi zY)8tFRY~Z?Qw!{@{sE~vQy)0&fKsJpj?yR`Yj+H5SDO1PBId3~d!yjh>FcI#Ug|^M z7-%>aeyQhL8Zmj1!O0D7A2pZE-$>+-6m<#`QX8(n)Fg>}l404xFmPR~at%$(h$hYD zoTzbxo`O{S{E}s8Mv6WviXMP}(YPZoL11xfd>bggPx;#&pFd;*#Yx%TtN1cp)MuHf z+Z*5CG_AFPwk624V9@&aL0;=@Ql=2h6aJoqWx|hPQQzdF{e7|fe(m){0==hk_!$ou zI|p_?kzdO9&d^GBS1u+$>JE-6Ov*o{mu@MF-?$r9V>i%;>>Fo~U`ac2hD*X}-gx*v z1&;@ey`rA0qNcD9-5;3_K&jg|qvn@m^+t?8(GTF0l#|({Zwp^5Ywik@bW9mN+5`MU zJ#_Ju|jtsq{tv)xA zY$5SnHgHj}c%qlQG72VS_(OSv;H~1GLUAegygT3T-J{<#h}))pk$FjfRQ+Kr%`2ZiI)@$96Nivh82#K@t>ze^H?R8wHii6Pxy z0o#T(lh=V>ZD6EXf0U}sG~nQ1dFI`bx;vivBkYSVkxXn?yx1aGxbUiNBawMGad;6? zm{zp?xqAoogt=I2H0g@826=7z^DmTTLB11byYvAO;ir|O0xmNN3Ec0w%yHO({-%q(go%?_X{LP?=E1uXoQgrEGOfL1?~ zI%uPHC23dn-RC@UPs;mxq6cFr{UrgG@e3ONEL^SoxFm%kE^LBhe_D6+Ia+u0J=)BC zf8FB!0J$dYg33jb2SxfmkB|8qeN&De!%r5|@H@GiqReK(YEpnXC;-v~*o<#JmYuze zW}p-K=9?0=*fZyYTE7A}?QR6}m_vMPK!r~y*6%My)d;x4R?-=~MMLC_02KejX9q6= z4sUB4AD0+H4ulSYz4;6mL8uaD07eXFvpy*i5X@dmx--+9`ur@rcJ5<L#s%nq3MRi4Dpr;#28}dl36M{MkVs4+Fm3Pjo5qSV)h}i(2^$Ty|<7N z>*LiBzFKH30D!$@n^3B@HYI_V1?yM(G$2Ml{oZ}?frfPU+{i|dHQOP^M0N2#NN_$+ zs*E=MXUOd=$Z2F4jSA^XIW=?KN=w6{_vJ4f(ZYhLxvFtPozPJv9k%7+z!Zj+_0|HC zMU0(8`8c`Sa=%e$|Mu2+CT22Ifbac@7Vn*he`|6Bl81j`44IRcTu8aw_Y%;I$Hnyd zdWz~I!tkWuGZx4Yjof(?jM;exFlUsrj5qO=@2F;56&^gM9D^ZUQ!6TMMUw19zslEu zwB^^D&nG96Y+Qwbvgk?Zmkn9%d{+V;DGKmBE(yBWX6H#wbaAm&O1U^ zS4YS7j2!1LDC6|>cfdQa`}_^satOz6vc$BfFIG07LoU^IhVMS_u+N=|QCJao0{F>p z-^UkM)ODJW9#9*o;?LPCRV1y~k9B`&U)jbTdvuxG&2%!n_Z&udT=0mb@e;tZ$_l3bj6d0K2;Ya!&)q`A${SmdG_*4WfjubB)Mn+vaLV+)L5$yD zYSTGxpVok&fJDG9iS8#oMN{vQneO|W{Y_xL2Hhb%YhQJgq7j~X7?bcA|B||C?R=Eo z!z;=sSeKiw4mM$Qm>|aIP3nw36Tbh6Eml?hL#&PlR5xf9^vQGN6J8op1dpLfwFg}p zlqYx$610Zf?=vCbB_^~~(e4IMic7C}X(L6~AjDp^;|=d$`=!gd%iwCi5E9<6Y~z0! zX8p$qprEadiMgq>gZ_V~n$d~YUqqqsL#BE6t9ufXIUrs@DCTfGg^-Yh5Ms(wD1xAf zTX8g52V!jr9TlWLl+whcUDv?Rc~JmYs3haeG*UnV;4bI=;__i?OSk)bF3=c9;qTdP zeW1exJwD+;Q3yAw9j_42Zj9nuvs%qGF=6I@($2Ue(a9QGRMZTd4ZAlxbT5W~7(alP1u<^YY!c3B7QV z@jm$vn34XnA6Gh1I)NBgTmgmR=O1PKp#dT*mYDPRZ=}~X3B8}H*e_;;BHlr$FO}Eq zJ9oWk0y#h;N1~ho724x~d)A4Z-{V%F6#e5?Z^(`GGC}sYp5%DKnnB+i-NWxwL-CuF+^JWNl`t@VbXZ{K3#aIX+h9-{T*+t(b0BM&MymW9AA*{p^&-9 zWpWQ?*z(Yw!y%AoeoYS|E!(3IlLksr@?Z9Hqlig?Q4|cGe;0rg#FC}tXTmTNfpE}; z$sfUYEG@hLHUb$(K{A{R%~%6MQN|Bu949`f#H6YC*E(p3lBBKcx z-~Bsd6^QsKzB0)$FteBf*b3i7CN4hccSa-&lfQz4qHm>eC|_X!_E#?=`M(bZ{$cvU zZpMbr|4omp`s9mrgz@>4=Fk3~8Y7q$G{T@?oE0<(I91_t+U}xYlT{c&6}zPAE8ikT z3DP!l#>}i!A(eGT+@;fWdK#(~CTkwjs?*i4SJVBuNB2$6!bCRmcm6AnpHHvnN8G<| zuh4YCYC%5}Zo;BO1>L0hQ8p>}tRVx~O89!${_NXhT!HUoGj0}bLvL2)qRNt|g*q~B z7U&U7E+8Ixy1U`QT^&W@ZSRN|`_Ko$-Mk^^c%`YzhF(KY9l5))1jSyz$&>mWJHZzHt0Jje%BQFxEV}C00{|qo5_Hz7c!FlJ|T(JD^0*yjkDm zL}4S%JU(mBV|3G2jVWU>DX413;d+h0C3{g3v|U8cUj`tZL37Sf@1d*jpwt4^B)`bK zZdlwnPB6jfc7rIKsldW81$C$a9BukX%=V}yPnaBz|i6(h>S)+Bn44@i8RtBZf0XetH&kAb?iAL zD%Ge{>Jo3sy2hgrD?15PM}X_)(6$LV`&t*D`IP)m}bzM)+x-xRJ zavhA)>hu2cD;LUTvN38FEtB94ee|~lIvk~3MBPzmTsN|7V}Kzi!h&za#NyY zX^0BnB+lfBuW!oR#8G&S#Er2bCVtA@5FI`Q+a-e?G)LhzW_chWN-ZQmjtR

eWu-UOPu^G}|k=o=;ffg>8|Z*qev7qS&oqA7%Z{4Ezb!t$f3& z^NuT8CSNp`VHScyikB1YO{BgaBVJR&>dNIEEBwYkfOkWN;(I8CJ|vIfD}STN z{097)R9iC@6($s$#dsb*4BXBx7 zb{6S2O}QUk>upEfij9C2tjqWy7%%V@Xfpe)vo6}PG+hmuY1Tc}peynUJLLmm)8pshG zb}HWl^|sOPtYk)CD-7{L+l(=F zOp}fX8)|n{JDa&9uI!*@jh^^9qP&SbZ(xxDhR)y|bjnn|K3MeR3gl6xcvh9uqzb#K zYkVjnK$;lUky~??mcqN-)d5~mk{wXhrf^<)!Jjqc zG~hX0P_@KvOKwV=X9H&KR3GnP3U)DfqafBt$e10}iuVRFBXx@uBQ)sn0J%%c<;R+! zQz;ETTVa+ma>+VF%U43w?_F6s0=x@N2(oisjA7LUOM<$|6iE|$WcO67W|KY8JUV_# zg7P9K3Yo-c*;EmbsqT!M4(WT`%9uk+s9Em-yB0bE{B%F4X<8fT!%4??vezaJ(wJhj zfOb%wKfkY3RU}7^FRq`UEbB-#A-%7)NJQwQd1As=!$u#~2vQ*CE~qp`u=_kL<`{OL zk>753UqJVx1-4~+d@(pnX-i zV4&=eRWbJ)9YEGMV53poXpv$vd@^yd05z$$@i5J7%>gYKBx?mR2qGv&BPn!tE-_aW zg*C!Z&!B zH>3J16dTJC(@M0*kIc}Jn}jf=f*agba|!HVm|^@+7A?V>Woo!$SJko*Jv1mu>;d}z z^vF{3u5Mvo_94`4kq2&R2`32oyoWc2lJco3`Ls0Ew4E7*AdiMbn^LCV%7%mU)hr4S3UVJjDLUoIKRQ)gm?^{1Z}OYzd$1?a~tEY ztjXmIM*2_qC|OC{7V%430T?RsY?ZLN$w!bkDOQ0}wiq69){Kdu3SqW?NMC))S}zq^ zu)w!>E1!;OrXO!RmT?m&PA;YKUjJy5-Seu=@o;m4*Vp$0OipBl4~Ub)1xBdWkZ47=UkJd$`Z}O8ZbpGN$i_WtY^00`S8=EHG#Ff{&MU1L(^wYjTchB zMTK%1LZ(eLLP($0UR2JVLaL|C2~IFbWirNjp|^=Fl48~Sp9zNOCZ@t&;;^avfN(NpNfq}~VYA{q%yjHo4D>JB>XEv(~Z!`1~SoY=9v zTq;hrjObE_h)cmHXLJ>LC_&XQ2BgGfV}e#v}ZF}iF97bG`Nog&O+SA`2zsn%bbB309}I$ zYi;vW$k@fC^muYBL?XB#CBuhC&^H)F4E&vw(5Q^PF{7~}(b&lF4^%DQzL0(BVk?lM zTHXTo4?Ps|dRICEiux#y77_RF8?5!1D-*h5UY&gRY`WO|V`xxB{f{DHzBwvt1W==r zdfAUyd({^*>Y7lObr;_fO zxDDw7X^dO`n!PLqHZ`by0h#BJ-@bAFPs{yJQ~Ylj^M5zWsxO_WFHG}8hH>OK{Q)9` zSRP94d{AM(q-2x0yhK@aNMv!qGA5@~2tB;X?l{Pf?DM5Y*QK`{mGA? zjx;gwnR~#Nep12dFk<^@-U{`&`P1Z}Z3T2~m8^J&7y}GaMElsTXg|GqfF3>E#HG=j zMt;6hfbfjHSQ&pN9(AT8q$FLKXo`N(WNHDY!K6;JrHZCO&ISBdX`g8sXvIf?|8 zX$-W^ut!FhBxY|+R49o44IgWHt}$1BuE|6|kvn1OR#zhyrw}4H*~cpmFk%K(CTGYc zNkJ8L$eS;UYDa=ZHWZy`rO`!w0oIcgZnK&xC|93#nHvfb^n1xgxf{$LB`H1ao+OGb zKG_}>N-RHSqL(RBdlc7J-Z$Gaay`wEGJ_u-lo88{`aQ*+T~+x(H5j?Q{uRA~>2R+} zB+{wM2m?$->unwg8-GaFrG%ZmoHEceOj{W21)Mi2lAfT)EQuNVo+Do%nHPuq7Ttt7 z%^6J5Yo64dH671tOUrA7I2hL@HKZq;S#Ejxt;*m-l*pPj?=i`=E~FAXAb#QH+a}-% z#3u^pFlg%p{hGiIp>05T$RiE*V7bPXtkz(G<+^E}Risi6F!R~Mbf(Qz*<@2&F#vDr zaL#!8!&ughWxjA(o9xtK{BzzYwm_z2t*c>2jI)c0-xo8ahnEqZ&K;8uF*!Hg0?Gd* z=eJK`FkAr>7$_i$;kq3Ks5NNJkNBnw|1f-&Ys56c9Y@tdM3VTTuXOCbWqye9va6+ZSeF0eh} zYb^ct&4lQTfNZ3M3(9?{;s><(zq%hza7zcxlZ+`F8J*>%4wq8s$cC6Z=F@ zhbvdv;n$%vEI$B~B)Q&LkTse!8Vt};7Szv2@YB!_Ztp@JA>rc(#R1`EZcIdE+JiI% zC2!hgYt+~@%xU?;ir+g92W`*j z3`@S;I6@2rO28zqj&SWO^CvA5MeNEhBF+8-U0O0Q1Co=I^WvPl%#}UFDMBVl z5iXV@d|`QTa$>iw;m$^}6JeuW zjr;{)S2TfK0Q%xgHvONSJb#NA|LOmg{U=k;R?&1tQbylMEY4<1*9mJh&(qo`G#9{X zYRs)#*PtEHnO;PV0G~6G`ca%tpKgb6<@)xc^SQY58lTo*S$*sv5w7bG+8YLKYU`8{ zNBVlvgaDu7icvyf;N&%42z2L4(rR<*Jd48X8Jnw zN>!R$%MZ@~Xu9jH?$2Se&I|ZcW>!26BJP?H7og0hT(S`nXh6{sR36O^7%v=31T+eL z)~BeC)15v>1m#(LN>OEwYFG?TE0_z)MrT%3SkMBBjvCd6!uD+03Jz#!s#Y~b1jf>S z&Rz5&8rbLj5!Y;(Hx|UY(2aw~W(8!3q3D}LRE%XX(@h5TnP@PhDoLVQx;6|r^+Bvs zaR55cR%Db9hZ<<|I%dDkone+8Sq7dqPOMnGoHk~-R*#a8w$c)`>4U`k+o?2|E>Sd4 zZ0ZVT{95pY$qKJ54K}3JB!(WcES>F+x56oJBRg))tMJ^#Qc(2rVcd5add=Us6vpBNkIg9b#ulk%!XBU zV^fH1uY(rGIAiFew|z#MM!qsVv%ZNb#why9%9In4Kj-hDYtMdirWLFzn~de!nnH(V zv0>I3;X#N)bo1$dFzqo(tzmvqNUKraAz~?)OSv42MeM!OYu;2VKn2-s7#fucX`|l~ zplxtG1Pgk#(;V=`P_PZ`MV{Bt4$a7;aLvG@KQo%E=;7ZO&Ws-r@XL+AhnPn>PAKc7 zQ_iQ4mXa-a4)QS>cJzt_j;AjuVCp8g^|dIV=DI0>v-f_|w5YWAX61lNBjZEZax3aV znher(j)f+a9_s8n#|u=kj0(unR1P-*L7`{F28xv054|#DMh}q=@rs@-fbyf(2+52L zN>hn3v!I~%jfOV=j(@xLOsl$Jv-+yR5{3pX)$rIdDarl7(C3)})P`QoHN|y<<2n;` zJ0UrF=Zv}d=F(Uj}~Yv9(@1pqUSRa5_bB*AvQ|Z-6YZ*N%p(U z<;Bpqr9iEBe^LFF!t{1UnRtaH-9=@p35fMQJ~1^&)(2D|^&z?m z855r&diVS6}jmt2)A7LZDiv;&Ys6@W5P{JHY!!n7W zvj3(2{1R9Y=TJ|{^2DK&be*ZaMiRHw>WVI^701fC) zAp1?8?oiU%Faj?Qhou6S^d11_7@tEK-XQ~%q!!7hha-Im^>NcRF7OH7s{IO7arZQ{ zE8n?2><7*!*lH}~usWPWZ}2&M+)VQo7C!AWJSQc>8g_r-P`N&uybK5)p$5_o;+58Q z-Ux2l<3i|hxqqur*qAfHq=)?GDchq}ShV#m6&w|mi~ar~`EO_S=fb~<}66U>5i7$H#m~wR;L~4yHL2R&;L*u7-SPdHxLS&Iy76q$2j#Pe)$WulRiCICG*t+ zeehM8`!{**KRL{Q{8WCEFLXu3+`-XF(b?c1Z~wg?c0lD!21y?NLq?O$STk3NzmrHM zsCgQS5I+nxDH0iyU;KKjzS24GJmG?{D`08|N-v+Egy92lBku)fnAM<}tELA_U`)xKYb=pq|hejMCT1-rg0Edt6(*E9l9WCKI1a=@c99swp2t6Tx zFHy`8Hb#iXS(8c>F~({`NV@F4w0lu5X;MH6I$&|h*qfx{~DJ*h5e|61t1QP}tZEIcjC%!Fa)omJTfpX%aI+OD*Y(l|xc0$1Zip;4rx; zV=qI!5tSuXG7h?jLR)pBEx!B15HCoVycD&Z2dlqN*MFQDb!|yi0j~JciNC!>){~ zQQgmZvc}0l$XB0VIWdg&ShDTbTkArryp3x)T8%ulR;Z?6APx{JZyUm=LC-ACkFm`6 z(x7zm5ULIU-xGi*V6x|eF~CN`PUM%`!4S;Uv_J>b#&OT9IT=jx5#nydC4=0htcDme zDUH*Hk-`Jsa>&Z<7zJ{K4AZE1BVW%zk&MZ^lHyj8mWmk|Pq8WwHROz0Kwj-AFqvR)H2gDN*6dzVk>R3@_CV zw3Z@6s^73xW)XY->AFwUlk^4Q=hXE;ckW=|RcZFchyOM0vqBW{2l*QR#v^SZNnT6j zZv|?ZO1-C_wLWVuYORQryj29JA; zS4BsxfVl@X!W{!2GkG9fL4}58Srv{$-GYngg>JuHz!7ZPQbfIQr4@6ZC4T$`;Vr@t zD#-uJ8A!kSM*gA&^6yWi|F}&59^*Rx{qn3z{(JYxrzg!X2b#uGd>&O0e=0k_2*N?3 zYXV{v={ONL{rW~z_FtFj7kSSJZ?s);LL@W&aND7blR8rlvkAb48RwJZlOHA~t~RfC zOD%ZcOzhYEV&s9%qns0&ste5U!^MFWYn`Od()5RwIz6%@Ek+Pn`s79unJY-$7n-Uf z&eUYvtd)f7h7zG_hDiFC!psCg#q&0c=GHKOik~$$>$Fw*k z;G)HS$IR)Cu72HH|JjeeauX;U6IgZ_IfxFCE_bGPAU25$!j8Etsl0Rk@R`$jXuHo8 z3Hhj-rTR$Gq(x)4Tu6;6rHQhoCvL4Q+h0Y+@Zdt=KTb0~wj7-(Z9G%J+aQu05@k6JHeCC|YRFWGdDCV}ja;-yl^9<`>f=AwOqML1a~* z9@cQYb?!+Fmkf}9VQrL8$uyq8k(r8)#;##xG9lJ-B)Fg@15&To(@xgk9SP*bkHlxiy8I*wJQylh(+9X~H-Is!g&C!q*eIYuhl&fS&|w)dAzXBdGJ&Mp$+8D| zZaD<+RtjI90QT{R0YLk6_dm=GfCg>7;$ zlyLsNYf@MfLH<}ott5)t2CXiQos zFLt^`%ygB2Vy^I$W3J_Rt4olRn~Gh}AW(`F@LsUN{d$sR%bU&3;rsD=2KCL+4c`zv zlI%D>9-)U&R3;>d1Vdd5b{DeR!HXDm44Vq*u?`wziLLsFUEp4El;*S0;I~D#TgG0s zBXYZS{o|Hy0A?LVNS)V4c_CFwyYj-E#)4SQq9yaf`Y2Yhk7yHSdos~|fImZG5_3~~o<@jTOH@Mc7`*xn-aO5F zyFT-|LBsm(NbWkL^oB-Nd31djBaYebhIGXhsJyn~`SQ6_4>{fqIjRp#Vb|~+Qi}Mdz!Zsw= zz?5L%F{c{;Cv3Q8ab>dsHp)z`DEKHf%e9sT(aE6$az?A}3P`Lm(~W$8Jr=;d8#?dm_cmv>2673NqAOenze z=&QW`?TQAu5~LzFLJvaJ zaBU3mQFtl5z?4XQDBWNPaH4y)McRpX#$(3o5Nx@hVoOYOL&-P+gqS1cQ~J;~1roGH zVzi46?FaI@w-MJ0Y7BuAg*3;D%?<_OGsB3)c|^s3A{UoAOLP8scn`!5?MFa|^cTvq z#%bYG3m3UO9(sH@LyK9-LSnlVcm#5^NRs9BXFtRN9kBY2mPO|@b7K#IH{B{=0W06) zl|s#cIYcreZ5p3j>@Ly@35wr-q8z5f9=R42IsII=->1stLo@Q%VooDvg@*K(H@*5g zUPS&cM~k4oqp`S+qp^*nxzm^0mg3h8ppEHQ@cXyQ=YKV-6)FB*$KCa{POe2^EHr{J zOxcVd)s3Mzs8m`iV?MSp=qV59blW9$+$P+2;PZDRUD~sr*CQUr&EDiCSfH@wuHez+ z`d5p(r;I7D@8>nbZ&DVhT6qe+accH;<}q$8Nzz|d1twqW?UV%FMP4Y@NQ`3(+5*i8 zP9*yIMP7frrneG3M9 zf>GsjA!O#Bifr5np-H~9lR(>#9vhE6W-r`EjjeQ_wdWp+rt{{L5t5t(Ho|4O24@}4 z_^=_CkbI`3;~sXTnnsv=^b3J}`;IYyvb1gM>#J9{$l#Zd*W!;meMn&yXO7x`Epx_Y zm-1wlu~@Ii_7D}>%tzlXW;zQT=uQXSG@t$<#6-W*^vy7Vr2TCpnix@7!_|aNXEnN<-m?Oq;DpN*x6f>w za1Wa5entFEDtA0SD%iZv#3{wl-S`0{{i3a9cmgNW`!TH{J*~{@|5f%CKy@uk*8~af zt_d34U4y&3y9IZ5cXxLQ?(XjH5?q3Z0KxK~y!-CUyWG6{<)5lkhbox0HnV&7^zNBn zjc|?X!Y=63(Vg>#&Wx%=LUr5{i@~OdzT#?P8xu#P*I_?Jl7xM4dq)4vi}3Wj_c=XI zSbc)@Q2Et4=(nBDU{aD(F&*%Ix!53_^0`+nOFk)}*34#b0Egffld|t_RV91}S0m)0 zap{cQDWzW$geKzYMcDZDAw480!1e1!1Onpv9fK9Ov~sfi!~OeXb(FW)wKx335nNY! za6*~K{k~=pw`~3z!Uq%?MMzSl#s%rZM{gzB7nB*A83XIGyNbi|H8X>a5i?}Rs+z^; z2iXrmK4|eDOu@{MdS+?@(!-Ar4P4?H_yjTEMqm7`rbV4P275(-#TW##v#Dt14Yn9UB-Sg3`WmL0+H~N;iC`Mg%pBl?1AAOfZ&e; z*G=dR>=h_Mz@i;lrGpIOQwezI=S=R8#);d*;G8I(39ZZGIpWU)y?qew(t!j23B9fD z?Uo?-Gx3}6r8u1fUy!u)7LthD2(}boE#uhO&mKBau8W8`XV7vO>zb^ZVWiH-DOjl2 zf~^o1CYVU8eBdmpAB=T%i(=y}!@3N%G-*{BT_|f=egqtucEtjRJJhSf)tiBhpPDpgzOpG12UgvOFnab&16Zn^2ZHjs)pbd&W1jpx%%EXmE^ zdn#R73^BHp3w%&v!0~azw(Fg*TT*~5#dJw%-UdxX&^^(~V&C4hBpc+bPcLRZizWlc zjR;$4X3Sw*Rp4-o+a4$cUmrz05RucTNoXRINYG*DPpzM&;d1GNHFiyl(_x#wspacQ zL)wVFXz2Rh0k5i>?Ao5zEVzT)R(4Pjmjv5pzPrav{T(bgr|CM4jH1wDp6z*_jnN{V ziN56m1T)PBp1%`OCFYcJJ+T09`=&=Y$Z#!0l0J2sIuGQtAr>dLfq5S;{XGJzNk@a^ zk^eHlC4Gch`t+ue3RviiOlhz81CD9z~d|n5;A>AGtkZMUQ#f>5M14f2d}2 z8<*LNZvYVob!p9lbmb!0jt)xn6O&JS)`}7v}j+csS3e;&Awj zoNyjnqLzC(QQ;!jvEYUTy73t_%16p)qMb?ihbU{y$i?=a7@JJoXS!#CE#y}PGMK~3 zeeqqmo7G-W_S97s2eed^erB2qeh4P25)RO1>MH7ai5cZJTEevogLNii=oKG)0(&f` z&hh8cO{of0;6KiNWZ6q$cO(1)9r{`}Q&%p*O0W7N--sw3Us;)EJgB)6iSOg(9p_mc zRw{M^qf|?rs2wGPtjVKTOMAfQ+ZNNkb$Ok0;Pe=dNc7__TPCzw^H$5J0l4D z%p(_0w(oLmn0)YDwrcFsc*8q)J@ORBRoZ54GkJpxSvnagp|8H5sxB|ZKirp%_mQt_ z81+*Y8{0Oy!r8Gmih48VuRPwoO$dDW@h53$C)duL4_(osryhwZSj%~KsZ?2n?b`Z* z#C8aMdZxYmCWSM{mFNw1ov*W}Dl=%GQpp90qgZ{(T}GOS8#>sbiEU;zYvA?=wbD5g+ahbd1#s`=| zV6&f#ofJC261~Ua6>0M$w?V1j##jh-lBJ2vQ%&z`7pO%frhLP-1l)wMs=3Q&?oth1 zefkPr@3Z(&OL@~|<0X-)?!AdK)ShtFJ;84G2(izo3cCuKc{>`+aDoziL z6gLTL(=RYeD7x^FYA%sPXswOKhVa4i(S4>h&mLvS##6-H?w8q!B<8Alk>nQEwUG)SFXK zETfcTwi=R3!ck|hSM`|-^N3NWLav&UTO{a9=&Tuz-Kq963;XaRFq#-1R18fi^Gb-; zVO>Q{Oe<^b0WA!hkBi9iJp3`kGwacXX2CVQ0xQn@Y2OhrM%e4)Ea7Y*Df$dY2BpbL zv$kX}*#`R1uNA(7lk_FAk~{~9Z*Si5xd(WKQdD&I?8Y^cK|9H&huMU1I(251D7(LL z+){kRc=ALmD;#SH#YJ+|7EJL6e~w!D7_IrK5Q=1DCulUcN(3j`+D_a|GP}?KYx}V+ zx_vLTYCLb0C?h;e<{K0`)-|-qfM16y{mnfX(GGs2H-;-lRMXyb@kiY^D;i1haxoEk zsQ7C_o2wv?;3KS_0w^G5#Qgf*>u)3bT<3kGQL-z#YiN9QH7<(oDdNlSdeHD zQJN-U*_wJM_cU}1YOH=m>DW~{%MAPxL;gLdU6S5xLb$gJt#4c2KYaEaL8ORWf=^(l z-2`8^J;&YG@vb9em%s~QpU)gG@24BQD69;*y&-#0NBkxumqg#YYomd2tyo0NGCr8N z5<5-E%utH?Ixt!(Y4x>zIz4R^9SABVMpLl(>oXnBNWs8w&xygh_e4*I$y_cVm?W-^ ze!9mPy^vTLRclXRGf$>g%Y{(#Bbm2xxr_Mrsvd7ci|X|`qGe5=54Zt2Tb)N zlykxE&re1ny+O7g#`6e_zyjVjRi5!DeTvSJ9^BJqQ*ovJ%?dkaQl!8r{F`@KuDEJB3#ho5 zmT$A&L=?}gF+!YACb=%Y@}8{SnhaGCHRmmuAh{LxAn0sg#R6P_^cJ-9)+-{YU@<^- zlYnH&^;mLVYE+tyjFj4gaAPCD4CnwP75BBXA`O*H(ULnYD!7K14C!kGL_&hak)udZ zkQN8)EAh&9I|TY~F{Z6mBv7sz3?<^o(#(NXGL898S3yZPTaT|CzZpZ~pK~*9Zcf2F zgwuG)jy^OTZD`|wf&bEdq4Vt$ir-+qM7BosXvu`>W1;iFN7yTvcpN_#at)Q4n+(Jh zYX1A-24l9H5jgY?wdEbW{(6U1=Kc?Utren80bP`K?J0+v@{-RDA7Y8yJYafdI<7-I z_XA!xeh#R4N7>rJ_?(VECa6iWhMJ$qdK0Ms27xG&$gLAy(|SO7_M|AH`fIY)1FGDp zlsLwIDshDU;*n`dF@8vV;B4~jRFpiHrJhQ6TcEm%OjWTi+KmE7+X{19 z>e!sg0--lE2(S0tK}zD&ov-{6bMUc%dNFIn{2^vjXWlt>+uxw#d)T6HNk6MjsfN~4 zDlq#Jjp_!wn}$wfs!f8NX3Rk#9)Q6-jD;D9D=1{$`3?o~caZjXU*U32^JkJ$ZzJ_% zQWNfcImxb!AV1DRBq`-qTV@g1#BT>TlvktYOBviCY!13Bv?_hGYDK}MINVi;pg)V- z($Bx1Tj`c?1I3pYg+i_cvFtcQ$SV9%%9QBPg&8R~Ig$eL+xKZY!C=;M1|r)$&9J2x z;l^a*Ph+isNl*%y1T4SviuK1Nco_spQ25v5-}7u?T9zHB5~{-+W*y3p{yjn{1obqf zYL`J^Uz8zZZN8c4Dxy~)k3Ws)E5eYi+V2C!+7Sm0uu{xq)S8o{9uszFTnE>lPhY=5 zdke-B8_*KwWOd%tQs_zf0x9+YixHp+Qi_V$aYVc$P-1mg?2|_{BUr$6WtLdIX2FaF zGmPRTrdIz)DNE)j*_>b9E}sp*(1-16}u za`dgT`KtA3;+e~9{KV48RT=CGPaVt;>-35}%nlFUMK0y7nOjoYds7&Ft~#>0$^ciZ zM}!J5Mz{&|&lyG^bnmh?YtR z*Z5EfDxkrI{QS#Iq752aiA~V)DRlC*2jlA|nCU!@CJwxO#<=j6ssn;muv zhBT9~35VtwsoSLf*(7vl&{u7d_K_CSBMbzr zzyjt&V5O#8VswCRK3AvVbS7U5(KvTPyUc0BhQ}wy0z3LjcdqH8`6F3!`)b3(mOSxL z>i4f8xor(#V+&#ph~ycJMcj#qeehjxt=~Na>dx#Tcq6Xi4?BnDeu5WBBxt603*BY& zZ#;o1kv?qpZjwK-E{8r4v1@g*lwb|8w@oR3BTDcbiGKs)a>Fpxfzh&b ziQANuJ_tNHdx;a*JeCo^RkGC$(TXS;jnxk=dx++D8|dmPP<0@ z$wh#ZYI%Rx$NKe-)BlJzB*bot0ras3I%`#HTMDthGtM_G6u-(tSroGp1Lz+W1Y`$@ zP`9NK^|IHbBrJ#AL3!X*g3{arc@)nuqa{=*2y+DvSwE=f*{>z1HX(>V zNE$>bbc}_yAu4OVn;8LG^naq5HZY zh{Hec==MD+kJhy6t=Nro&+V)RqORK&ssAxioc7-L#UQuPi#3V2pzfh6Ar400@iuV5 z@r>+{-yOZ%XQhsSfw%;|a4}XHaloW#uGluLKux0II9S1W4w=X9J=(k&8KU()m}b{H zFtoD$u5JlGfpX^&SXHlp$J~wk|DL^YVNh2w(oZ~1*W156YRmenU;g=mI zw({B(QVo2JpJ?pJqu9vijk$Cn+%PSw&b4c@uU6vw)DjGm2WJKt!X}uZ43XYlDIz%& z=~RlgZpU-tu_rD`5!t?289PTyQ zZgAEp=zMK>RW9^~gyc*x%vG;l+c-V?}Bm;^{RpgbEnt_B!FqvnvSy)T=R zGa!5GACDk{9801o@j>L8IbKp#!*Td5@vgFKI4w!5?R{>@^hd8ax{l=vQnd2RDHopo zwA+qb2cu4Rx9^Bu1WNYT`a(g}=&&vT`&Sqn-irxzX_j1=tIE#li`Hn=ht4KQXp zzZj`JO+wojs0dRA#(bXBOFn**o+7rPY{bM9m<+UBF{orv$#yF8)AiOWfuas5Fo`CJ zqa;jAZU^!bh8sjE7fsoPn%Tw11+vufr;NMm3*zC=;jB{R49e~BDeMR+H6MGzDlcA^ zKg>JEL~6_6iaR4i`tSfUhkgPaLXZ<@L7poRF?dw_DzodYG{Gp7#24<}=18PBT}aY` z{)rrt`g}930jr3^RBQNA$j!vzTh#Mo1VL`QCA&US?;<2`P+xy8b9D_Hz>FGHC2r$m zW>S9ywTSdQI5hh%7^e`#r#2906T?))i59O(V^Rpxw42rCAu-+I3y#Pg6cm#&AX%dy ze=hv0cUMxxxh1NQEIYXR{IBM&Bk8FK3NZI3z+M>r@A$ocd*e%x-?W;M0pv50p+MVt zugo<@_ij*6RZ;IPtT_sOf2Zv}-3R_1=sW37GgaF9Ti(>V z1L4ju8RzM%&(B}JpnHSVSs2LH#_&@`4Kg1)>*)^i`9-^JiPE@=4l$+?NbAP?44hX&XAZy&?}1;=8c(e0#-3bltVWg6h=k!(mCx=6DqOJ-I!-(g;*f~DDe={{JGtH7=UY|0F zNk(YyXsGi;g%hB8x)QLpp;;`~4rx>zr3?A|W$>xj>^D~%CyzRctVqtiIz7O3pc@r@JdGJiH@%XR_9vaYoV?J3K1cT%g1xOYqhXfSa`fg=bCLy% zWG74UTdouXiH$?H()lyx6QXt}AS)cOa~3IdBxddcQp;(H-O}btpXR-iwZ5E)di9Jf zfToEu%bOR11xf=Knw7JovRJJ#xZDgAvhBDF<8mDu+Q|!}Z?m_=Oy%Ur4p<71cD@0OGZW+{-1QT?U%_PJJ8T!0d2*a9I2;%|A z9LrfBU!r9qh4=3Mm3nR_~X-EyNc<;?m`?dKUNetCnS)}_-%QcWuOpw zAdZF`4c_24z&m{H9-LIL`=Hrx%{IjrNZ~U<7k6p{_wRkR84g>`eUBOQd3x5 zT^kISYq)gGw?IB8(lu1=$#Vl?iZdrx$H0%NxW)?MO$MhRHn8$F^&mzfMCu>|`{)FL z`ZgOt`z%W~^&kzMAuWy9=q~$ldBftH0}T#(K5e8;j~!x$JjyspJ1IISI?ON5OIPB$ z-5_|YUMb+QUsiv3R%Ys4tVYW+x$}dg;hw%EdoH%SXMp`)v?cxR4wic{X9pVBH>=`#`Kcj!}x4 zV!`6tj|*q?jZdG(CSevn(}4Ogij5 z-kp;sZs}7oNu0x+NHs~(aWaKGV@l~TBkmW&mPj==N!f|1e1SndS6(rPxsn7dz$q_{ zL0jSrihO)1t?gh8N zosMjR3n#YC()CVKv zos2TbnL&)lHEIiYdz|%6N^vAUvTs6?s|~kwI4uXjc9fim`KCqW3D838Xu{48p$2?I zOeEqQe1}JUZECrZSO_m=2<$^rB#B6?nrFXFpi8jw)NmoKV^*Utg6i8aEW|^QNJuW& z4cbXpHSp4|7~TW(%JP%q9W2~@&@5Y5%cXL#fMhV59AGj<3$Hhtfa>24DLk{7GZUtr z5ql**-e58|mbz%5Kk~|f!;g+Ze^b);F+5~^jdoq#m+s?Y*+=d5ruym%-Tnn8htCV; zDyyUrWydgDNM&bI{yp<_wd-q&?Ig+BN-^JjWo6Zu3%Eov^Ja>%eKqrk&7kUqeM8PL zs5D}lTe_Yx;e=K`TDya!-u%y$)r*Cr4bSfN*eZk$XT(Lv2Y}qj&_UaiTevxs_=HXjnOuBpmT> zBg|ty8?|1rD1~Ev^6=C$L9%+RkmBSQxlnj3j$XN?%QBstXdx+Vl!N$f2Ey`i3p@!f zzqhI3jC(TZUx|sP%yValu^nzEV96o%*CljO>I_YKa8wMfc3$_L()k4PB6kglP@IT#wBd*3RITYADL}g+hlzLYxFmCt=_XWS}=jg8`RgJefB57z(2n&&q>m ze&F(YMmoRZW7sQ;cZgd(!A9>7mQ2d#!-?$%G8IQ0`p1|*L&P$GnU0i0^(S;Rua4v8 z_7Qhmv#@+kjS-M|($c*ZOo?V2PgT;GKJyP1REABlZhPyf!kR(0UA7Bww~R<7_u6#t z{XNbiKT&tjne(&=UDZ+gNxf&@9EV|fblS^gxNhI-DH;|`1!YNlMcC{d7I{u_E~cJOalFEzDY|I?S3kHtbrN&}R3k zK(Ph_Ty}*L3Et6$cUW`0}**BY@44KtwEy(jW@pAt`>g> z&8>-TmJiDwc;H%Ae%k6$ndZlfKruu1GocgZrLN=sYI52}_I%d)~ z6z40!%W4I6ch$CE2m>Dl3iwWIbcm27QNY#J!}3hqc&~(F8K{^gIT6E&L!APVaQhj^ zjTJEO&?**pivl^xqfD(rpLu;`Tm1MV+Wtd4u>X6u5V{Yp%)xH$k410o{pGoKdtY0t@GgqFN zO=!hTcYoa^dEPKvPX4ukgUTmR#q840gRMMi%{3kvh9gt(wK;Fniqu9A%BMsq?U&B5DFXC8t8FBN1&UIwS#=S zF(6^Eyn8T}p)4)yRvs2rCXZ{L?N6{hgE_dkH_HA#L3a0$@UMoBw6RE9h|k_rx~%rB zUqeEPL|!Pbp|up2Q=8AcUxflck(fPNJYP1OM_4I(bc24a**Qnd-@;Bkb^2z8Xv?;3yZp*| zoy9KhLo=;8n0rPdQ}yAoS8eb zAtG5QYB|~z@Z(Fxdu`LmoO>f&(JzsO|v0V?1HYsfMvF!3| zka=}6U13(l@$9&=1!CLTCMS~L01CMs@Abl4^Q^YgVgizWaJa%{7t)2sVcZg0mh7>d z(tN=$5$r?s={yA@IX~2ot9`ZGjUgVlul$IU4N}{ zIFBzY3O0;g$BZ#X|VjuTPKyw*|IJ+&pQ` z(NpzU`o=D86kZ3E5#!3Ry$#0AW!6wZe)_xZ8EPidvJ0f+MQJZ6|ZJ$CEV6;Yt{OJnL`dewc1k>AGbkK9Gf5BbB-fg? zgC4#CPYX+9%LLHg@=c;_Vai_~#ksI~)5|9k(W()g6ylc(wP2uSeJ$QLATtq%e#zpT zp^6Y)bV+e_pqIE7#-hURQhfQvIZpMUzD8&-t$esrKJ}4`ZhT|woYi>rP~y~LRf`*2!6 z6prDzJ~1VOlYhYAuBHcu9m>k_F>;N3rpLg>pr;{EDkeQPHfPv~woj$?UTF=txmaZy z?RrVthxVcqUM;X*(=UNg4(L|0d250Xk)6GF&DKD@r6{aZo;(}dnO5@CP7pMmdsI)- zeYH*@#+|)L8x7)@GNBu0Npyyh6r z^~!3$x&w8N)T;|LVgnwx1jHmZn{b2V zO|8s#F0NZhvux?0W9NH5;qZ?P_JtPW86)4J>AS{0F1S0d}=L2`{F z_y;o;17%{j4I)znptnB z%No1W>o}H2%?~CFo~0j?pzWk?dV4ayb!s{#>Yj`ZJ!H)xn}*Z_gFHy~JDis)?9-P=z4iOQg{26~n?dTms7)+F}? zcXvnHHnnbNTzc!$t+V}=<2L<7l(84v1I3b;-)F*Q?cwLNlgg{zi#iS)*rQ5AFWe&~ zWHPPGy{8wEC9JSL?qNVY76=es`bA{vUr~L7f9G@mP}2MNF0Qhv6Sgs`r_k!qRbSXK zv16Qqq`rFM9!4zCrCeiVS~P2e{Pw^A8I?p?NSVR{XfwlQo*wj|Ctqz4X-j+dU7eGkC(2y`(P?FM?P4gKki3Msw#fM6paBq#VNc>T2@``L{DlnnA-_*i10Kre&@-H!Z7gzn9pRF61?^^ z8dJ5kEeVKb%Bly}6NLV}<0(*eZM$QTLcH#+@iWS^>$Of_@Mu1JwM!>&3evymgY6>C_)sK+n|A5G6(3RJz0k>(z2uLdzXeTw)e4*g!h} zn*UvIx-Ozx<3rCF#C`khSv`Y-b&R4gX>d5osr$6jlq^8vi!M$QGx05pJZoY#RGr*J zsJmOhfodAzYQxv-MoU?m_|h^aEwgEHt5h_HMkHwtE+OA03(7{hm1V?AlYAS7G$u5n zO+6?51qo@aQK5#l6pM`kD5OmI28g!J2Z{5kNlSuKl=Yj3QZ|bvVHU}FlM+{QV=<=) z+b|%Q!R)FE z@ycDMSKV2?*XfcAc5@IOrSI&3&aR$|oAD8WNA6O;p~q-J@ll{x`jP<*eEpIYOYnT zer_t=dYw6a0avjQtKN&#n&(KJ5Kr$RXPOp1@Fq#0Of zTXQkq4qQxKWR>x#d{Hyh?6Y)U07;Q$?BTl7mx2bSPY_juXub1 z%-$)NKXzE<%}q>RX25*oeMVjiz&r_z;BrQV-(u>!U>C*OisXNU*UftsrH6vAhTEm@ zoKA`?fZL1sdd!+G@*NNvZa>}37u^x8^T>VH0_6Bx{3@x5NAg&55{2jUE-w3zCJNJi z^IlU=+DJz-9K&4c@7iKj(zlj@%V}27?vYmxo*;!jZVXJMeDg;5T!4Y1rxNV-e$WAu zkk6^Xao8HC=w2hpLvM(!xwo|~$eG6jJj39zyQHf)E+NPJlfspUhzRv&_qr8+Z1`DA zz`EV=A)d=;2&J;eypNx~q&Ir_7e_^xXg(L9>k=X4pxZ3y#-ch$^TN}i>X&uwF%75c(9cjO6`E5 z16vbMYb!lEIM?jxn)^+Ld8*hmEXR4a8TSfqwBg1(@^8$p&#@?iyGd}uhWTVS`Mlpa zGc+kV)K7DJwd46aco@=?iASsx?sDjbHoDVU9=+^tk46|Fxxey1u)_}c1j z^(`5~PU%og1LdSBE5x4N&5&%Nh$sy0oANXwUcGa>@CCMqP`4W$ZPSaykK|giiuMIw zu#j)&VRKWP55I(5K1^cog|iXgaK1Z%wm%T;;M3X`-`TTWaI}NtIZj;CS)S%S(h}qq zRFQ#{m4Qk$7;1i*0PC^|X1@a1pcMq1aiRSCHq+mnfj^FS{oxWs0McCN-lK4>SDp#` z7=Duh)kXC;lr1g3dqogzBBDg6>et<<>m>KO^|bI5X{+eMd^-$2xfoP*&e$vdQc7J% zmFO~OHf7aqlIvg%P`Gu|3n;lKjtRd@;;x#$>_xU(HpZos7?ShZlQSU)bY?qyQM3cHh5twS6^bF8NBKDnJgXHa)? zBYv=GjsZuYC2QFS+jc#uCsaEPEzLSJCL=}SIk9!*2Eo(V*SAUqKw#?um$mUIbqQQb zF1Nn(y?7;gP#@ws$W76>TuGcG=U_f6q2uJq?j#mv7g;llvqu{Yk~Mo>id)jMD7;T> zSB$1!g)QpIf*f}IgmV;!B+3u(ifW%xrD=`RKt*PDC?M5KI)DO`VXw(7X-OMLd3iVU z0CihUN(eNrY;m?vwK{55MU`p1;JDF=6ITN$+!q8W#`iIsN8;W7H?`htf%RS9Lh+KQ z_p_4?qO4#*`t+8l-N|kAKDcOt zoHsqz_oO&n?@4^Mr*4YrkDX44BeS*0zaA1j@*c}{$;jUxRXx1rq7z^*NX6d`DcQ}L z6*cN7e%`2#_J4z8=^GM6>%*i>>X^_0u9qn%0JTUo)c0zIz|7a`%_UnB)-I1cc+ z0}jAK0}jBl|6-2VT759oxBnf%-;7vs>7Mr}0h3^$0`5FAy}2h{ps5%RJA|^~6uCqg zxBMK5bQVD{Aduh1lu4)`Up*&( zCJQ>nafDb#MuhSZ5>YmD@|TcrNv~Q%!tca;tyy8Iy2vu2CeA+AsV^q*Wohg%69XYq zP0ppEDEYJ9>Se&X(v=U#ibxg()m=83pLc*|otbG;`CYZ z*YgsakGO$E$E_$|3bns7`m9ARe%myU3$DE;RoQ<6hR8e;%`pxO1{GXb$cCZl9lVnJ$(c` z``G?|PhXaz`>)rb7jm2#v7=(W?@ zjUhrNndRFMQ}%^^(-nmD&J>}9w@)>l;mhRr@$}|4ueOd?U9ZfO-oi%^n4{#V`i}#f zqh<@f^%~(MnS?Z0xsQI|Fghrby<&{FA+e4a>c(yxFL!Pi#?DW!!YI{OmR{xEC7T7k zS_g*9VWI}d0IvIXx*d5<7$5Vs=2^=ews4qZGmAVyC^9e;wxJ%BmB(F5*&!yyABCtLVGL@`qW>X9K zpv=W~+EszGef=am3LG+#yIq5oLXMnZ_dxSLQ_&bwjC^0e8qN@v!p?7mg02H<9`uaJ zy0GKA&YQV2CxynI3T&J*m!rf4@J*eo235*!cB1zEMQZ%h5>GBF;8r37K0h?@|E*0A zIHUg0y7zm(rFKvJS48W7RJwl!i~<6X2Zw+Fbm9ekev0M;#MS=Y5P(kq^(#q11zsvq zDIppe@xOMnsOIK+5BTFB=cWLalK#{3eE>&7fd11>l2=MpNKjsZT2kmG!jCQh`~Fu0 z9P0ab`$3!r`1yz8>_7DYsO|h$kIsMh__s*^KXv?Z1O8|~sEz?Y{+GDzze^GPjk$E$ zXbA-1gd77#=tn)YKU=;JE?}De0)WrT%H9s3`fn|%YibEdyZov3|MJ>QWS>290eCZj z58i<*>dC9=kz?s$sP_9kK1p>nV3qvbleExyq56|o+oQsb{ZVmuu1n~JG z0sUvo_i4fSM>xRs8rvG$*+~GZof}&ISxn(2JU*K{L<3+b{bBw{68H&Uiup@;fWWl5 zgB?IWMab0LkXK(Hz#yq>scZbd2%=B?DO~^q9tarlzZysN+g}n0+v);JhbjUT8AYrt z3?;0r%p9zLJv1r$%q&HKF@;3~0wVwO!U5m;J`Mm|`Nc^80sZd+Wj}21*SPoF82hCF zoK?Vw;4ioafdAkZxT1er-LLVi-*0`@2Ur&*!b?0U>R;no+S%)xoBuBxRw$?weN-u~tKE}8xb@7Gs%(aC;e1-LIlSfXDK(faFW)mnHdrLc3`F z6ZBsT^u0uVS&il=>YVX^*5`k!P4g1)2LQmz{?&dgf`7JrA4ZeE0sikL`k!Eb6r=g0 z{aCy_0I>fxSAXQYz3lw5G|ivg^L@(x-uch!AphH+d;E4`175`R0#b^)Zp>EM1Ks=zx6_261>!7 z{7F#a{Tl@Tpw9S`>7_i|PbScS-(dPJv9_0-FBP_aa@Gg^2IoKNZM~#=sW$SH3MJ|{ zsQy8F43lX7hYx<{v^Q9`2QsMzeen3cGpiTgzVp- z`aj3&Wv0(he1qKI!2jpGpO-i0Wpcz%vdn`2o9x&3;^nsZPt3czbL-0C3_3~ zRZ#mYf6f1oqJoH`jHHCB8l!^by~4z}yc`4LEP@;Z?bO6{g9`Hk+s@(L1jC5Tq{1Yf z4E;CQvrx0-gF+peRxFC*gF=&$zNYjO?K|gN=WqXMz`tYs@0o%B{dRD+{C_6(f9t^g zhmNJQv6-#;f2)f2uc{u-#*U8W&i{|ewYN^n_1~cv|1J!}zc&$eaBy{T{cEpa46s*q zHFkD2cV;xTHFj}{*3kBt*FgS4A5SI|$F%$gB@It9FlC}D3y`sbZG{2P6gGwC$U`6O zb_cId9AhQl#A<&=x>-xDD%=Ppt$;y71@Lwsl{x943#T@8*?cbR<~d`@@}4V${+r$jICUIOzgZJy_9I zu*eA(F)$~J07zX%tmQN}1^wj+RM|9bbwhQA=xrPE*{vB_P!pPYT5{Or^m*;Qz#@Bl zRywCG_RDyM6bf~=xn}FtiFAw|rrUxa1+z^H`j6e|GwKDuq}P)z&@J>MEhsVBvnF|O zOEm)dADU1wi8~mX(j_8`DwMT_OUAnjbWYer;P*^Uku_qMu3}qJU zTAkza-K9aj&wcsGuhQ>RQoD?gz~L8RwCHOZDzhBD$az*$TQ3!uygnx_rsXG`#_x5t zn*lb(%JI3%G^MpYp-Y(KI4@_!&kBRa3q z|Fzn&3R%ZsoMNEn4pN3-BSw2S_{IB8RzRv(eQ1X zyBQZHJ<(~PfUZ~EoI!Aj`9k<+Cy z2DtI<+9sXQu!6&-Sk4SW3oz}?Q~mFvy(urUy<)x!KQ>#7yIPC)(ORhKl7k)4eSy~} z7#H3KG<|lt68$tk^`=yjev%^usOfpQ#+Tqyx|b#dVA(>fPlGuS@9ydo z!Cs#hse9nUETfGX-7lg;F>9)+ml@M8OO^q|W~NiysX2N|2dH>qj%NM`=*d3GvES_# zyLEHw&1Fx<-dYxCQbk_wk^CI?W44%Q9!!9aJKZW-bGVhK?N;q`+Cgc*WqyXcxZ%U5QXKu!Xn)u_dxeQ z;uw9Vysk!3OFzUmVoe)qt3ifPin0h25TU zrG*03L~0|aaBg7^YPEW^Yq3>mSNQgk-o^CEH?wXZ^QiPiuH}jGk;75PUMNquJjm$3 zLcXN*uDRf$Jukqg3;046b;3s8zkxa_6yAlG{+7{81O3w96i_A$KcJhD&+oz1<>?lun#C3+X0q zO4JxN{qZ!e#FCl@e_3G?0I^$CX6e$cy7$BL#4<`AA)Lw+k`^15pmb-447~5lkSMZ` z>Ce|adKhb-F%yy!vx>yQbXFgHyl(an=x^zi(!-~|k;G1=E(e@JgqbAF{;nv`3i)oi zDeT*Q+Mp{+NkURoabYb9@#Bi5FMQnBFEU?H{~9c;g3K%m{+^hNe}(MdpPb?j9`?2l z#%AO!|2QxGq7-2Jn2|%atvGb(+?j&lmP509i5y87`9*BSY++<%%DXb)kaqG0(4Eft zj|2!Od~2TfVTi^0dazAIeVe&b#{J4DjN6;4W;M{yWj7#+oLhJyqeRaO;>?%mX>Ec{Mp~;`bo}p;`)@5dA8fNQ38FyMf;wUPOdZS{U*8SN6xa z-kq3>*Zos!2`FMA7qjhw-`^3ci%c91Lh`;h{qX1r;x1}eW2hYaE*3lTk4GwenoxQ1kHt1Lw!*N8Z%DdZSGg5~Bw}+L!1#d$u+S=Bzo7gi zqGsBV29i)Jw(vix>De)H&PC; z-t2OX_ak#~eSJ?Xq=q9A#0oaP*dO7*MqV;dJv|aUG00UX=cIhdaet|YEIhv6AUuyM zH1h7fK9-AV)k8sr#POIhl+?Z^r?wI^GE)ZI=H!WR<|UI(3_YUaD#TYV$Fxd015^mT zpy&#-IK>ahfBlJm-J(n(A%cKV;)8&Y{P!E|AHPtRHk=XqvYUX?+9po4B$0-6t74UUef${01V{QLEE8gzw* z5nFnvJ|T4dlRiW9;Ed_yB{R@)fC=zo4hCtD?TPW*WJmMXYxN_&@YQYg zBQ$XRHa&EE;YJrS{bn7q?}Y&DH*h;){5MmE(9A6aSU|W?{3Ox%5fHLFScv7O-txuRbPG1KQtI`Oay=IcEG=+hPhlnYC;`wSHeo|XGio0aTS6&W($E$ z?N&?TK*l8;Y^-xPl-WVZwrfdiQv10KdsAb9u-*1co*0-Z(h#H)k{Vc5CT!708cs%sExvPC+7-^UY~jTfFq=cj z!Dmy<+NtKp&}}$}rD{l?%MwHdpE(cPCd;-QFPk1`E5EVNY2i6E`;^aBlx4}h*l42z zpY#2cYzC1l6EDrOY*ccb%kP;k8LHE3tP>l3iK?XZ%FI<3666yPw1rM%>eCgnv^JS_ zK7c~;g7yXt9fz@(49}Dj7VO%+P!eEm& z;z8UXs%NsQ%@2S5nve)@;yT^61BpVlc}=+i6{ZZ9r7<({yUYqe==9*Z+HguP3`sA& z{`inI4G)eLieUQ*pH9M@)u7yVnWTQva;|xq&-B<>MoP(|xP(HqeCk1&h>DHNLT>Zi zQ$uH%s6GoPAi0~)sC;`;ngsk+StYL9NFzhFEoT&Hzfma1f|tEnL0 zMWdX4(@Y*?*tM2@H<#^_l}BC&;PYJl%~E#veQ61{wG6!~nyop<^e)scV5#VkGjYc2 z$u)AW-NmMm%T7WschOnQ!Hbbw&?`oMZrJ&%dVlN3VNra1d0TKfbOz{dHfrCmJ2Jj= zS#Gr}JQcVD?S9X!u|oQ7LZ+qcq{$40 ziG5=X^+WqeqxU00YuftU7o;db=K+Tq!y^daCZgQ)O=M} zK>j*<3oxs=Rcr&W2h%w?0Cn3);~vqG>JO_tTOzuom^g&^vzlEjkx>Sv!@NNX%_C!v zaMpB>%yVb}&ND9b*O>?HxQ$5-%@xMGe4XKjWh7X>CYoRI2^JIwi&3Q5UM)?G^k8;8 zmY$u;(KjZx>vb3fe2zgD7V;T2_|1KZQW$Yq%y5Ioxmna9#xktcgVitv7Sb3SlLd6D zfmBM9Vs4rt1s0M}c_&%iP5O{Dnyp|g1(cLYz^qLqTfN6`+o}59Zlu%~oR3Q3?{Bnr zkx+wTpeag^G12fb_%SghFcl|p2~<)Av?Agumf@v7y-)ecVs`US=q~=QG%(_RTsqQi z%B&JdbOBOmoywgDW|DKR5>l$1^FPhxsBrja<&}*pfvE|5dQ7j-wV|ur%QUCRCzBR3q*X`05O3U@?#$<>@e+Zh&Z&`KfuM!0XL& zI$gc@ZpM4o>d&5)mg7+-Mmp98K^b*28(|Ew8kW}XEV7k^vnX-$onm9OtaO@NU9a|as7iA%5Wrw9*%UtJYacltplA5}gx^YQM` zVkn`TIw~avq)mIQO0F0xg)w$c)=8~6Jl|gdqnO6<5XD)&e7z7ypd3HOIR+ss0ikSVrWar?548HFQ*+hC)NPCq*;cG#B$7 z!n?{e9`&Nh-y}v=nK&PR>PFdut*q&i81Id`Z<0vXUPEbbJ|<~_D!)DJMqSF~ly$tN zygoa)um~xdYT<7%%m!K8+V(&%83{758b0}`b&=`))Tuv_)OL6pf=XOdFk&Mfx9y{! z6nL>V?t=#eFfM$GgGT8DgbGRCF@0ZcWaNs_#yl+6&sK~(JFwJmN-aHX{#Xkpmg;!} zgNyYYrtZdLzW1tN#QZAh!z5>h|At3m+ryJ-DFl%V>w?cmVTxt^DsCi1ZwPaCe*D{) z?#AZV6Debz{*D#C2>44Czy^yT3y92AYDcIXtZrK{L-XacVl$4i=X2|K=Fy5vAzhk{ zu3qG=qSb_YYh^HirWf~n!_Hn;TwV8FU9H8+=BO)XVFV`nt)b>5yACVr!b98QlLOBDY=^KS<*m9@_h3;64VhBQzb_QI)gbM zSDto2i*iFrvxSmAIrePB3i`Ib>LdM8wXq8(R{-)P6DjUi{2;?}9S7l7bND4w%L2!; zUh~sJ(?Yp}o!q6)2CwG*mgUUWlZ;xJZo`U`tiqa)H4j>QVC_dE7ha0)nP5mWGB268 zn~MVG<#fP#R%F=Ic@(&Va4dMk$ysM$^Avr1&hS!p=-7F>UMzd(M^N9Ijb|364}qcj zcIIh7suk$fQE3?Z^W4XKIPh~|+3(@{8*dSo&+Kr(J4^VtC{z*_{2}ld<`+mDE2)S| zQ}G#Q0@ffZCw!%ZGc@kNoMIdQ?1db%N1O0{IPPesUHI;(h8I}ETudk5ESK#boZgln z(0kvE`&6z1xH!s&={%wQe;{^&5e@N0s7IqR?L*x%iXM_czI5R1aU?!bA7)#c4UN2u zc_LZU+@elD5iZ=4*X&8%7~mA;SA$SJ-8q^tL6y)d150iM)!-ry@TI<=cnS#$kJAS# zq%eK**T*Wi2OlJ#w+d_}4=VN^A%1O+{?`BK00wkm)g8;u?vM;RR+F1G?}({ENT3i= zQsjJkp-dmJ&3-jMNo)wrz0!g*1z!V7D(StmL(A}gr^H-CZ~G9u?*Uhcx|x7rb`v^X z9~QGx;wdF4VcxCmEBp$F#sms@MR?CF67)rlpMxvwhEZLgp2?wQq|ci#rLtrYRV~iR zN?UrkDDTu114&d~Utjcyh#tXE_1x%!dY?G>qb81pWWH)Ku@Kxbnq0=zL#x@sCB(gs zm}COI(!{6-XO5li0>1n}Wz?w7AT-Sp+=NQ1aV@fM$`PGZjs*L+H^EW&s!XafStI!S zzgdntht=*p#R*o8-ZiSb5zf6z?TZr$^BtmIfGAGK;cdg=EyEG)fc*E<*T=#a?l=R5 zv#J;6C(umoSfc)W*EODW4z6czg3tXIm?x8{+8i^b;$|w~k)KLhJQnNW7kWXcR^sol z1GYOp?)a+}9Dg*nJ4fy*_riThdkbHO37^csfZRGN;CvQOtRacu6uoh^gg%_oEZKDd z?X_k67s$`|Q&huidfEonytrq!wOg07H&z@`&BU6D114p!rtT2|iukF}>k?71-3Hk< zs6yvmsMRO%KBQ44X4_FEYW~$yx@Y9tKrQ|rC1%W$6w}-9!2%4Zk%NycTzCB=nb)r6*92_Dg+c0;a%l1 zsJ$X)iyYR2iSh|%pIzYV1OUWER&np{w1+RXb~ zMUMRymjAw*{M)UtbT)T!kq5ZAn%n=gq3ssk3mYViE^$paZ;c^7{vXDJ`)q<}QKd2?{r9`X3mpZ{AW^UaRe2^wWxIZ$tuyKzp#!X-hXkHwfD zj@2tA--vFi3o_6B?|I%uwD~emwn0a z+?2Lc1xs(`H{Xu>IHXpz=@-84uw%dNV;{|c&ub|nFz(=W-t4|MME(dE4tZQi?0CE|4_?O_dyZj1)r zBcqB8I^Lt*#)ABdw#yq{OtNgf240Jvjm8^zdSf40 z;H)cp*rj>WhGSy|RC5A@mwnmQ`y4{O*SJ&S@UFbvLWyPdh)QnM=(+m3p;0&$^ysbZ zJt!ZkNQ%3hOY*sF2_~-*`aP|3Jq7_<18PX*MEUH*)t{eIx%#ibC|d&^L5FwoBN}Oe z?!)9RS@Zz%X1mqpHgym75{_BM4g)k1!L{$r4(2kL<#Oh$Ei7koqoccI3(MN1+6cDJ zp=xQhmilz1?+ZjkX%kfn4{_6K_D{wb~rdbkh!!k!Z@cE z^&jz55*QtsuNSlGPrU=R?}{*_8?4L7(+?>?(^3Ss)f!ou&{6<9QgH>#2$?-HfmDPN z6oIJ$lRbDZb)h-fFEm^1-v?Slb8udG{7GhbaGD_JJ8a9f{6{TqQN;m@$&)t81k77A z?{{)61za|e2GEq2)-OqcEjP`fhIlUs_Es-dfgX-3{S08g`w=wGj2{?`k^GD8d$}6Z zBT0T1lNw~fuwjO5BurKM593NGYGWAK%UCYiq{$p^GoYz^Uq0$YQ$j5CBXyog8(p_E znTC+$D`*^PFNc3Ih3b!2Lu|OOH6@46D)bbvaZHy%-9=$cz}V^|VPBpmPB6Ivzlu&c zPq6s7(2c4=1M;xlr}bkSmo9P`DAF>?Y*K%VPsY`cVZ{mN&0I=jagJ?GA!I;R)i&@{ z0Gl^%TLf_N`)`WKs?zlWolWvEM_?{vVyo(!taG$`FH2bqB`(o50pA=W34kl-qI62lt z1~4LG_j%sR2tBFteI{&mOTRVU7AH>>-4ZCD_p6;-J<=qrod`YFBwJz(Siu(`S}&}1 z6&OVJS@(O!=HKr-Xyzuhi;swJYK*ums~y1ePdX#~*04=b9)UqHHg;*XJOxnS6XK#j zG|O$>^2eW2ZVczP8#$C`EpcWwPFX4^}$omn{;P(fL z>J~%-r5}*D3$Kii z34r@JmMW2XEa~UV{bYP=F;Y5=9miJ+Jw6tjkR+cUD5+5TuKI`mSnEaYE2=usXNBs9 zac}V13%|q&Yg6**?H9D620qj62dM+&&1&a{NjF}JqmIP1I1RGppZ|oIfR}l1>itC% zl>ed${{_}8^}m2^br*AIX$L!Vc?Sm@H^=|LnpJg`a7EC+B;)j#9#tx-o0_e4!F5-4 zF4gA;#>*qrpow9W%tBzQ89U6hZ9g=-$gQpCh6Nv_I0X7t=th2ajJ8dBbh{i)Ok4{I z`Gacpl?N$LjC$tp&}7Sm(?A;;Nb0>rAWPN~@3sZ~0_j5bR+dz;Qs|R|k%LdreS3Nn zp*36^t#&ASm=jT)PIjNqaSe4mTjAzlAFr*@nQ~F+Xdh$VjHWZMKaI+s#FF#zjx)BJ zufxkW_JQcPcHa9PviuAu$lhwPR{R{7CzMUi49=MaOA%ElpK;A)6Sgsl7lw)D$8FwE zi(O6g;m*86kcJQ{KIT-Rv&cbv_SY4 zpm1|lSL*o_1LGOlBK0KuU2?vWcEcQ6f4;&K=&?|f`~X+s8H)se?|~2HcJo{M?Ity) zE9U!EKGz2^NgB6Ud;?GcV*1xC^1RYIp&0fr;DrqWLi_Kts()-#&3|wz{wFQsKfnnsC||T?oIgUp z{O(?Df7&vW!i#_~*@naguLLjDAz+)~*_xV2iz2?(N|0y8DMneikrT*dG`mu6vdK`% z=&nX5{F-V!Reau}+w_V3)4?}h@A@O)6GCY7eXC{p-5~p8x{cH=hNR;Sb{*XloSZ_%0ZKYG=w<|!vy?spR4!6mF!sXMUB5S9o_lh^g0!=2m55hGR; z-&*BZ*&;YSo474=SAM!WzrvjmNtq17L`kxbrZ8RN419e=5CiQ-bP1j-C#@@-&5*(8 zRQdU~+e(teUf}I3tu%PB1@Tr{r=?@0KOi3+Dy8}+y#bvgeY(FdN!!`Kb>-nM;7u=6 z;0yBwOJ6OdWn0gnuM{0`*fd=C(f8ASnH5aNYJjpbY1apTAY$-%)uDi$%2)lpH=#)=HH z<9JaYwPKil@QbfGOWvJ?cN6RPBr`f+jBC|-dO|W@x_Vv~)bmY(U(!cs6cnhe0z31O z>yTtL4@KJ*ac85u9|=LFST22~!lb>n7IeHs)_(P_gU}|8G>{D_fJX)8BJ;Se? z67QTTlTzZykb^4!{xF!=C}VeFd@n!9E)JAK4|vWVwWop5vSWcD<;2!88v-lS&ve7C zuYRH^85#hGKX(Mrk};f$j_V&`Nb}MZy1mmfz(e`nnI4Vpq(R}26pZx?fq%^|(n~>* z5a5OFtFJJfrZmgjyHbj1`9||Yp?~`p2?4NCwu_!!*4w8K`&G7U_|np&g7oY*-i;sI zu)~kYH;FddS{7Ri#Z5)U&X3h1$Mj{{yk1Q6bh4!7!)r&rqO6K~{afz@bis?*a56i& zxi#(Ss6tkU5hDQJ0{4sKfM*ah0f$>WvuRL zunQ-eOqa3&(rv4kiQ(N4`FO6w+nko_HggKFWx@5aYr}<~8wuEbD(Icvyl~9QL^MBt zSvD)*C#{2}!Z55k1ukV$kcJLtW2d~%z$t0qMe(%2qG`iF9K_Gsae7OO%Tf8E>ooch ztAw01`WVv6?*14e1w%Wovtj7jz_)4bGAqqo zvTD|B4)Ls8x7-yr6%tYp)A7|A)x{WcI&|&DTQR&2ir(KGR7~_RhNOft)wS<+vQ*|sf;d>s zEfl&B^*ZJp$|N`w**cXOza8(ARhJT{O3np#OlfxP9Nnle4Sto)Fv{w6ifKIN^f1qO*m8+MOgA1^Du!=(@MAh8)@wU8t=Ymh!iuT_lzfm za~xEazL-0xwy9$48!+?^lBwMV{!Gx)N>}CDi?Jwax^YX@_bxl*+4itP;DrTswv~n{ zZ0P>@EB({J9ZJ(^|ptn4ks^Z2UI&87d~J_^z0&vD2yb%*H^AE!w= zm&FiH*c%vvm{v&i3S>_hacFH${|(2+q!`X~zn4$aJDAry>=n|{C7le(0a)nyV{kAD zlud4-6X>1@-XZd`3SKKHm*XNn_zCyKHmf*`C_O509$iy$Wj`Sm3y?nWLCDy>MUx1x zl-sz7^{m(&NUk*%_0(G^>wLDnXW90FzNi$Tu6* z<+{ePBD`%IByu977rI^x;gO5M)Tfa-l*A2mU-#IL2?+NXK-?np<&2rlF;5kaGGrx2 zy8Xrz`kHtTVlSSlC=nlV4_oCsbwyVHG4@Adb6RWzd|Otr!LU=% zEjM5sZ#Ib4#jF(l!)8Na%$5VK#tzS>=05GpV?&o* z3goH1co0YR=)98rPJ~PuHvkA59KUi#i(Mq_$rApn1o&n1mUuZfFLjx@3;h`0^|S##QiTP8rD`r8P+#D@gvDJh>amMIl065I)PxT6Hg(lJ?X7*|XF2Le zv36p8dWHCo)f#C&(|@i1RAag->5ch8TY!LJ3(+KBmLxyMA%8*X%_ARR*!$AL66nF= z=D}uH)D)dKGZ5AG)8N-;Il*-QJ&d8u30&$_Q0n1B58S0ykyDAyGa+BZ>FkiOHm1*& zNOVH;#>Hg5p?3f(7#q*dL74;$4!t?a#6cfy#}9H3IFGiCmevir5@zXQj6~)@zYrWZ zRl*e66rjwksx-)Flr|Kzd#Bg>We+a&E{h7bKSae9P~ z(g|zuXmZ zD?R*MlmoZ##+0c|cJ(O{*h(JtRdA#lChYhfsx25(Z`@AK?Q-S8_PQqk z>|Z@Ki1=wL1_c6giS%E4YVYD|Y-{^ZzFwB*yN8-4#+TxeQ`jhks7|SBu7X|g=!_XL z`mY=0^chZfXm%2DYHJ4z#soO7=NONxn^K3WX={dV>$CTWSZe@<81-8DVtJEw#Uhd3 zxZx+($6%4a&y_rD8a&E`4$pD6-_zZJ%LEE*1|!9uOm!kYXW< zOBXZAowsX-&$5C`xgWkC43GcnY)UQt2Qkib4!!8Mh-Q!_M%5{EC=Gim@_;0+lP%O^ zG~Q$QmatQk{Mu&l{q~#kOD;T-{b1P5u7)o-QPPnqi?7~5?7%IIFKdj{;3~Hu#iS|j z)Zoo2wjf%+rRj?vzWz(6JU`=7H}WxLF*|?WE)ci7aK?SCmd}pMW<{#1Z!_7BmVP{w zSrG>?t}yNyCR%ZFP?;}e8_ zRy67~&u11TN4UlopWGj6IokS{vB!v!n~TJYD6k?~XQkpiPMUGLG2j;lh>Eb5bLTkX zx>CZlXdoJsiPx=E48a4Fkla>8dZYB%^;Xkd(BZK$z3J&@({A`aspC6$qnK`BWL;*O z-nRF{XRS`3Y&b+}G&|pE1K-Ll_NpT!%4@7~l=-TtYRW0JJ!s2C-_UsRBQ=v@VQ+4> z*6jF0;R@5XLHO^&PFyaMDvyo?-lAD(@H61l-No#t@at@Le9xOgTFqkc%07KL^&iss z!S2Ghm)u#26D(e1Q7E;L`rxOy-N{kJ zTgfw}az9=9Su?NEMMtpRlYwDxUAUr8F+P=+9pkX4%iA4&&D<|=B|~s*-U+q6cq`y* zIE+;2rD7&D5X;VAv=5rC5&nP$E9Z3HKTqIFCEV%V;b)Y|dY?8ySn|FD?s3IO>VZ&&f)idp_7AGnwVd1Z znBUOBA}~wogNpEWTt^1Rm-(YLftB=SU|#o&pT7vTr`bQo;=ZqJHIj2MP{JuXQPV7% z0k$5Ha6##aGly<}u>d&d{Hkpu?ZQeL_*M%A8IaXq2SQl35yW9zs4^CZheVgHF`%r= zs(Z|N!gU5gj-B^5{*sF>;~fauKVTq-Ml2>t>E0xl9wywD&nVYZfs1F9Lq}(clpNLz z4O(gm_i}!k`wUoKr|H#j#@XOXQ<#eDGJ=eRJjhOUtiKOG;hym-1Hu)1JYj+Kl*To<8( za1Kf4_Y@Cy>eoC59HZ4o&xY@!G(2p^=wTCV>?rQE`Upo^pbhWdM$WP4HFdDy$HiZ~ zRUJFWTII{J$GLVWR?miDjowFk<1#foE3}C2AKTNFku+BhLUuT>?PATB?WVLzEYyu+ zM*x((pGdotzLJ{}R=OD*jUexKi`mb1MaN0Hr(Wk8-Uj0zA;^1w2rmxLI$qq68D>^$ zj@)~T1l@K|~@YJ6+@1vlWl zHg5g%F{@fW5K!u>4LX8W;ua(t6YCCO_oNu}IIvI6>Fo@MilYuwUR?9p)rKNzDmTAN zzN2d>=Za&?Z!rJFV*;mJ&-sBV80%<-HN1;ciLb*Jk^p?u<~T25%7jjFnorfr={+wm zzl5Q6O>tsN8q*?>uSU6#xG}FpAVEQ_++@}G$?;S7owlK~@trhc#C)TeIYj^N(R&a} zypm~c=fIs;M!YQrL}5{xl=tUU-Tfc0ZfhQuA-u5(*w5RXg!2kChQRd$Fa8xQ0CQIU zC`cZ*!!|O!*y1k1J^m8IIi|Sl3R}gm@CC&;4840^9_bb9%&IZTRk#=^H0w%`5pMDCUef5 zYt-KpWp2ijh+FM`!zZ35>+7eLN;s3*P!bp%-oSx34fdTZ14Tsf2v7ZrP+mitUx$rS zW(sOi^CFxe$g3$x45snQwPV5wpf}>5OB?}&Gh<~i(mU&ss#7;utaLZ!|KaTHniGO9 zVC9OTzuMKz)afey_{93x5S*Hfp$+r*W>O^$2ng|ik!<`U1pkxm3*)PH*d#>7md1y} zs7u^a8zW8bvl92iN;*hfOc-=P7{lJeJ|3=NfX{(XRXr;*W3j845SKG&%N zuBqCtDWj*>KooINK1 zFPCsCWr!-8G}G)X*QM~34R*k zmRmDGF*QE?jCeNfc?k{w<}@29e}W|qKJ1K|AX!htt2|B`nL=HkC4?1bEaHtGBg}V( zl(A`6z*tck_F$4;kz-TNF%7?=20iqQo&ohf@S{_!TTXnVh}FaW2jxAh(DI0f*SDG- z7tqf5X@p#l?7pUNI(BGi>n_phw=lDm>2OgHx-{`T>KP2YH9Gm5ma zb{>7>`tZ>0d5K$j|s2!{^sFWQo3+xDb~#=9-jp(1ydI3_&RXGB~rxWSMgDCGQG)oNoc#>)td zqE|X->35U?_M6{^lB4l(HSN|`TC2U*-`1jSQeiXPtvVXdN-?i1?d#;pw%RfQuKJ|e zjg75M+Q4F0p@8I3ECpBhGs^kK;^0;7O@MV=sX^EJLVJf>L;GmO z3}EbTcoom7QbI(N8ad!z(!6$!MzKaajSRb0c+ZDQ($kFT&&?GvXmu7+V3^_(VJx1z zP-1kW_AB&_A;cxm*g`$ z#Pl@Cg{siF0ST2-w)zJkzi@X)5i@)Z;7M5ewX+xcY36IaE0#flASPY2WmF8St0am{ zV|P|j9wqcMi%r-TaU>(l*=HxnrN?&qAyzimA@wtf;#^%{$G7i4nXu=Pp2#r@O~wi)zB>@25A*|axl zEclXBlXx1LP3x0yrSx@s-kVW4qlF+idF+{M7RG54CgA&soDU-3SfHW@-6_ z+*;{n_SixmGCeZjHmEE!IF}!#aswth_{zm5Qhj0z-@I}pR?cu=P)HJUBClC;U+9;$#@xia30o$% zDw%BgOl>%vRenxL#|M$s^9X}diJ9q7wI1-0n2#6>@q}rK@ng(4M68(t52H_Jc{f&M9NPxRr->vj-88hoI?pvpn}llcv_r0`;uN>wuE{ z&TOx_i4==o;)>V4vCqG)A!mW>dI^Ql8BmhOy$6^>OaUAnI3>mN!Zr#qo4A>BegYj` zNG_)2Nvy2Cqxs1SF9A5HHhL7sai#Umw%K@+riaF+q)7&MUJvA&;$`(w)+B@c6!kX@ zzuY;LGu6|Q2eu^06PzSLspV2v4E?IPf`?Su_g8CX!75l)PCvyWKi4YRoRThB!-BhG zubQ#<7oCvj@z`^y&mPhSlbMf0<;0D z?5&!I?nV-jh-j1g~&R(YL@c=KB_gNup$8abPzXZN`N|WLqxlN)ZJ+#k4UWq#WqvVD z^|j+8f5uxTJtgcUscKTqKcr?5g-Ih3nmbvWvvEk})u-O}h$=-p4WE^qq7Z|rLas0$ zh0j&lhm@Rk(6ZF0_6^>Rd?Ni-#u1y`;$9tS;~!ph8T7fLlYE{P=XtWfV0Ql z#z{_;A%p|8+LhbZT0D_1!b}}MBx9`R9uM|+*`4l3^O(>Mk%@ha>VDY=nZMMb2TnJ= zGlQ+#+pmE98zuFxwAQcVkH1M887y;Bz&EJ7chIQQe!pgWX>(2ruI(emhz@_6t@k8Z zqFEyJFX2PO`$gJ6p$=ku{7!vR#u+$qo|1r;orjtp9FP^o2`2_vV;W&OT)acRXLN^m zY8a;geAxg!nbVu|uS8>@Gvf@JoL&GP`2v4s$Y^5vE32&l;2)`S%e#AnFI-YY7_>d#IKJI!oL6e z_7W3e=-0iz{bmuB*HP+D{Nb;rn+RyimTFqNV9Bzpa0?l`pWmR0yQOu&9c0S*1EPr1 zdoHMYlr>BycjTm%WeVuFd|QF8I{NPT&`fm=dITj&3(M^q ze2J{_2zB;wDME%}SzVWSW6)>1QtiX)Iiy^p2eT}Ii$E9w$5m)kv(3wSCNWq=#DaKZ zs%P`#^b7F-J0DgQ1?~2M`5ClYtYN{AlU|v4pEg4z03=g6nqH`JjQuM{k`!6jaIL_F zC;sn?1x?~uMo_DFg#ypNeie{3udcm~M&bYJ1LI zE%y}P9oCX3I1Y9yhF(y9Ix_=8L(p)EYr&|XZWCOb$7f2qX|A4aJ9bl7pt40Xr zXUT#NMBB8I@xoIGSHAZkYdCj>eEd#>a;W-?v4k%CwBaR5N>e3IFLRbDQTH#m_H+4b zk2UHVymC`%IqwtHUmpS1!1p-uQB`CW1Y!+VD!N4TT}D8(V0IOL|&R&)Rwj@n8g@=`h&z9YTPDT+R9agnwPuM!JW~=_ya~% zIJ*>$Fl;y7_`B7G4*P!kcy=MnNmR`(WS5_sRsvHF42NJ;EaDram5HwQ4Aw*qbYn0j;#)bh1lyKLg#dYjN*BMlh+fxmCL~?zB;HBWho;20WA==ci0mAqMfyG>1!HW zO7rOga-I9bvut1Ke_1eFo9tbzsoPTXDW1Si4}w3fq^Z|5LGf&egnw%DV=b11$F=P~ z(aV+j8S}m=CkI*8=RcrT>GmuYifP%hCoKY22Z4 zmu}o08h3YhcXx-v-QC??8mDn<+}+*X{+gZH-I;G^|7=1fBveS?J$27H&wV5^V^P$! z84?{UeYSmZ3M!@>UFoIN?GJT@IroYr;X@H~ax*CQ>b5|Xi9FXt5j`AwUPBq`0sWEJ z3O|k+g^JKMl}L(wfCqyMdRj9yS8ncE7nI14Tv#&(?}Q7oZpti{Q{Hw&5rN-&i|=fWH`XTQSu~1jx(hqm$Ibv zRzFW9$xf@oZAxL~wpj<0ZJ3rdPAE=0B>G+495QJ7D>=A&v^zXC9)2$$EnxQJ<^WlV zYKCHb1ZzzB!mBEW2WE|QG@&k?VXarY?umPPQ|kziS4{EqlIxqYHP!HN!ncw6BKQzKjqk!M&IiOJ9M^wc~ZQ1xoaI z;4je%ern~?qi&J?eD!vTl__*kd*nFF0n6mGEwI7%dI9rzCe~8vU1=nE&n4d&8}pdL zaz`QAY?6K@{s2x%Sx%#(y+t6qLw==>2(gb>AksEebXv=@ht>NBpqw=mkJR(c?l7vo z&cV)hxNoYPGqUh9KAKT)kc(NqekzE6(wjjotP(ac?`DJF=Sb7^Xet-A3PRl%n&zKk zruT9cS~vV1{%p>OVm1-miuKr<@rotj*5gd$?K`oteNibI&K?D63RoBjw)SommJ5<4 zus$!C8aCP{JHiFn2>XpX&l&jI7E7DcTjzuLYvON2{rz<)#$HNu(;ie-5$G<%eLKnTK7QXfn(UR(n+vX%aeS6!q6kv z!3nzY76-pdJp339zsl_%EI|;ic_m56({wdc(0C5LvLULW=&tWc5PW-4;&n+hm1m`f zzQV0T>OPSTjw=Ox&UF^y< zarsYKY8}YZF+~k70=olu$b$zdLaozBE|QE@H{_R21QlD5BilYBTOyv$D5DQZ8b1r- zIpSKX!SbA0Pb5#cT)L5!KpxX+x+8DRy&`o-nj+nmgV6-Gm%Fe91R1ca3`nt*hRS|^ z<&we;TJcUuPDqkM7k0S~cR%t7a`YP#80{BI$e=E!pY}am)2v3-Iqk2qvuAa1YM>xj#bh+H2V z{b#St2<;Gg>$orQ)c2a4AwD5iPcgZ7o_}7xhO86(JSJ(q(EWKTJDl|iBjGEMbX8|P z4PQHi+n(wZ_5QrX0?X_J)e_yGcTM#E#R^u_n8pK@l5416`c9S=q-e!%0RjoPyTliO zkp{OC@Ep^#Ig-n!C)K0Cy%8~**Vci8F1U(viN{==KU0nAg2(+K+GD_Gu#Bx!{tmUm zCwTrT(tCr6X8j43_n96H9%>>?4akSGMvgd+krS4wRexwZ1JxrJy!Uhz#yt$-=aq?A z@?*)bRZxjG9OF~7d$J0cwE_^CLceRK=LvjfH-~{S><^D;6B2&p-02?cl?|$@>`Qt$ zP*iaOxg<+(rbk>34VQDQpNQ|a9*)wScu!}<{oXC87hRPqyrNWpo?#=;1%^D2n2+C* zKKQH;?rWn-@%Y9g%NHG&lHwK9pBfV1a`!TqeU_Fv8s6_(@=RHua7`VYO|!W&WL*x= zIWE9eQaPq3zMaXuf)D0$V`RIZ74f)0P73xpeyk4)-?8j;|K%pD$eq4j2%tL=;&+E91O(2p91K|85b)GQcbRe&u6Ilu@SnE={^{Ix1Eqgv8D z4=w65+&36|;5WhBm$!n*!)ACCwT9Sip#1_z&g~E1kB=AlEhO0lu`Ls@6gw*a)lzc# zKx!fFP%eSBBs)U>xIcQKF(r_$SWD3TD@^^2Ylm=kC*tR+I@X>&SoPZdJ2fT!ysjH% z-U%|SznY8Fhsq7Vau%{Ad^Pvbf3IqVk{M2oD+w>MWimJA@VSZC$QooAO3 zC=DplXdkyl>mSp^$zk7&2+eoGQ6VVh_^E#Z3>tX7Dmi<2aqlM&YBmK&U}m>a%8)LQ z8v+c}a0QtXmyd%Kc2QNGf8TK?_EK4wtRUQ*VDnf5jHa?VvH2K(FDZOjAqYufW8oIZ z31|o~MR~T;ZS!Lz%8M0*iVARJ>_G2BXEF8(}6Dmn_rFV~5NI`lJjp`Mi~g7~P%H zO`S&-)Fngo3VXDMo7ImlaZxY^s!>2|csKca6!|m7)l^M0SQT1_L~K29%x4KV8*xiu zwP=GlyIE9YPSTC0BV`6|#)30=hJ~^aYeq7d6TNfoYUkk-^k0!(3qp(7Mo-$|48d8Z2d zrsfsRM)y$5)0G`fNq!V?qQ+nh0xwFbcp{nhW%vZ?h);=LxvM(pWd9FG$Bg1;@Bv)mKDW>AP{ol zD(R~mLzdDrBv$OSi{E%OD`Ano=F^vwc)rNb*Bg3-o)bbAgYE=M7Gj2OHY{8#pM${_^ zwkU|tnTKawxUF7vqM9UfcQ`V49zg78V%W)$#5ssR}Rj7E&p(4_ib^?9luZPJ%iJTvW&-U$nFYky>KJwHpEHHx zVEC;!ETdkCnO|${Vj#CY>LLut_+c|(hpWk8HRgMGRY%E--%oKh@{KnbQ~0GZd}{b@ z`J2qHBcqqjfHk^q=uQL!>6HSSF3LXL*cCd%opM|k#=xTShX~qcxpHTW*BI!c3`)hQq{@!7^mdUaG7sFsFYnl1%blslM;?B8Q zuifKqUAmR=>33g~#>EMNfdye#rz@IHgpM$~Z7c5@bO@S>MyFE3_F}HVNLnG0TjtXU zJeRWH^j5w_qXb$IGs+E>daTa}XPtrUnnpTRO9NEx4g6uaFEfHP9gW;xZnJi{oqAH~ z5dHS(ch3^hbvkv@u3QPLuWa}ImaElDrmIc%5HN<^bwej}3+?g) z-ai7D&6Iq_P(}k`i^4l?hRLbCb>X9iq2UYMl=`9U9Rf=3Y!gnJbr?eJqy>Zpp)m>Ae zcQ4Qfs&AaE?UDTODcEj#$_n4KeERZHx-I+E5I~E#L_T3WI3cj$5EYR75H7hy%80a8Ej?Y6hv+fR6wHN%_0$-xL!eI}fdjOK7(GdFD%`f%-qY@-i@fTAS&ETI99jUVg8 zslPSl#d4zbOcrgvopvB2c2A6r^pEr&Sa5I5%@1~BpGq`Wo|x=&)WnnQjE+)$^U-wW zr2Kv?XJby(8fcn z8JgPn)2_#-OhZ+;72R6PspMfCVvtLxFHeb7d}fo(GRjm_+R(*?9QRBr+yPF(iPO~ zA4Tp1<0}#fa{v0CU6jz}q9;!3Pew>ikG1qh$5WPRTQZ~ExQH}b1hDuzRS1}65uydS z~Te*3@?o8fih=mZ`iI!hL5iv3?VUBLQv0X zLtu58MIE7Jbm?)NFUZuMN2_~eh_Sqq*56yIo!+d_zr@^c@UwR&*j!fati$W<=rGGN zD$X`$lI%8Qe+KzBU*y3O+;f-Csr4$?3_l+uJ=K@dxOfZ?3APc5_x2R=a^kLFoxt*_ z4)nvvP+(zwlT5WYi!4l7+HKqzmXKYyM9kL5wX$dTSFSN&)*-&8Q{Q$K-})rWMin8S zy*5G*tRYNqk7&+v;@+>~EIQgf_SB;VxRTQFcm5VtqtKZ)x=?-f+%OY(VLrXb^6*aP zP&0Nu@~l2L!aF8i2!N~fJiHyxRl?I1QNjB)`uP_DuaU?2W;{?0#RGKTr2qH5QqdhK zP__ojm4WV^PUgmrV)`~f>(769t3|13DrzdDeXxqN6XA|_GK*;zHU()a(20>X{y-x| z2P6Ahq;o=)Nge`l+!+xEwY`7Q(8V=93A9C+WS^W%p&yR)eiSX+lp)?*7&WSYSh4i> zJa6i5T9o;Cd5z%%?FhB?J{l+t_)c&_f86gZMU{HpOA=-KoU5lIL#*&CZ_66O5$3?# ztgjGLo`Y7bj&eYnK#5x1trB_6tpu4$EomotZLb*9l6P(JmqG`{z$?lNKgq?GAVhkA zvw!oFhLyX=$K=jTAMwDQ)E-8ZW5$X%P2$YB5aq!VAnhwGv$VR&;Ix#fu%xlG{|j_K zbEYL&bx%*YpXcaGZj<{Y{k@rsrFKh7(|saspt?OxQ~oj_6En(&!rTZPa7fLCEU~mA zB7tbVs=-;cnzv*#INgF_9f3OZhp8c5yk!Dy1+`uA7@eJfvd~g34~wKI1PW%h(y&nA zRwMni12AHEw36)C4Tr-pt6s82EJa^8N#bjy??F*rg4fS@?6^MbiY3;7x=gd~G|Hi& zwmG+pAn!aV>>nNfP7-Zn8BLbJm&7}&ZX+$|z5*5{{F}BRSxN=JKZTa#{ut$v0Z0Fs za@UjXo#3!wACv+p9k*^9^n+(0(YKIUFo`@ib@bjz?Mh8*+V$`c%`Q>mrc5bs4aEf4 zh0qtL1qNE|xQ9JrM}qE>X>Y@dQ?%` zBx(*|1FMzVY&~|dE^}gHJ37O9bjnk$d8vKipgcf+As(kt2cbxAR3^4d0?`}}hYO*O z{+L&>G>AYaauAxE8=#F&u#1YGv%`d*v+EyDcU2TnqvRE33l1r}p#Vmcl%n>NrYOqV z2Car_^^NsZ&K=a~bj%SZlfxzHAxX$>=Q|Zi;E0oyfhgGgqe1Sd5-E$8KV9=`!3jWZCb2crb;rvQ##iw}xm7Da za!H${ls5Ihwxkh^D)M<4Yy3bp<-0a+&KfV@CVd9X6Q?v)$R3*rfT@jsedSEhoV(vqv?R1E8oWV;_{l_+_6= zLjV^-bZU$D_ocfSpRxDGk*J>n4G6s-e>D8JK6-gA>aM^Hv8@)txvKMi7Pi#DS5Y?r zK0%+L;QJdrIPXS2 ztjWAxkSwt2xG$L)Zb7F??cjs!KCTF+D{mZ5e0^8bdu_NLgFHTnO*wx!_8#}NO^mu{FaYeCXGjnUgt_+B-Ru!2_Ue-0UPg2Y)K3phLmR<4 zqUCWYX!KDU!jYF6c?k;;vF@Qh^q(PWwp1ez#I+0>d7V(u_h|L+kX+MN1f5WqMLn!L z!c(pozt7tRQi&duH8n=t-|d)c^;%K~6Kpyz(o53IQ_J+aCapAif$Ek#i0F9U>i+94 zFb=OH5(fk-o`L(o|DyQ(hlozl*2cu#)Y(D*zgNMi1Z!DTex#w#)x(8A-T=S+eByJW z%-k&|XhdZOWjJ&(FTrZNWRm^pHEot_MRQ_?>tKQ&MB~g(&D_e>-)u|`Ot(4j=UT6? zQ&YMi2UnCKlBpwltP!}8a2NJ`LlfL=k8SQf69U)~=G;bq9<2GU&Q#cHwL|o4?ah1` z;fG)%t0wMC;DR?^!jCoKib_iiIjsxCSxRUgJDCE%0P;4JZhJCy)vR1%zRl>K?V6#) z2lDi*W3q9rA zo;yvMujs+)a&00~W<-MNj=dJ@4%tccwT<@+c$#CPR%#aE#Dra+-5eSDl^E>is2v^~ z8lgRwkpeU$|1LW4yFwA{PQ^A{5JY!N5PCZ=hog~|FyPPK0-i;fCl4a%1 z?&@&E-)b4cK)wjXGq|?Kqv0s7y~xqvSj-NpOImt{Riam*Z!wz-coZIMuQU>M%6ben z>P@#o^W;fizVd#?`eeEPs#Gz^ySqJn+~`Pq%-Ee6*X+E>!PJGU#rs6qu0z5{+?`-N zxf1#+JNk7e6AoJTdQwxs&GMTq?Djch_8^xL^A;9XggtGL>!@0|BRuIdE&j$tzvt7I zr@I@0<0io%lpF697s1|qNS|BsA>!>-9DVlgGgw2;;k;=7)3+&t!);W3ulPgR>#JiV zUerO;WxuJqr$ghj-veVGfKF?O7si#mzX@GVt+F&atsB@NmBoV4dK|!owGP005$7LN7AqCG(S+={YA- zn#I{UoP_$~Epc=j78{(!2NLN)3qSm-1&{F&1z4Dz&7Mj_+SdlR^Q5{J=r822d4A@?Rj~xATaWewHUOus{*C|KoH`G zHB8SUT06GpSt)}cFJ18!$Kp@r+V3tE_L^^J%9$&fcyd_AHB)WBghwqBEWW!oh@StV zDrC?ttu4#?Aun!PhC4_KF1s2#kvIh~zds!y9#PIrnk9BWkJpq}{Hlqi+xPOR&A1oP zB0~1tV$Zt1pQuHpJw1TAOS=3$Jl&n{n!a+&SgYVe%igUtvE>eHqKY0`e5lwAf}2x( zP>9Wz+9uirp7<7kK0m2&Y*mzArUx%$CkV661=AIAS=V=|xY{;$B7cS5q0)=oq0uXU z_roo90&gHSfM6@6kmB_FJZ)3y_tt0}7#PA&pWo@_qzdIMRa-;U*Dy>Oo#S_n61Fn! z%mrH%tRmvQvg%UqN_2(C#LSxgQ>m}FKLGG=uqJQuSkk=S@c~QLi4N+>lr}QcOuP&% zQCP^cRk&rk-@lpa0^Lcvdu`F*qE)-0$TnxJlwZf|dP~s8cjhL%>^+L~{umxl5Xr6@ z^7zVKiN1Xg;-h+kr4Yt2BzjZs-Mo54`pDbLc}fWq{34=6>U9@sBP~iWZE`+FhtU|x zTV}ajn*Hc}Y?3agQ+bV@oIRm=qAu%|zE;hBw7kCcDx{pm!_qCxfPX3sh5^B$k_2d` z6#rAeUZC;e-LuMZ-f?gHeZogOa*mE>ffs+waQ+fQl4YKoAyZii_!O0;h55EMzD{;) z8lSJvv((#UqgJ?SCQFqJ-UU?2(0V{;7zT3TW`u6GH6h4m3}SuAAj_K(raGBu>|S&Q zZGL?r9@caTbmRm7p=&Tv?Y1)60*9At38w)$(1c?4cpFY2RLyw9c<{OwQE{b@WI}FQ zTT<2HOF4222d%k70yL~x_d#6SNz`*%@4++8gYQ8?yq0T@w~bF@aOHL2)T4xj`AVps9k z?m;<2ClJh$B6~fOYTWIV*T9y1BpB1*C?dgE{%lVtIjw>4MK{wP6OKTb znbPWrkZjYCbr`GGa%Xo0h;iFPNJBI3fK5`wtJV?wq_G<_PZ<`eiKtvN$IKfyju*^t zXc}HNg>^PPZ16m6bfTpmaW5=qoSsj>3)HS}teRa~qj+Y}mGRE?cH!qMDBJ8 zJB!&-=MG8Tb;V4cZjI_#{>ca0VhG_P=j0kcXVX5)^Sdpk+LKNv#yhpwC$k@v^Am&! z_cz2^4Cc{_BC!K#zN!KEkPzviUFPJ^N_L-kHG6}(X#$>Q=9?!{$A(=B3)P?PkxG9gs#l! zo6TOHo$F|IvjTC3MW%XrDoc7;m-6wb9mL(^2(>PQXY53hE?%4FW$rTHtN`!VgH72U zRY)#?Y*pMA<)x3B-&fgWQ(TQ6S6nUeSY{9)XOo_k=j$<*mA=f+ghSALYwBw~!Egn!jtjubOh?6Cb-Zi3IYn*fYl()^3u zRiX0I{5QaNPJ9w{yh4(o#$geO7b5lSh<5ZaRg9_=aFdZjxjXv(_SCv^v-{ZKQFtAA}kw=GPC7l81GY zeP@0Da{aR#{6`lbI0ON0y#K=t|L*}MG_HSl$e{U;v=BSs{SU3(e*qa(l%rD;(zM^3 zrRgN3M#Sf(Cr9>v{FtB`8JBK?_zO+~{H_0$lLA!l{YOs9KQd4Zt<3*Ns7dVbT{1Ut z?N9{XkN(96?r(4BH~3qeiJ_CAt+h1}O_4IUF$S(5EyTyo=`{^16P z=VhDY!NxkDukQz>T`0*H=(D3G7Np*2P`s(6M*(*ZJa;?@JYj&_z`d5bap=KK37p3I zr5#`%aC)7fUo#;*X5k7g&gQjxlC9CF{0dz*m2&+mf$Sc1LnyXn9lpZ!!Bl!@hnsE5px};b-b-`qne0Kh;hziNC zXV|zH%+PE!2@-IrIq!HM2+ld;VyNUZiDc@Tjt|-1&kq}>muY;TA3#Oy zWdYGP3NOZWSWtx6?S6ES@>)_Yz%%nLG3P>Z7`SrhkZ?shTfrHkYI;2zAn8h65wV3r z^{4izW-c9!MTge3eN=~r5aTnz6*6l#sD68kJ7Nv2wMbL~Ojj0H;M`mAvk*`Q!`KI? z7nCYBqbu$@MSNd+O&_oWdX()8Eh|Z&v&dJPg*o-sOBb2hriny)< zd(o&&kZM^NDtV=hufp8L zCkKu7)k`+czHaAU567$?GPRGdkb4$37zlIuS&<&1pgArURzoWCbyTEl9OiXZBn4p<$48-Gekh7>e)v*?{9xBt z=|Rx!@Y3N@ffW5*5!bio$jhJ7&{!B&SkAaN`w+&3x|D^o@s{ZAuqNss8K;211tUWIi1B!%-ViYX+Ys6w)Q z^o1{V=hK#+tt&aC(g+^bt-J9zNRdv>ZYm9KV^L0y-yoY7QVZJ_ivBS02I|mGD2;9c zR%+KD&jdXjPiUv#t1VmFOM&=OUE2`SNm4jm&a<;ZH`cYqBZoAglCyixC?+I+}*ScG#;?SEAFob{v0ZKw{`zw*tX}<2k zoH(fNh!>b5w8SWSV}rQ*E24cO=_eQHWy8J!5;Y>Bh|p;|nWH|nK9+ol$k`A*u*Y^Uz^%|h4Owu}Cb$zhIxlVJ8XJ0xtrErT zcK;34CB;ohd|^NfmVIF=XlmB5raI}nXjFz;ObQ4Mpl_`$dUe7sj!P3_WIC~I`_Xy@ z>P5*QE{RSPpuV=3z4p3}dh>Dp0=We@fdaF{sJ|+_E*#jyaTrj-6Y!GfD@#y@DUa;& zu4Iqw5(5AamgF!2SI&WT$rvChhIB$RFFF|W6A>(L9XT{0%DM{L`knIQPC$4F`8FWb zGlem_>>JK-Fib;g*xd<-9^&_ue95grYH>5OvTiM;#uT^LVmNXM-n8chJBD2KeDV7t zbnv3CaiyN>w(HfGv86K5MEM{?f#BTR7**smpNZ}ftm+gafRSt=6fN$(&?#6m3hF!>e$X)hFyCF++Qvx(<~q3esTI zH#8Sv!WIl2<&~=B)#sz1x2=+KTHj=0v&}iAi8eD=M->H|a@Qm|CSSzH#eVIR3_Tvu zG8S**NFbz%*X?DbDuP(oNv2;Lo@#_y4k$W+r^#TtJ8NyL&&Rk;@Q}~24`BB)bgwcp z=a^r(K_NEukZ*|*7c2JKrm&h&NP)9<($f)eTN}3|Rt`$5uB0|!$Xr4Vn#i;muSljn zxG?zbRD(M6+8MzGhbOn%C`M#OcRK!&ZHihwl{F+OAnR>cyg~No44>vliu$8^T!>>*vYQJCJg=EF^lJ*3M^=nGCw`Yg@hCmP(Gq^=eCEE1!t-2>%Al{w@*c% zUK{maww*>K$tu;~I@ERb9*uU@LsIJ|&@qcb!&b zsWIvDo4#9Qbvc#IS%sV1_4>^`newSxEcE08c9?rHY2%TRJfK2}-I=Fq-C)jc`gzV( zCn?^noD(9pAf2MP$>ur0;da`>Hr>o>N@8M;X@&mkf;%2A*2CmQBXirsJLY zlX21ma}mKH_LgYUM-->;tt;6F?E5=fUWDwQhp*drQ%hH0<5t2m)rFP%=6aPIC0j$R znGI0hcV~}vk?^&G`v~YCKc7#DrdMM3TcPBmxx#XUC_JVEt@k=%3-+7<3*fTcQ>f~?TdLjv96nb66xj=wVQfpuCD(?kzs~dUV<}P+Fpd)BOTO^<*E#H zeE80(b~h<*Qgez(iFFOkl!G!6#9NZAnsxghe$L=Twi^(Q&48 zD0ohTj)kGLD){xu%pm|}f#ZaFPYpHtg!HB30>F1c=cP)RqzK2co`01O5qwAP zUJm0jS0#mci>|Nu4#MF@u-%-4t>oUTnn_#3K09Hrwnw13HO@9L;wFJ*Z@=gCgpA@p zMswqk;)PTXWuMC-^MQxyNu8_G-i3W9!MLd2>;cM+;Hf&w| zLv{p*hArp9+h2wsMqT5WVqkkc0>1uokMox{AgAvDG^YJebD-czexMB!lJKWllLoBI zetW2;;FKI1xNtA(ZWys!_un~+834+6y|uV&Lo%dKwhcoDzRADYM*peh{o`-tHvwWIBIXW`PKwS3|M>CW37Z2dr!uJWNFS5UwY4;I zNIy1^sr+@8Fob%DHRNa&G{lm?KWU7sV2x9(Ft5?QKsLXi!v6@n&Iyaz5&U*|hCz+d z9vu60IG<v6+^ZmBs_aN!}p|{f(ikVl&LcB+UY;PPz* zj84Tm>g5~-X=GF_4JrVmtEtm=3mMEL1#z+pc~t^Iify^ft~cE=R0TymXu*iQL+XLX zdSK$~5pglr3f@Lrcp`>==b5Z6r7c=p=@A5nXNacsPfr(5m;~ks@*Wu7A z%WyY$Pt*RAKHz_7cghHuQqdU>hq$vD?plol_1EU(Fkgyo&Q2&2e?FT3;H%!|bhU~D z>VX4-6}JLQz8g3%Bq}n^NhfJur~v5H0dbB^$~+7lY{f3ES}E?|JnoLsAG%l^%eu_PM zEl0W(sbMRB3rFeYG&tR~(i2J0)RjngE`N_Jvxx!UAA1mc7J>9)`c=`}4bVbm8&{A` z3sMPU-!r-8de=P(C@7-{GgB<5I%)x{WfzJwEvG#hn3ict8@mexdoTz*(XX!C&~}L* z^%3eYQ8{Smsmq(GIM4d5ilDUk{t@2@*-aevxhy7yk(wH?8yFz%gOAXRbCYzm)=AsM z?~+vo2;{-jkA%Pqwq&co;|m{=y}y2lN$QPK>G_+jP`&?U&Ubq~T`BzAj1TlC`%8+$ zzdwNf<3suPnbh&`AI7RAYuQ<#!sD|A=ky2?hca{uHsB|0VqShI1G3lG5g}9~WSvy4 zX3p~Us^f5AfXlBZ0hA;mR6aj~Q8yb^QDaS*LFQwg!!<|W!%WX9Yu}HThc7>oC9##H zEW`}UQ%JQ38UdsxEUBrA@=6R-v1P6IoIw8$8fw6F{OSC7`cOr*u?p_0*Jvj|S)1cd z-9T);F8F-Y_*+h-Yt9cQQq{E|y^b@r&6=Cd9j0EZL}Pj*RdyxgJentY49AyC@PM<< zl&*aq_ubX%*pqUkQ^Zsi@DqhIeR&Ad)slJ2g zmeo&+(g!tg$z1ao1a#Qq1J022mH4}y?AvWboI4H028;trScqDQrB36t!gs|uZS9}KG0}DD$ zf2xF}M*@VJSzEJ5>ucf+L_AtN-Ht=34g&C?oPP>W^bwoigIncKUyf61!ce!2zpcNT zj&;rPGI~q2!Sy>Q7_lRX*DoIs-1Cei=Cd=+Xv4=%bn#Yqo@C=V`|QwlF0Y- zONtrwpHQ##4}VCL-1ol(e<~KU9-ja^kryz!g!})y-2S5z2^gE$Isj8l{%tF=Rzy`r z^RcP7vu`jHgHLKUE957n3j+BeE(bf;f)Zw($XaU6rZ26Upl#Yv28=8Y`hew{MbH>* z-sGI6dnb5D&dUCUBS`NLAIBP!Vi!2+~=AU+)^X^IpOEAn#+ab=`7c z%7B|mZ>wU+L;^&abXKan&N)O;=XI#dTV|9OMYxYqLbtT#GY8PP$45Rm2~of+J>>HIKIVn(uQf-rp09_MwOVIp@6!8bKV(C#(KxcW z;Pesq(wSafCc>iJNV8sg&`!g&G55<06{_1pIoL`2<7hPvAzR1+>H6Rx0Ra%4j7H-<-fnivydlm{TBr06;J-Bq8GdE^Amo)ptV>kS!Kyp*`wUx=K@{3cGZnz53`+C zLco1jxLkLNgbEdU)pRKB#Pq(#(Jt>)Yh8M?j^w&RPUueC)X(6`@@2R~PV@G(8xPwO z^B8^+`qZnQr$8AJ7<06J**+T8xIs)XCV6E_3W+al18!ycMqCfV>=rW0KBRjC* zuJkvrv;t&xBpl?OB3+Li(vQsS(-TPZ)Pw2>s8(3eF3=n*i0uqv@RM^T#Ql7(Em{(~%f2Fw|Reg@eSCey~P zBQlW)_DioA*yxxDcER@_=C1MC{UswPMLr5BQ~T6AcRyt0W44ffJG#T~Fk}wU^aYoF zYTayu-s?)<`2H(w+1(6X&I4?m3&8sok^jpXBB<|ZENso#?v@R1^DdVvKoD?}3%@{}}_E7;wt9USgrfR3(wabPRhJ{#1es81yP!o4)n~CGsh2_Yj2F^z|t zk((i&%nDLA%4KFdG96pQR26W>R2^?C1X4+a*hIzL$L=n4M7r$NOTQEo+k|2~SUI{XL{ynLSCPe%gWMMPFLO{&VN2pom zBUCQ(30qj=YtD_6H0-ZrJ46~YY*A;?tmaGvHvS^H&FXUG4)%-a1K~ly6LYaIn+4lG zt=wuGLw!%h=Pyz?TP=?6O-K-sT4W%_|Nl~;k~YA^_`gqfe{Xw=PWn#9f1mNz)sFuL zJbrevo(DPgpirvGMb6ByuEPd=Rgn}fYXqeUKyM+!n(cKeo|IY%p!#va6`D8?A*{u3 zEeWw0*oylJ1X!L#OCKktX2|>-z3#>`9xr~azOH+2dXHRwdfnpri9|xmK^Q~AuY!Fg z`9Xx?hxkJge~)NVkPQ(VaW(Ce2pXEtgY*cL8i4E)mM(iz_vdm|f@%cSb*Lw{WbShh41VGuplex9E^VvW}irx|;_{VK=N_WF39^ zH4<*peWzgc)0UQi4fBk2{FEzldDh5+KlRd!$_*@eYRMMRb1gU~9lSO_>Vh-~q|NTD zL}X*~hgMj$*Gp5AEs~>Bbjjq7G>}>ki1VxA>@kIhLe+(EQS0mjNEP&eXs5)I;7m1a zmK0Ly*!d~Dk4uxRIO%iZ!1-ztZxOG#W!Q_$M7_DKND0OwI+uC;PQCbQ#k#Y=^zQve zTZVepdX>5{JSJb;DX3%3g42Wz2D@%rhIhLBaFmx#ZV8mhya}jo1u{t^tzoiQy=jJp zjY2b7D2f$ZzJx)8fknqdD6fd5-iF8e(V}(@xe)N=fvS%{X$BRvW!N3TS8jn=P%;5j zShSbzsLs3uqycFi3=iSvqH~}bQn1WQGOL4?trj(kl?+q2R23I42!ipQ&`I*&?G#i9 zWvNh8xoGKDt>%@i0+}j?Ykw&_2C4!aYEW0^7)h2Hi7$;qgF3;Go?bs=v)kHmvd|`R z%(n94LdfxxZ)zh$ET8dH1F&J#O5&IcPH3=8o;%>OIT6w$P1Yz4S!}kJHNhMQ1(prc zM-jSA-7Iq=PiqxKSWb+YbLB-)lSkD6=!`4VL~`ExISOh2ud=TI&SKfR4J08Bad&rj zcXxMpcNgOB?w$~L7l^wPcXxw$0=$oV?)`I44)}b#ChS`_lBQhvb6ks?HDr3tFgkg&td19?b8=!sETXtp=&+3T$cCwZe z0nAET-7561gsbBws$TVjP7QxY(NuBYXVn9~9%vyN-B#&tJhWgtL1B<%BTS*-2$xB` zO)cMDHoWsm%JACZF--Pa7oP;f!n%p`*trlpvZ!HKoB={l+-(8O;;eYv2A=ra z3U7rSMCkP_6wAy`l|Se(&5|AefXvV1E#XA(LT!% zjj4|~xlZ-kPLNeQLFyXb%$K}YEfCBvHA-Znw#dZSI6V%3YD{Wj2@utT5Hieyofp6Qi+lz!u)htnI1GWzvQsA)baEuw9|+&(E@p8M+#&fsX@Kf`_YQ>VM+40YLv`3-(!Z7HKYg@+l00WGr779i-%t`kid%e zDtbh8UfBVT3|=8FrNian@aR3*DTUy&u&05x%(Lm3yNoBZXMHWS7OjdqHp>cD>g!wK z#~R{1`%v$IP;rBoP0B0P><;dxN9Xr+fp*s_EK3{EZ94{AV0#Mtv?;$1YaAdEiq5)g zYME;XN9cZs$;*2p63Q9^x&>PaA1p^5m7|W?hrXp2^m;B@xg0bD?J;wIbm6O~Nq^^K z2AYQs@7k)L#tgUkTOUHsh&*6b*EjYmwngU}qesKYPWxU-z_D> zDWr|K)XLf_3#k_9Rd;(@=P^S^?Wqlwert#9(A$*Y$s-Hy)BA0U0+Y58zs~h=YtDKxY0~BO^0&9{?6Nny;3=l59(6ec9j(79M?P1cE zex!T%$Ta-KhjFZLHjmPl_D=NhJULC}i$}9Qt?nm6K6-i8&X_P+i(c*LI3mtl3 z*B+F+7pnAZ5}UU_eImDj(et;Khf-z^4uHwrA7dwAm-e4 zwP1$Ov3NP5ts+e(SvM)u!3aZMuFQq@KE-W;K6 zag=H~vzsua&4Sb$4ja>&cSJ)jjVebuj+?ivYqrwp3!5>ul`B*4hJGrF;!`FaE+wKo z#};5)euvxC1zX0-G;AV@R(ZMl=q_~u8mQ5OYl;@BAkt)~#PynFX#c1K zUQ1^_N8g+IZwUl*n0Bb-vvliVtM=zuMGU-4a8|_8f|2GEd(2zSV?aSHUN9X^GDA8M zgTZW06m*iAy@7l>F3!7+_Y3mj^vjBsAux3$%U#d$BT^fTf-7{Y z_W0l=7$ro5IDt7jp;^cWh^Zl3Ga1qFNrprdu#g=n9=KH!CjLF#ucU5gy6*uASO~|b z7gcqm90K@rqe({P>;ww_q%4}@bq`ST8!0{V08YXY)5&V!>Td)?j7#K}HVaN4FU4DZ z%|7OppQq-h`HJ;rw-BAfH* z1H$ufM~W{%+b@9NK?RAp-$(P0N=b<(;wFbBN0{u5vc+>aoZ|3&^a866X@el7E8!E7 z=9V(Ma**m_{DKZit2k;ZOINI~E$|wO99by=HO{GNc1t?nl8soP@gxk8)WfxhIoxTP zoO`RA0VCaq)&iRDN9yh_@|zqF+f07Esbhe!e-j$^PS57%mq2p=+C%0KiwV#t^%_hH zoO?{^_yk5x~S)haR6akK6d|#2TN& zfWcN zc7QAWl)E9`!KlY>7^DNw$=yYmmRto>w0L(~fe?|n6k2TBsyG@sI)goigj=mn)E)I* z4_AGyEL7?(_+2z=1N@D}9$7FYdTu;%MFGP_mEJXc2OuXEcY1-$fpt8m_r2B|<~Xfs zX@3RQi`E-1}^9N{$(|YS@#{ZWuCxo)91{k>ESD54g_LYhm~vlOK_CAJHeYFfuIVB^%cqCfvpy#sU8Do8u}# z>>%PLKOZ^+$H54o@brtL-hHorSKcsjk_ZibBKBgyHt~L z=T6?e0oLX|h!Z3lbkPMO27MM?xn|uZAJwvmX?Yvp#lE3sQFY)xqet>`S2Y@1t)Z*& z;*I3;Ha8DFhk=YBt~{zp=%%*fEC}_8?9=(-k7HfFeN^GrhNw4e?vx*#oMztnO*&zY zmRT9dGI@O)t^=Wj&Og1R3b%(m*kb&yc;i`^-tqY9(0t!eyOkH<$@~1lXmm!SJllE_ zr~{a&w|8*LI>Z^h!m%YLgKv06Js7j7RaoX}ZJGYirR<#4Mghd{#;38j3|V+&=ZUq#1$ zgZb-7kV)WJUko?{R`hpSrC;w2{qa`(Z4gM5*ZL`|#8szO=PV^vpSI-^K_*OQji^J2 zZ_1142N}zG$1E0fI%uqHOhV+7%Tp{9$bAR=kRRs4{0a`r%o%$;vu!_Xgv;go)3!B#;hC5qD-bcUrKR&Sc%Zb1Y($r78T z=eG`X#IpBzmXm(o6NVmZdCQf6wzqawqI63v@e%3TKuF!cQ#NQbZ^?6K-3`_b=?ztW zA>^?F#dvVH=H-r3;;5%6hTN_KVZ=ps4^YtRk>P1i>uLZ)Ii2G7V5vy;OJ0}0!g>j^ z&TY&E2!|BDIf1}U(+4G5L~X6sQ_e7In0qJmWYpn!5j|2V{1zhjZt9cdKm!we6|Pp$ z07E+C8=tOwF<<}11VgVMzV8tCg+cD_z?u+$sBjwPXl^(Ge7y8-=c=fgNg@FxI1i5Y-HYQMEH z_($je;nw`Otdhd1G{Vn*w*u@j8&T=xnL;X?H6;{=WaFY+NJfB2(xN`G)LW?4u39;x z6?eSh3Wc@LR&yA2tJj;0{+h6rxF zKyHo}N}@004HA(adG~0solJ(7>?LoXKoH0~bm+xItnZ;3)VJt!?ue|~2C=ylHbPP7 zv2{DH()FXXS_ho-sbto)gk|2V#;BThoE}b1EkNYGT8U#0ItdHG>vOZx8JYN*5jUh5Fdr9#12^ zsEyffqFEQD(u&76zA^9Jklbiz#S|o1EET$ujLJAVDYF znX&4%;vPm-rT<8fDutDIPC@L=zskw49`G%}q#l$1G3atT(w70lgCyfYkg7-=+r7$%E`G?1NjiH)MvnKMWo-ivPSQHbk&_l5tedNp|3NbU^wk0SSXF9ohtM zUqXiOg*8ERKx{wO%BimK)=g^?w=pxB1Vu_x<9jKOcU7N;(!o3~UxyO+*ZCw|jy2}V*Z22~KhmvxoTszc+#EMWXTM6QF*ks% zW47#2B~?wS)6>_ciKe1Fu!@Tc6oN7e+6nriSU;qT7}f@DJiDF@P2jXUv|o|Wh1QPf zLG31d>@CpThA+Ex#y)ny8wkC4x-ELYCXGm1rFI=1C4`I5qboYgDf322B_Nk@#eMZ% znluCKW2GZ{r9HR@VY`>sNgy~s+D_GkqFyz6jgXKD)U|*eKBkJRRIz{gm3tUd*yXmR z(O4&#ZA*us6!^O*TzpKAZ#}B5@}?f=vdnqnRmG}xyt=)2o%<9jj>-4wLP1X-bI{(n zD9#|rN#J;G%LJ&$+Gl2eTRPx6BQC6Uc~YK?nMmktvy^E8#Y*6ZJVZ>Y(cgsVnd!tV z!%twMNznd)?}YCWyy1-#P|2Fu%~}hcTGoy>_uawRTVl=(xo5!%F#A38L109wyh@wm zdy+S8E_&$Gjm=7va-b7@Hv=*sNo0{i8B7=n4ex-mfg`$!n#)v@xxyQCr3m&O1Jxg! z+FXX^jtlw=utuQ+>Yj$`9!E<5-c!|FX(~q`mvt6i*K!L(MHaqZBTtuSA9V~V9Q$G? zC8wAV|#XY=;TQD#H;;dcHVb9I7Vu2nI0hHo)!_{qIa@|2}9d ztpC*Q{4Py~2;~6URN^4FBCBip`QDf|O_Y%iZyA0R`^MQf$ce0JuaV(_=YA`knEMXw zP6TbjYSGXi#B4eX=QiWqb3bEw-N*a;Yg?dsVPpeYFS*&AsqtW1j2D$h$*ZOdEb$8n0 zGET4Igs^cMTXWG{2#A7w_usx=KMmNfi4oAk8!MA8Y=Rh9^*r>jEV(-{I0=rc);`Y) zm+6KHz-;MIy|@2todN&F+Yv1e&b&ZvycbTHpDoZ>FIiUn+M-=%A2C(I*^Yx@VKf(Z zxJOny&WoWcyKodkeN^5))aV|-UBFw{?AGo?;NNFFcKzk+6|gYfA#FR=y@?;3IoQ zUMI=7lwo9gV9fRvYi}Nd)&gQw7(K3=a0#p27u6Q)7JlP#A)piUUF8B3Li&38Xk$@| z9OR+tU~qgd3T3322E))eV)hAAHYIj$TmhH#R+C-&E-}5Qd{3B}gD{MXnsrS;{Erv1 z6IyQ=S2qD>Weqqj#Pd65rDSdK54%boN+a?=CkR|agnIP6;INm0A*4gF;G4PlA^3%b zN{H%#wYu|!3fl*UL1~f+Iu|;cqDax?DBkZWSUQodSDL4Es@u6zA>sIm>^Aq-&X#X8 zI=#-ucD|iAodfOIY4AaBL$cFO@s(xJ#&_@ZbtU+jjSAW^g;_w`FK%aH_hAY=!MTjI zwh_OEJ_25zTQv$#9&u0A11x_cGd92E74AbOrD`~f6Ir9ENNQAV2_J2Ig~mHWhaO5a zc>fYG$zke^S+fBupw+klDkiljJAha z6DnTemhkf>hv`8J*W_#wBj-2w(cVtXbkWWtE(3j@!A-IfF?`r$MhVknTs3D1N`rYN zKth9jZtX#>v#%U@^DVN!;ni#n1)U&H_uB{6pcq7$TqXJX!Q0P7U*JUZyclb~)l*DS zOLpoQfW_3;a0S$#V0SOwVeeqE$Hd^L`$;l_~2giLYd?7!gUYIpOs!jqSL~pI)4`YuB_692~A z^T#YYQ_W3Rakk}$SL&{`H8mc{>j+3eKprw6BK`$vSSIn;s31M~YlJLApJ)+Gi1{^- zw96WnT9M0Vr_D=e=a}${raR{(35Q!g+8`}vOFj1e&Or(_wp2U2aVQP0_jP57 z2(R4E(E$n!xl<}Zx38wO;27wuQ`P#_j!}L2 z2qr;As4D4n2X$-Jd_-!fsbu_D(64i;c4cJnP576x_>Q4WNushFwkBV!kVd(AYFXe{ zaqO5`Qfr!#ETmE(B;u_&FITotv~W}QYFCI!&ENKIb1p4fg*Yv1)EDMb==EjHHWM#{ zGMpqb2-LXdHB@D~pE3|+B392Gh4q)y9jBd$a^&cJM60VEUnLtHQD5i-X6PVF>9m_k zDvG3P(?CzdaIrC8s4cu~N9MEb!Tt(g*GK~gIp1Gyeaw3b7#YPx_1T6i zRi#pAMr~PJKe9P~I+ARa$a!K~)t(4LaVbjva1yd;b1Yz2$7MMc`aLmMl(a^DgN(u? zq2o9&Gif@Tq~Yq+qDfx^F*nCnpuPv%hRFc$I!p74*quLt^M}D_rwl10uMTr!)(*=7 zSC5ea@#;l(h87k4T4x)(o^#l76P-GYJA(pOa&F9YT=fS<*O{4agzba^dIrh0hjls<~APlIz9{ zgRY{OMv2s|`;VCoYVj?InYoq^QWuA&*VDyOn@pPvK8l~g#1~~MGVVvtLDt}>id_Z` zn(ihfL?Y}Y4YX335m*Xx(y+bbukchHrM zycIGp#1*K3$!(tgTsMD2VyUSg^yvCwB8*V~sACE(yq2!MS6f+gsxv^GR|Q7R_euYx z&X+@@H?_oQddGxJYS&ZG-9O(X+l{wcw;W7srpYjZZvanY(>Q1utSiyuuonkjh5J0q zGz6`&meSuxixIPt{UoHVupUbFKIA+3V5(?ijn}(C(v>=v?L*lJF8|yRjl-m#^|krg zLVbFV6+VkoEGNz6he;EkP!Z6|a@n8?yCzX9>FEzLnp21JpU0x!Qee}lwVKA})LZJq zlI|C??|;gZ8#fC3`gzDU%7R87KZyd)H__0c^T^$zo@TBKTP*i{)Gp3E0TZ}s3mKSY zix@atp^j#QnSc5K&LsU38#{lUdwj%xF zcx&l^?95uq9on1m*0gp$ruu||5MQo)XaN>|ngV5Jb#^wWH^5AdYcn_1>H~XtNwJd3 zd9&?orMSSuj=lhO?6)Ay7;gdU#E}pTBa5wFu`nejq##Xd71BHzH2XqLA5 zeLEo;9$}~u0pEu@(?hXB_l;{jQ=7m?~mwj-ME~Tw-OHPrR7K2Xq9eCNwQO$hR z3_A?=`FJctNXA#yQEorVoh{RWxJbdQga zU%K##XEPgy?E|K(=o#IPgnbk7E&5%J=VHube|2%!Qp}@LznjE%VQhJ?L(XJOmFVY~ zo-az+^5!Ck7Lo<7b~XC6JFk>17*_dY;=z!<0eSdFD2L?CSp_XB+?;N+(5;@=_Ss3& zXse>@sA7hpq;IAeIp3hTe9^$DVYf&?)={zc9*hZAV)|UgKoD!1w{UVo8D)Htwi8*P z%#NAn+8sd@b{h=O)dy9EGKbpyDtl@NBZw0}+Wd=@65JyQ2QgU}q2ii;ot1OsAj zUI&+Pz+NvuRv#8ugesT<<@l4L$zso0AQMh{we$tkeG*mpLmOTiy8|dNYhsqhp+q*yfZA`Z)UC*(oxTNPfOFk3RXkbzAEPofVUy zZ3A%mO?WyTRh@WdXz+zD!ogo}gbUMV!YtTNhr zrt@3PcP%5F;_SQ>Ui`Gq-lUe&taU4*h2)6RDh@8G1$o!){k~3)DT87%tQeHYdO?B` zAmoJvG6wWS?=0(Cj?Aqj59`p(SIEvYyPGJ^reI z`Hr?3#U2zI7k0=UmqMD35l`>3xMcWlDv$oo6;b`dZq3d!~)W z=4Qk)lE8&>#HV>?kRLOHZYz83{u7?^KoXmM^pazj8`7OwQ=5I!==; zA!uN`Q#n=Drmzg}@^nG!mJp9ml3ukWk96^6*us*;&>s+7hWfLXtl?a}(|-#=P12>A zon1}yqh^?9!;on?tRd6Fk0knQSLl4vBGb87A_kJNDGyrnpmn48lz_%P{* z_G*3D#IR<2SS54L5^h*%=)4D9NPpji7DZ5&lHD|99W86QN_(|aJ<5C~PX%YB`Qt_W z>jF_Os@kI6R!ub4n-!orS(G6~mKL7()1g=Lf~{D!LR7#wRHfLxTjYr{*c{neyhz#U zbm@WBKozE+kTd+h-mgF+ELWqTKin57P;0b){ zii5=(B%S(N!Z=rAFGnM6iePtvpxB_Q9-oq_xH!URn2_d-H~i;lro8r{-g!k-Ydb6_w5K@FOV?zPF_hi z%rlxBv$lQi%bjsu^7KT~@u#*c$2-;AkuP)hVEN?W5MO8C9snj*EC&|M!aK6o12q3+ z8e?+dH17E!A$tRlbJW~GtMDkMPT=m1g-v67q{sznnWOI$`g(8E!Pf!#KpO?FETxLK z2b^8^@mE#AR1z(DT~R3!nnvq}LG2zDGoE1URR=A2SA z%lN$#V@#E&ip_KZL}Q6mvm(dsS?oHoRf8TWL~1)4^5<3JvvVbEsQqSa3(lF*_mA$g zv`LWarC79G)zR0J+#=6kB`SgjQZ2460W zN%lZt%M@=EN>Wz4I;eH>C0VnDyFe)DBS_2{h6=0ZJ*w%s)QFxLq+%L%e~UQ0mM9ud zm&|r){_<*Om%vlT(K9>dE(3AHjSYro5Y1I?ZjMqWyHzuCE0nyCn`6eq%MEt(aY=M2rIzHeMds)4^Aub^iTIT|%*izG4YH;sT`D9MR(eND-SB+e66LZT z2VX)RJsn${O{D48aUBl|(>ocol$1@glsxisc#GE*=DXHXA?|hJT#{;X{i$XibrA}X zFHJa+ssa2$F_UC(o2k2Z0vwx%Wb(<6_bdDO#=a$0gK2NoscCr;vyx?#cF)JjM%;a| z$^GIlIzvz%Hx3WVU481}_e4~aWcyC|j&BZ@uWW1`bH1y9EWXOxd~f-VE5DpueNofN zv7vZeV<*!A^|36hUE;`#x%MHhL(~?eZ5fhA9Ql3KHTWoAeO-^7&|2)$IcD1r5X#-u zN~N0$6pHPhop@t1_d`dO3#TC0>y5jm>8;$F5_A2& zt#=^IDfYv?JjPPTPNx2TL-Lrl82VClQSLWW_$3=XPbH}xM34)cyW5@lnxy=&h%eRq zv29&h^fMoxjsDnmua(>~OnX{Cq!7vM0M4Mr@_18|YuSKPBKUTV$s^So zc}JlAW&bVz|JY#Eyup6Ny{|P_s0Pq;5*tinH+>5Xa--{ z2;?2PBs((S4{g=G`S?B3Ien`o#5DmUVwzpGuABthYG~OKIY`2ms;33SN9u^I8i_H5`BQ%yOfW+N3r|ufHS_;U;TWT5z;b14n1gX%Pn`uuO z6#>Vl)L0*8yl|#mICWQUtgzeFp9$puHl~m&O+vj3Ox#SxQUa?fY*uK?A;00RiFg(G zK?g=7b5~U4QIK`C*um%=Sw=OJ1eeaV@WZ%hh-3<=lR#(Xesk%?)l4p(EpTwPvN99V@TT)!A8SeFTV+frN=r|5l?K#odjijx2nFgc3kI zC$hVs1S-!z9>xn9MZcRk0YXdYlf~8*LfH$IHKD59H&gLz%6 z#mAYSRJufbRi~LRadwM*G!O2>&U<^d`@<)otXZJJxT@G}4kTx0zPDVhVXwiU)$}5Y z`0iV`8EEh&GlUk&VY9m0Mqr*U&|^Bc?FB`<%{x-o0ATntwIA%(YDcxWs$C)%a%d_@ z?fx!Co+@3p7ha$|pWYD}p6#(PG%_h8K7sQjT_P~|3ZEH0DRxa3~bP&&lPMj3C~!H2QD zq>(f^RUFSqf6K3BMBFy$jiuoSE+DhEq$xLDb7{57 z0B|1pSjYJ5F@cHG%qDZ{ogL$P!BK&sR%zD`gbK#9gRZX17EtAJxN% zys^gb2=X9=7HP}N(iRqt(tot2yyeE%s;L}AcMh;~-W~s_eAe!gIUYdQz5j~T)0trh z>#1U$uOyyl%!Pi(gD&)uHe9Q^27_kHyFCC}n^-KL(=OxHqUfex1YS__RJh0m-S>eM zqAk`aSev*z1lI&-?CycgDm=bdQCp}RqS0_d-4Mf&>u2KyGFxKe8JM1N{GNWw0n$FL z1UDp(h0(1I2Jh9I`?IS}h4R~n zRwRz>8?$fFMB2{UPe^$Ifl;Oc>}@Q9`|8DCeR{?LUQLPfaMsxs8ps=D_aAXORZH~< zdcIOca-F;+D3~M+)Vi4h)I4O3<)$65yI)goQ_vk#fb;Uim>UI4Dv9#2b1;N_Wg>-F zNwKeMKY+su#~NL0uE%_$mw1%ddX2Qs2P!ncM+>wnz}OCQX1!q~oS?OqYU;&ESAAwP z452QWL0&u^mraF#=j_ZeBWhm&F|d!QjwRl^7=Bl7@(43=BkN=3{BRv#QHIk>Umc_w zvP>q|q{lJ=zs|W9%a@8%W>C@MYN1D5{(=Af31+pR#kB`cd0-YlQQTg}+ zL|_h=F9JQ|Gux5c0ehaffHNYLf8VwF+qnM6IjBEI_eceee;o;FY@#~FFVsZjBSp!j z8V*Bgmn{RK!!zqGc;jy)z@Zjo>5{%m1?K}fLEL$l6Dl4f=ye0wNI#)2L=^K(&18Gb zJoj8@WBB;P^T#V)I0`aDSy?$rJU{+-5472NyFp>;Vw43j@3Z=;D2eSfyw5*0Q+&ML zsV&&*3c3$pa`qcaGbEB0*CA~Wp3%PkF?B87FV&rWNb|@GU$LB;l|;YutU*k za1hjUL_BX%G^s;BuzRi4Hl?eqC2z&ZrKh1tZDwnufG$g$LX(j!h%F5(n8D@in3lnX z(*8+3ZT6TVYRcSpM1eMeCps=Fz8q%gyM&B=a7(Vf`4k3dN$IM+`BO^_7HZq4BR|7w z+5kOJ;9_$X%-~arA@qmXSzD|+NMh--%5-9u6t(M=f%&z$<_V#Y_lzn{E$MZZG)+A> zu2E`_Y(MBJ2l*AqvCUmU;yBT}#oQ{V=((mC-QGJwsCOH*a;{1JRTKv7DBNG+M!XL7(^jbv&Qy-o9HNFrmN)-`D3WFtXs>1vBOJpI(=x; zKhJlFdfMf^G#oU(w1+ucMKYPZaDp>$kt=wiYsBCjUY-uz<4JziB>6fXDSLH*2Y z&Px5y`#3!fF=c4>fCMdg-tX582pemU@ZxyFbznL8-=TTo1Sybg9>7h*J^9^~XxXJO z`k9v~=4amxl<;FCV9h2k%?^-ZUzQy^#{JleyH23o1S{r<+t#z6jKS<9rbAM96^1iY zi6{IjauB)UwBhC-_L(MzGCxhhv`?ryc zja_Uwi7$8l!}*vjJppGyp#Wz=*?;jC*xQ&J894rql5A$2giJRtV&DWQh#(+Vs3-5_ z69_tj(>8%z1VtVp>a74r5}j2rG%&;uaTQ|fr&r%ew-HO}76i8`&ki%#)~}q4Y|d$_ zfNp9uc#$#OEca>>MaY6rF`dB|5#S)bghf>>TmmE&S~IFw;PF0UztO6+R-0!TSC?QP z{b(RA_;q3QAPW^XN?qQqu{h<}Vfiv}Rr!lA$C79^1=U>+ng9Dh>v{`?AOZt>CrQ=o zI}=mSnR))8fJpO->rcX?H);oqSQUZ?sR!fH2SoFdcPm5*2y<_u;4h;BqcF*XbwWSv zcJN%!g|L(22Xp!^1?c;T&qm%rpkP&2EQC3JF+SENm$+@7#e!UKD1uQ{TDw43?!b!3 zUooS_rt=xJfa&h?c^hfV>YwQXre3qosz_^c#)FO~d!<)2o}Oxz5HWtr<)1Yw012v4 zhv0w(RfJspDnA^-6Jmr;GkWt%{mAYOm6yPb&Vl&rv@D^K&;#?=X{kaK5FhScNJ_3> z#5u(Saisq2(~pVlrfG#@kLM#Ot~5rZZc%B&h1=gen?R+#t^1bYKf zVvtefX=D$*)39e^2@!~A_}9c${Gf0?1;dk=!Itp#s%0>Io%k`9(bDeI-udd&E6Zfu zcaiv(h`DM3W3Mfda)fYwhB=8RAPkotVt5-z21Ij~Ot9A^SK-1u*zFVK&mF?q1;|wy zrF+XWs^5Q-%Z6I62gTwrRe#F>riVM#fv_TihxSJ6to1X7NVszgivoTa!fPfBBYj94 zuc2m zL_k-<1FoORng1aL{Zx(P7JmUiH zlmTHdzkn75=mS{V=o$V;gzhEaunoJzJ3uq>0_w~77eID^U*w+v0po_N8=sS-DL~!V z%-~rL<0V7PCEWPCpNgpfsein`Fr)+8=N}mUn2x=K`z%efnhSs#23&N1fjdO`M>s%z zP3(;v93%lLq>ZfqBi#QI-aCXAP8-may8x5s`G)KA;{HSYe2szWINWf^b*fc{jl0KecD zRTle?)%_YzJJcVb>;VJ>P?3Lu2S)vCJZlF>Jxj~~X2U5-NNNy(H?8%XD~yFUxNKs&hwWx^)iF@ zGmEv<|7Q7hGrY_+`iz+d_=^9c(_c}UCzq2#%A0|5WjzCXjZUOxOX zU&-^smw$iwKPe;r`&{rP{L35^&+wk6f2-Sn;D2Ww@sjAJj{Gwbp4H!o{#5_}qALFq z{-q%LGklZvKf%A4D!+t%sRRBDi(>mvuz&V4yu^GdD*KFy?fg%ef5ZU%w=d&M`POGt zNSEJ0{qJI~FRTAjlJc1-+x>Tm{%D?m3sk-&cq#w)OpxI98wCF#2KbWcrAXK_(}M4B zF#VQf*h|irx=+uXZUMi+`A;fPFR5M%Wjs^Wh5rWCKgedhWO^w|@XS;b^&3oom;>K0 zB??|ry^IBarYem6Z7RU`#rDs-ZZAn*hSollv?csD$sh0QpTtI9vb>Dpd}e7*`fZj! zM|8d{~YM@vfW-r0z8vJ z<^6B6Ur(}L?ms_c9@hO0^Iy&J_uc51^?d33e#Y!-``?)VG)BGjCq5$&0G8A*r!2qk zUHscGc;VxE=1KqbH=dW%&Ogl({>L!>((m$2W8M9KQ@a1=h51jN|KoG{v(x0K&*iy% e1c3cF4~(n?C}6GmGu)3JNC)6=LGAhZ*Z%`+-T+_# diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a95009c3..b0acbdcd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index cccdd3d5..8e25e6c1 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/gradlew.bat b/gradlew.bat index e95643d6..24467a14 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome From 00f08a2683b7c563643e0f74b02c44d6f077a289 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 3 Aug 2019 14:41:35 +0200 Subject: [PATCH 52/62] We need JavaFX support (should be a separate library in the future) --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 57217bf2..11baa946 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'application' id 'java' id 'maven-publish' + id 'org.openjfx.javafxplugin' version '0.0.7' //id 'net.nemerosa.versioning' version '2.8.2' id 'com.jfrog.bintray' version '1.8.4' } From 2f494303975498e5c2ab7de642afab25b71774f1 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 3 Aug 2019 14:43:08 +0200 Subject: [PATCH 53/62] Compiles on JDK 11. --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 11baa946..a5c82d57 100644 --- a/build.gradle +++ b/build.gradle @@ -106,6 +106,10 @@ jar { } } +javafx { + modules = [ 'javafx.graphics', 'javafx.fxml'] +} + def pomConfig = { name 'jcsg' From a4c723e715b55a8121fd8922f533d41d23946756 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 3 Aug 2019 14:45:34 +0200 Subject: [PATCH 54/62] Jar file now has an automatic module name --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a5c82d57..c4f2d3de 100644 --- a/build.gradle +++ b/build.gradle @@ -101,7 +101,8 @@ jar { 'Specification-Title': project.name, 'Specification-Version': project.version, 'Implementation-Title': project.name, - 'Implementation-Version': project.version + 'Implementation-Version': project.version, + 'Automatic-Module-Name': eu.mihosoft.jcsg ) } } From 8026aa402141be236316f0d169785c44dd81d624 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 3 Aug 2019 15:40:51 +0200 Subject: [PATCH 55/62] we use jdk 11 and 12 for travis --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 09e0367d..99f8a710 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,10 @@ sudo: false language: java jdk: - - oraclejdk8 - - oraclejdk9 + #- oraclejdk8 + #- oraclejdk9 + - openjdk11 + - openjdk12 # - oraclejdk10 openjdk 10 is broken on travis (maybe they use the wrong download link?) before_install: From a984f246958dc6fac1d9bbf08bfa56f78e9d4764 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 3 Aug 2019 15:42:18 +0200 Subject: [PATCH 56/62] travis related fix --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 99f8a710..990acaac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,10 +8,10 @@ jdk: - openjdk12 # - oraclejdk10 openjdk 10 is broken on travis (maybe they use the wrong download link?) -before_install: - - "export DISPLAY=:99.0" - - "export TERM=dumb" - - "sh -e /etc/init.d/xvfb start" +# before_install: +# - "export DISPLAY=:99.0" +# - "export TERM=dumb" +# - "sh -e /etc/init.d/xvfb start" install: - TERM=dumb ./gradlew --refresh-dependencies --stacktrace From a6d15129c9f101dd478250a6bc1a4d375a597667 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 3 Aug 2019 15:45:09 +0200 Subject: [PATCH 57/62] fixed automatic module name --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c4f2d3de..dc7c8280 100644 --- a/build.gradle +++ b/build.gradle @@ -102,7 +102,7 @@ jar { 'Specification-Version': project.version, 'Implementation-Title': project.name, 'Implementation-Version': project.version, - 'Automatic-Module-Name': eu.mihosoft.jcsg + 'Automatic-Module-Name': "eu.mihosoft.jcsg" ) } } From ca8c2d7ccc569e4566776efbfed95b987b95e6c0 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 3 Aug 2019 15:45:41 +0200 Subject: [PATCH 58/62] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ebcdb86d..abf29950 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ To see what's possible with JCSG try [JFXScad](https://github.com/miho/JFXScad). ### Requirements -- Java >= 1.8 +- Java >= 11 - Internet connection (dependencies are downloaded automatically) - IDE: [Gradle](http://www.gradle.org/) Plugin (not necessary for command line usage) From f9b4451ebe1b73397e33a8b9ae0df7ba6572c9ec Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 3 Aug 2019 15:54:13 +0200 Subject: [PATCH 59/62] tests need more memory --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index dc7c8280..a9cef3c5 100644 --- a/build.gradle +++ b/build.gradle @@ -111,6 +111,10 @@ javafx { modules = [ 'javafx.graphics', 'javafx.fxml'] } +test { + maxHeapSize = '2G' +} + def pomConfig = { name 'jcsg' From 1645be376521af72c6976bfea666468dd548eef6 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sat, 3 Aug 2019 16:00:15 +0200 Subject: [PATCH 60/62] fix for issue #52 --- src/main/java/eu/mihosoft/jcsg/Edge.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/mihosoft/jcsg/Edge.java b/src/main/java/eu/mihosoft/jcsg/Edge.java index 71e86bf2..86dd106a 100644 --- a/src/main/java/eu/mihosoft/jcsg/Edge.java +++ b/src/main/java/eu/mihosoft/jcsg/Edge.java @@ -599,11 +599,11 @@ private static List boundaryEdgesOfPlaneGroup(List planeGroup) { Stream pStream; - if (planeGroup.size() > 200) { - pStream = planeGroup.parallelStream(); - } else { + // if (planeGroup.size() > 200) { + // pStream = planeGroup.parallelStream(); + // } else { pStream = planeGroup.stream(); - } + // } pStream.map((p) -> Edge.fromPolygon(p)).forEach((pEdges) -> { edges.addAll(pEdges); @@ -611,11 +611,11 @@ private static List boundaryEdgesOfPlaneGroup(List planeGroup) { Stream edgeStream; - if (edges.size() > 200) { - edgeStream = edges.parallelStream(); - } else { + // if (edges.size() > 200) { + // edgeStream = edges.parallelStream(); + // } else { edgeStream = edges.stream(); - } + // } // find potential boundary edges, i.e., edges that occur once (freq=1) List potentialBoundaryEdges = new ArrayList<>(); From 3b73084f17334f7bad3f23ff94bffab645abcb96 Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Sun, 31 May 2020 16:41:36 +0200 Subject: [PATCH 61/62] Fixed problem with bounds (#61). Updated gradle and playground ideas (preparing large update). --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- src/main/java/eu/mihosoft/jcsg/Bounds.java | 4 +- .../eu/mihosoft/jcsg/playground/Main.java | 2 +- .../eu/mihosoft/jcsg/playground/Main2.java | 723 ++++++++++++++++++ 5 files changed, 727 insertions(+), 6 deletions(-) create mode 100644 src/main/java/eu/mihosoft/jcsg/playground/Main2.java diff --git a/build.gradle b/build.gradle index a9cef3c5..8715a74d 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ sourceCompatibility = '11' //repairHeaders.licenseHeaderText = new File(projectDir,'./license-template.txt') wrapper { - gradleVersion = '5.5' + gradleVersion = '6.4' } if (!hasProperty('mainClass')) { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b0acbdcd..4c5803d1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/eu/mihosoft/jcsg/Bounds.java b/src/main/java/eu/mihosoft/jcsg/Bounds.java index 2f6efcb1..cbfb4b66 100644 --- a/src/main/java/eu/mihosoft/jcsg/Bounds.java +++ b/src/main/java/eu/mihosoft/jcsg/Bounds.java @@ -166,11 +166,9 @@ public boolean contains(Polygon p) { * @param p polygon to check * @return {@code true} if the polygon intersects this bounding box; * {@code false} otherwise - * @deprecated not implemented yet */ - @Deprecated public boolean intersects(Polygon p) { - throw new UnsupportedOperationException("Implementation missing!"); + return p.vertices.stream().filter(this::contains).count()>0; } /** diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main.java b/src/main/java/eu/mihosoft/jcsg/playground/Main.java index 0b122874..f67b4e5f 100644 --- a/src/main/java/eu/mihosoft/jcsg/playground/Main.java +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main.java @@ -517,7 +517,7 @@ public static void testCut() { p = pCSG.getPolygons().get(0); } catch (Exception ex) { - // + ex.printStackTrace(); } CSG cube = new Cube(Vector3d.xyz(1, 1, 1), Vector3d.xyz(2, 2, 2)).toCSG() diff --git a/src/main/java/eu/mihosoft/jcsg/playground/Main2.java b/src/main/java/eu/mihosoft/jcsg/playground/Main2.java new file mode 100644 index 00000000..a65958b6 --- /dev/null +++ b/src/main/java/eu/mihosoft/jcsg/playground/Main2.java @@ -0,0 +1,723 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package eu.mihosoft.jcsg.playground; + +import eu.mihosoft.jcsg.*; +import eu.mihosoft.vvecmath.ModifiableVector3d; +import eu.mihosoft.vvecmath.Plane; +import eu.mihosoft.vvecmath.Transform; +import eu.mihosoft.vvecmath.Vector3d; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * @author Michael Hoffer (info@michaelhoffer.de) + */ +public class Main2 { + + public static final double EPS = 1e-8; + // s3 = s1 - s2 + + // # Step 1: + // + // Take all polygons of s2 that have vertices inside the bounding box b 1of s1 + // (polygons with only some vertices inside of b are cut along the planes of b1) + // All polygons outside of b1 are ignored from now on. + // + // Remark: collinear polygons are considered as being outside of b1. + // + // # Step 2: + // + // Cut all remaining polygons of s2 with the polygons of s1. Only keep the polygons with all vertices + // inside of b1. + // + // # Step 3: + // + // a) For each remaining polygon p of s2: cast an orthogonal ray from the center of p (normal) and + // count the number of intersecting polygons of s1. If the ray hits a vertex of p or cuts the + // boundary, it counts as being intersected by the ray. + // + // b) Classify the resulting polygon by whether the number of intersections is even or uneven number + // of intersections. An uneven number of intersections indicates that the polygon is inside of s1; + // An even number of intersections indicates that the polygon is outside of s2. + // + // + // # Step 5: + // + // Repeat these steps (1-4) with s1 and s2 reversed. + // + // # Step 6: + // + // s3 consists of all polygons of s1 classified as being outside of s2 and all polygons of s2 being + // classified as being inside of s1. + // + // _________ + // / /*\ \ + // | |*| | * = intersection + // \___\*/___/ + // + // _________ ____ + // / / \ \ / / + // | | | | === | | + // \___\ /___/ \___\ + public static void main(String[] args) throws IOException { + + CSG s1 = new Cube(2).toCSG(); + CSG s2 = new Sphere(Vector3d.x(0.0), 1.25, 32, 32).toCSG(); + + Files.write(Paths.get("diff-orig.stl"), s2.difference(s1).toStlString().getBytes()); + + Classification classification = classify(s1,s2); + + List polygons = new ArrayList<>(); + + polygons.addAll(classification.outsideS1); +// polygons.addAll(classification.outsideS2); +// polygons.addAll(classification.insideS1); + polygons.addAll(classification.insideS2); + + CSG difference = CSG.fromPolygons(polygons); + + Files.write(Paths.get("diff.stl"), difference.toStlString().getBytes()); + } + + static class Classification { + public List insideS1; + public List outsideS1; + public List insideS2; + public List outsideS2; + } + + static class Classification1 { + public List inside; + public List outside; + } + + public static Classification classify(CSG s1, CSG s2) { + Classification result = new Classification(); + + Classification1 r1 = classify1(s1,s2); + Classification1 r2 = classify1(s2,s1); + + result.insideS1 = r1.inside; + result.outsideS1 = r1.outside; + result.insideS2 = r2.inside; + result.outsideS2 = r2.outside; + + return result; + } + + public static Classification1 classify1(CSG s1, CSG s2) { + + // step 1 + + // get polygons inside of b + Bounds b1 = s1.getBounds(); + Bounds b2 = s2.getBounds(); + List ps1 = s1.getPolygons(); + List ps2 = s2.getPolygons();//.stream().filter(p->b1.intersects(p)).collect(Collectors.toList()); + + // step 2 + + // cut polygons + ps2 = splitPolygons(ps1, ps2, b1, b2);//.stream().filter(p->p.vertices.stream().filter(v->b1.contains(v)).count()==p.vertices.size()).collect(Collectors.toList()); + + // step 3 + + double TOL = 1e-10; + + Map> polygons = ps2.parallelStream().collect(Collectors.partitioningBy(p-> { + return classifyPolygon(p, ps1, b1) == PolygonType.OUTSIDE; + })); + + List inside = polygons.get(false); + List outside = polygons.get(true); + + Classification1 result = new Classification1(); + result.inside = inside; + result.outside = outside; + + return result; + } + + + + public static PolygonType classifyPolygon(Polygon p1, List polygons, Bounds b) { + + double TOL = 1e-10; + + // we are definitely outside if bounding boxes don't intersect + if (!p1.getBounds().intersects(b)) { + return PolygonType.OUTSIDE; + } + + Vector3d rayCenter = p1.centroid(); + Vector3d rayDirection = p1.getPlane().getNormal(); + + List intersections = getPolygonsThatIntersectWithRay( + rayCenter, rayDirection, polygons, TOL); + + if (intersections.isEmpty()) { + return PolygonType.OUTSIDE; + } + + // find the closest polygon to the centroid of p1 which intersects the + // ray + RayIntersection min = null; //intersections.get(0); + double dist = 0; + double prevDist = Double.MAX_VALUE; // min.polygon.centroid().minus(rayCenter).magnitude(); + int i = 0; + for (RayIntersection ri : intersections) { + + int frontOrBack = p1.getPlane().compare(ri.intersectionPoint, TOL); + + if (frontOrBack < 0) { + // System.out.println(" -> skipping intersection behind ray " + i); + continue; + } + + //try { + // ObjFile objF = CSG.fromPolygons(ri.polygon).toObj(3); + // objF.toFiles(Paths.get("test-intersection-" + i + ".obj")); + //} catch (IOException ex) { + // Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + //} + + dist = ri.polygon.centroid().minus(rayCenter).magnitude(); + + //System.out.println("dist-"+i+": " + dist); + + if (dist < TOL && ri.polygon.getPlane().getNormal().dot(rayDirection) < TOL) { + // System.out.println(" -> skipping intersection " + i); + continue; + } + + if (dist < prevDist) { + prevDist = dist; + min = ri; + } + + i++; + } + + if (min == null) { + return PolygonType.OUTSIDE; + } + + // try { + // ObjFile objF = CSG.fromPolygons(min.polygon).toObj(); + // objF.toFiles(Paths.get("test-intersection-min.obj")); + //} catch (IOException ex) { + // Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + //} + + int frontOrBack = p1.getPlane().compare(min.intersectionPoint, TOL); + + Vector3d planePoint = p1.getPlane().getAnchor(); + + int sameOrOpposite = p1.getPlane().compare( + planePoint.plus(min.polygon.getPlane().getNormal()), TOL + ); + + if (frontOrBack > 0 && sameOrOpposite > 0) { + return PolygonType.INSIDE; + } + + if (frontOrBack > 0 && sameOrOpposite < 0) { + return PolygonType.OUTSIDE; + } + + if (frontOrBack < 0 && sameOrOpposite < 0) { + return PolygonType.INSIDE; + } + + if (frontOrBack < 0 && sameOrOpposite > 0) { + return PolygonType.OUTSIDE; + } + + if (frontOrBack == 0 && sameOrOpposite > 0) { + return PolygonType.SAME; + } + + if (frontOrBack == 0 && sameOrOpposite < 0) { + return PolygonType.OPPOSITE; + } + + System.err.println("I need help (2) !"); + + return PolygonType.UNKNOWN; + } + + public static final class PlaneIntersection { + + public final IntersectionType type; + public Optional point; + + public PlaneIntersection( + IntersectionType type, Optional point) { + this.type = type; + this.point = point; + } + + public static enum IntersectionType { + ON, + PARALLEL, + NON_PARALLEL + } + } + + public static final class RayIntersection { + + public final Vector3d intersectionPoint; + public final Polygon polygon; + public final PlaneIntersection.IntersectionType type; + + public RayIntersection(Vector3d intersectionPoint, + Polygon polygon, PlaneIntersection.IntersectionType type) { + this.intersectionPoint = intersectionPoint; + this.polygon = polygon; + this.type = type; + } + + + @Override + public String toString() { + return "" + + "[\n" + + " -> point: " + intersectionPoint + "\n" + + " -> polygon-normal: " + polygon.getPlane().getNormal() + "\n" + + " -> type: " + type + "\n" + + "]"; + } + + } + + public static List getPolygonsThatIntersectWithRay( + Vector3d point, Vector3d direction, List polygons, double TOL) { + List intersection = new ArrayList<>(); + for (Polygon p : polygons) { + PlaneIntersection res = computePlaneIntersection(p.getPlane(), point, direction, TOL); + if (res.point.isPresent()) { + if (p.contains(res.point.get())) { + intersection.add(new RayIntersection(res.point.get(), p, res.type)); + } + } + } + + return intersection; + } + + public static PlaneIntersection computePlaneIntersection( + Plane plane, Vector3d point, Vector3d direction, double TOL) { + + //Ax + By + Cz + D = 0 + //x = x0 + t(x1 x0) + //y = y0 + t(y1 y0) + //z = z0 + t(z1 z0) + //(x1 - x0) = dx, (y1 - y0) = dy, (z1 - z0) = dz + //t = -(A*x0 + B*y0 + C*z0 )/(A*dx + B*dy + C*dz) + Vector3d normal = plane.getNormal(); + Vector3d planePoint = plane.getAnchor(); + + double A = normal.x(); + double B = normal.y(); + double C = normal.z(); + double D = -(normal.x() * planePoint.x() + normal.y() * planePoint.y() + normal.z() * planePoint.z()); + + double numerator = A * point.x() + B * point.y() + C * point.z() + D; + double denominator = A * direction.x() + B * direction.y() + C * direction.z(); + + //if line is parallel to the plane... + if (Math.abs(denominator) < TOL) { + //if line is contained in the plane... + if (Math.abs(numerator) < TOL) { + return new PlaneIntersection( + PlaneIntersection.IntersectionType.ON, + Optional.of(point)); + } else { + return new PlaneIntersection( + PlaneIntersection.IntersectionType.PARALLEL, + Optional.empty()); + } + } //if line intercepts the plane... + else { + double t = -numerator / denominator; + Vector3d resultPoint = Vector3d.xyz( + point.x() + t * direction.x(), + point.y() + t * direction.y(), + point.z() + t * direction.z()); + + return new PlaneIntersection( + PlaneIntersection.IntersectionType.NON_PARALLEL, + Optional.of(resultPoint)); + } + } + + /** + * Splits polygons ps2 with planes from polygons ps1. + * + * @param ps1 + * @param ps2 + * @param b1 + * @param b2 + * @return + */ + public static List splitPolygons( + List ps1, + List ps2, + Bounds b1, Bounds b2) { + + System.out.println("#ps1: " + ps1.size() + ", #ps2: " + ps2.size()); + + if (ps1.isEmpty() || ps2.isEmpty()) return Collections.EMPTY_LIST; + + List ps2WithCuts = new ArrayList<>(ps2); + + for (Polygon p1 : ps1) { + + // return early if polygon bounds do not intersect object bounds + if (!p1.getBounds().intersects(b2)) { + continue; + } + + List cutsWithP1 = new ArrayList<>(); + List p2ToDelete = new ArrayList<>(); + for (Polygon p2 : ps2WithCuts) { + + // return early if polygon bounds do not intersect other polygon bound + if (!p1.getBounds().intersects(p2.getBounds())) { + continue; + } + + List cutsOfP2WithP1 = cutPolygonWithPlaneIf(p2, p1.getPlane(), + (Predicate>) segments -> { + + //if(true)return true; + if(segments.size()!=2) return true; + + Vector3d s1 = segments.get(0); + Vector3d s2 = segments.get(1); + + int numIntersectionsPoly1 = 0; + for(int i = 0; i< p1.vertices.size()-1;i++) { + //System.out.println("i,j : " + i + ", " + (i+1%p1.vertices.size())); + Vector3d e1 = p1.vertices.get(i).pos; + Vector3d e2 = p1.vertices.get(i+1%p1.vertices.size()).pos; + LineIntersectionResult iRes = calculateLineLineIntersection(e1,e2,s1,s2); + if(iRes.type == LineIntersectionResult.IntersectionType.INTERSECTING && + p1.contains(iRes.segmentPoint1.get())) { + numIntersectionsPoly1++; + } + } + + int numIntersectionsPoly2 = 0; + for(int i = 0; i< p2.vertices.size()-1;i++) { + Vector3d e1 = p2.vertices.get(i).pos; + Vector3d e2 = p2.vertices.get(i+1%p2.vertices.size()).pos; + LineIntersectionResult iRes = calculateLineLineIntersection(e1,e2,s1,s2); + if(iRes.type == LineIntersectionResult.IntersectionType.INTERSECTING && + p2.contains(iRes.segmentPoint1.get())) { + numIntersectionsPoly2++; + } + } + + return numIntersectionsPoly1 > 0 && numIntersectionsPoly2 > 0; + }); + + if (!cutsOfP2WithP1.isEmpty()) { + cutsWithP1.addAll(cutsOfP2WithP1); + p2ToDelete.add(p2); + } + } + ps2WithCuts.addAll(cutsWithP1); + ps2WithCuts.removeAll(p2ToDelete); + } + + return ps2WithCuts; + } + + + private static void cutPolygonWithPlaneAndTypes(Polygon polygon, Plane cutPlane, + int[] vertexTypes, List frontPolygon, + List backPolygon, List onPlane) { + +// System.out.println("polygon: \n" + polygon.toStlString()); +// System.out.println("--------------------"); +// System.out.println("plane: \n -> p: " + cutPlane.getAnchor() + "\n -> n: " + cutPlane.getNormal()); +// System.out.println("--------------------"); + for (int i = 0; i < polygon.vertices.size(); i++) { + int j = (i + 1) % polygon.vertices.size(); + int ti = vertexTypes[i]; + int tj = vertexTypes[j]; + Vertex vi = polygon.vertices.get(i); + Vertex vj = polygon.vertices.get(j); + if (ti == 1 /*front*/) { + frontPolygon.add(vi.pos); + } + if (ti == -1 /*back*/) { + backPolygon.add(vi.pos); + } + + if (ti == 0) { + frontPolygon.add(vi.pos); + backPolygon.add(vi.pos); +// segmentPoints.add(vi.pos); + } + + if (ti != tj && (ti != 0 && tj != 0)/*spanning*/) { + PlaneIntersection pI = computePlaneIntersection(cutPlane, vi.pos, vj.pos.minus(vi.pos), EPS); + + if (pI.type != PlaneIntersection.IntersectionType.NON_PARALLEL) { + throw new RuntimeException("I need help (3)!"); + } + + Vector3d intersectionPoint = pI.point.get(); + + frontPolygon.add(intersectionPoint); + backPolygon.add(intersectionPoint); + onPlane.add(intersectionPoint); + } + } + } + + public static void testCut() { + + Polygon p = Polygon.fromPoints( + Vector3d.xyz(0, 0, 0), + Vector3d.xyz(1, 0, 0), + Vector3d.xyz(1, 0, 1), + Vector3d.xyz(0, 0, 1) + ); + + try { + CSG pCSG = STL.file(Paths.get("sphere-test-01.stl")); + + p = pCSG.getPolygons().get(0); + } catch (Exception ex) { + ex.printStackTrace(); + } + + CSG cube = new Cube(Vector3d.xyz(1, 1, 1), Vector3d.xyz(2, 2, 2)).toCSG() + .transformed(Transform.unity().rot(Vector3d.ZERO, Vector3d.UNITY, 17)); + + cube = new Sphere(Vector3d.x(0.), 0.5, 16, 16).toCSG(); + +// CSG cube = new Cube(1).toCSG().transformed( +// Transform.unity().translate(0.5,-0.55,0.5).rot(Vector3d.ZERO, Vector3d.UNITY, 0) +// ); + + int cubePolyFrom = 0; + int cubePolyTo = 6; + + List cubePolys = cube.getPolygons();//.subList(cubePolyFrom, cubePolyTo); + + System.out.println("p: " + p.toStlString()); + System.out.println("p-centroid: " + p.centroid()); + + List intersections = + getPolygonsThatIntersectWithRay( + p.centroid(), + p.getPlane().getNormal(), + cubePolys, EPS); + + System.out.println("my normal: " + p.getPlane().getNormal()); + + System.out.println("#intersections: " + intersections.size()); + for (RayIntersection ri : intersections) { + System.out.println(ri); + } + + PolygonType pType = classifyPolygon(p, cubePolys, cube.getBounds()); + + System.out.println("#pType:"); + System.out.println(" -> " + pType); + + List cutsWithCube = splitPolygons(cubePolys, + Arrays.asList(p), p.getBounds(), cube.getBounds()); + + cutsWithCube.addAll(cube.getPolygons()/*.subList(cubePolyFrom, cubePolyTo)*/); + + try { + ObjFile objF = CSG.fromPolygons(cutsWithCube).toObj(3); + objF.toFiles(Paths.get("test-split1.obj")); +// Files.write(Paths.get("test-split1.stl"), +// CSG.fromPolygons(cutsWithP1).toStlString().getBytes()); + } catch (IOException ex) { + Logger.getLogger(Main2.class.getName()).log(Level.SEVERE, null, ex); + } + + ModifiableVector3d segmentP1 = Vector3d.zero().asModifiable(); + ModifiableVector3d segmentP2 = Vector3d.zero().asModifiable(); + LineIntersectionResult lineRes = calculateLineLineIntersection( + Vector3d.xyz(-1, 0, 0), Vector3d.xyz(1, 0, 0), + Vector3d.xyz(0, -1, 0), Vector3d.xyz(0, 1, 0)); + + System.out.println("l1 intersect l2: "); + System.out.println(lineRes); + + // System.exit(0); + } + + private static List cutPolygonWithPlaneIf(Polygon p, Plane plane, Predicate> check) { + + boolean typesEqual = true; + int types[] = new int[p.vertices.size()]; + for (int i = 0; i < p.vertices.size(); i++) { + types[i] = plane.compare(p.vertices.get(i).pos, EPS); +// System.out.println("type " + i + ": " + types[i]); + + if (i > 0 && typesEqual) { + typesEqual = typesEqual && (types[i] == types[i - 1]); + } + } + + // planes are parallel, thus polygons do not intersect + if (typesEqual) { + return Collections.EMPTY_LIST; + } + + List front = new ArrayList<>(); + List back = new ArrayList<>(); + List on = new ArrayList<>(); + cutPolygonWithPlaneAndTypes(p, plane, types, front, back, on); + + boolean checkResult = check == null; + + if (check != null) { + checkResult = check.test(on); + } + + if (!checkResult) return Collections.EMPTY_LIST; + + List cutsWithP1 = new ArrayList<>(); + if (front.size() > 2) { + Polygon frontCut = Polygon.fromPoints( + front); + if (frontCut.isValid()) { + cutsWithP1.add(frontCut); + } + } + if (back.size() > 2) { + Polygon backCut = Polygon.fromPoints( + back); + if (backCut.isValid()) { + cutsWithP1.add(backCut); + } + } + return cutsWithP1; + } + + enum PolygonType { + UNKNOWN, + INSIDE, + OUTSIDE, + OPPOSITE, + SAME + } + + + static class LineIntersectionResult { + + public final IntersectionType type; + + public final Optional segmentPoint1; + public final Optional segmentPoint2; + + LineIntersectionResult(IntersectionType type, Vector3d segmentPoint1, Vector3d segmentPoint2) { + this.type = type; + this.segmentPoint1 = Optional.ofNullable(segmentPoint1); + this.segmentPoint2 = Optional.ofNullable(segmentPoint2); + } + + static enum IntersectionType { + PARALLEL, + NON_PARALLEL, + INTERSECTING + } + + static final LineIntersectionResult PARALLEL = + new LineIntersectionResult(IntersectionType.PARALLEL, null, null); + + @Override + public String toString() { + return "[\n -> type: " + type + + "\n -> segmentP1: " + (segmentPoint1.isPresent() ? segmentPoint1.get() : "none") + + "\n -> segmentP2: " + (segmentPoint2.isPresent() ? segmentPoint2.get() : "none") + + "\n]"; + } + } + + /** + * Calculates the intersection line segment between two lines. + * + * @param line1Point1 + * @param line1Point2 + * @param line2Point1 + * @param line2Point2 + * @return {@code true} if the intersection line segment exists; {@code false} otherwise + */ + public static LineIntersectionResult calculateLineLineIntersection(Vector3d line1Point1, Vector3d line1Point2, + Vector3d line2Point1, Vector3d line2Point2) { + // Algorithm is ported from the C algorithm of + // Paul Bourke at http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline3d/ + + Vector3d p1 = line1Point1; + Vector3d p2 = line1Point2; + Vector3d p3 = line2Point1; + Vector3d p4 = line2Point2; + Vector3d p13 = p1.minus(p3); + Vector3d p43 = p4.minus(p3); + + if (p43.magnitudeSq() < EPS) { + return LineIntersectionResult.PARALLEL; + } + Vector3d p21 = p2.minus(p1); + if (p21.magnitudeSq() < EPS) { + return LineIntersectionResult.PARALLEL; + } + + double d1343 = p13.x() * (double) p43.x() + (double) p13.y() * p43.y() + (double) p13.z() * p43.z(); + double d4321 = p43.x() * (double) p21.x() + (double) p43.y() * p21.y() + (double) p43.z() * p21.z(); + double d1321 = p13.x() * (double) p21.x() + (double) p13.y() * p21.y() + (double) p13.z() * p21.z(); + double d4343 = p43.x() * (double) p43.x() + (double) p43.y() * p43.y() + (double) p43.z() * p43.z(); + double d2121 = p21.x() * (double) p21.x() + (double) p21.y() * p21.y() + (double) p21.z() * p21.z(); + + double denom = d2121 * d4343 - d4321 * d4321; + if (Math.abs(denom) < EPS) { + return LineIntersectionResult.PARALLEL; + } + double numer = d1343 * d4321 - d1321 * d4343; + + double mua = numer / denom; + double mub = (d1343 + d4321 * (mua)) / d4343; + + ModifiableVector3d resultSegmentPoint1 = Vector3d.zero().asModifiable(); + ModifiableVector3d resultSegmentPoint2 = Vector3d.zero().asModifiable(); + + resultSegmentPoint1.setX(p1.x() + mua * p21.x()); + resultSegmentPoint1.setY(p1.y() + mua * p21.y()); + resultSegmentPoint1.setZ(p1.z() + mua * p21.z()); + resultSegmentPoint2.setX(p3.x() + mub * p43.x()); + resultSegmentPoint2.setY(p3.y() + mub * p43.y()); + resultSegmentPoint2.setZ(p3.z() + mub * p43.z()); + + if (resultSegmentPoint1.equals(resultSegmentPoint2)) { + return new LineIntersectionResult(LineIntersectionResult.IntersectionType.INTERSECTING, + resultSegmentPoint1, resultSegmentPoint2); + } else { + return new LineIntersectionResult(LineIntersectionResult.IntersectionType.NON_PARALLEL, + resultSegmentPoint1, resultSegmentPoint2); + } + } + +} From ab42ebc9252f40f5a3faafff3303e0cd94eb1b3a Mon Sep 17 00:00:00 2001 From: Michael Hoffer Date: Fri, 3 Sep 2021 18:37:08 +0200 Subject: [PATCH 62/62] Update README.md --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index abf29950..d9df0ebe 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ JCSG ======= -[![Build Status](https://travis-ci.org/miho/JCSG.png?branch=master)](https://travis-ci.org/miho/JCSG) [ ![Download](https://api.bintray.com/packages/miho/JCSG/jcsg/images/download.svg) ](https://bintray.com/miho/JCSG/jcsg/_latestVersion) [![Javadocs](https://www.javadoc.io/badge/eu.mihosoft.vrl.jcsg/jcsg.svg)](https://www.javadoc.io/doc/eu.mihosoft.vrl.jcsg/jcsg) - +[![Build Status](https://travis-ci.org/miho/JCSG.png?branch=master)](https://travis-ci.org/miho/JCSG) +[![Javadocs](https://www.javadoc.io/badge/eu.mihosoft.vrl.jcsg/jcsg.svg)](https://www.javadoc.io/doc/eu.mihosoft.vrl.jcsg/jcsg) + + + + +
Java implementation of BSP based CSG (Constructive Solid Geometry). It is the only simple and free Java implementation I am aware of. This implementation uses an optimized CSG algorithm based on [csg.js](https://github.com/evanw/csg.js) (see `CSG` and `Node` classes). Thanks to the author for creating the [csg.js](https://github.com/evanw/csg.js) library.