
eu.mihosoft.vrl.v3d.Edge Maven / Gradle / Ivy
/**
* Edge.java
*
* Copyright 2014-2016 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.vrl.v3d;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import eu.mihosoft.vrl.v3d.ext.org.poly2tri.PolygonUtil;
/**
*
* @author miho
*/
public class Edge {
private final Vertex p1;
private final Vertex p2;
private final Vector3d direction;
public Edge(Vertex p1, Vertex p2) {
this.p1 = p1;
this.p2 = p2;
direction = p2.pos.minus(p1.pos).normalized();
}
/**
* @return the p1
*/
public Vertex getP1() {
return p1;
}
// /**
// * @param p1 the p1 to set
// */
// public void setP1(Vertex p1) {
// this.p1 = p1;
// }
/**
* @return the p2
*/
public Vertex getP2() {
return p2;
}
// /**
// * @param p2 the p2 to set
// */
// public void setP2(Vertex p2) {
// this.p2 = p2;
// }
public static List fromPolygon(Polygon poly) {
List result = new ArrayList<>();
for (int i = 0; i < poly.vertices.size(); i++) {
Edge e = new Edge(poly.vertices.get(i), poly.vertices.get((i + 1) % poly.vertices.size()));
result.add(e);
}
return result;
}
public static List toVertices(List edges) {
return edges.stream().map(e -> e.p1).collect(Collectors.toList());
}
public static List toPoints(List edges) {
return edges.stream().map(e -> e.p1.pos).collect(Collectors.toList());
}
private static Polygon toPolygon(List points, Plane plane) {
// List points = edges.stream().().map(e -> e.p1.pos).
// collect(Collectors.toList());
Polygon p = Polygon.fromPoints(points);
p.vertices.stream().forEachOrdered((vertex) -> {
vertex.normal = plane.normal.clone();
});
// // we try to detect wrong orientation by comparing normals
// if (p.plane.normal.angle(plane.normal) > 0.1) {
// p.flip();
// }
return p;
}
public static List toPolygons(List boundaryEdges, Plane plane) {
List boundaryPath = new ArrayList<>();
boolean[] used = new boolean[boundaryEdges.size()];
Edge edge = boundaryEdges.get(0);
used[0] = true;
while (true) {
Edge finalEdge = edge;
boundaryPath.add(finalEdge.p1.pos);
int nextEdgeIndex = boundaryEdges.indexOf(boundaryEdges.stream().
filter(e -> finalEdge.p2.equals(e.p1)).findFirst().get());
if (used[nextEdgeIndex]) {
// System.out.println("nexIndex: " + nextEdgeIndex);
break;
}
// System.out.print("edge: " + edge.p2.pos);
edge = boundaryEdges.get(nextEdgeIndex);
// System.out.println("-> edge: " + edge.p1.pos);
used[nextEdgeIndex] = true;
}
List result = new ArrayList<>();
System.out.println("#bnd-path-length: " + boundaryPath.size());
result.add(toPolygon(boundaryPath, plane));
return result;
}
private static class Node {
private Node parent;
private final List children = new ArrayList<>();
private final int index;
private final T value;
private boolean isHole;
public Node(int index, T value) {
this.index = index;
this.value = value;
}
public void addChild(int index, T value) {
children.add(new Node(index, value));
}
public List getChildren() {
return this.children;
}
/**
* @return the parent
*/
public Node getParent() {
return parent;
}
/**
* @return the index
*/
public int getIndex() {
return index;
}
/**
* @return the value
*/
public T getValue() {
return value;
}
@Override
public int hashCode() {
int hash = 7;
hash = 67 * hash + this.index;
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Node> other = (Node>) obj;
if (this.index != other.index) {
return false;
}
return true;
}
public int distanceToRoot() {
int dist = 0;
Node pNode = getParent();
while (pNode != null) {
dist++;
pNode = getParent();
}
return dist;
}
/**
* @return the isHole
*/
public boolean isIsHole() {
return isHole;
}
/**
* @param isHole the isHole to set
*/
public void setIsHole(boolean isHole) {
this.isHole = isHole;
}
}
public static final String KEY_POLYGON_HOLES = "jcsg:edge:polygon-holes";
public static List boundaryPathsWithHoles(List boundaryPaths) {
List result = boundaryPaths.stream().
map(p -> p.clone()).collect(Collectors.toList());
List> parents = new ArrayList<>();
boolean[] isHole = new boolean[result.size()];
for (int i = 0; i < result.size(); i++) {
Polygon p1 = result.get(i);
List parentsOfI = new ArrayList<>();
parents.add(parentsOfI);
for (int j = 0; j < result.size(); j++) {
Polygon p2 = result.get(j);
if (i != j) {
if (p2.contains(p1)) {
parentsOfI.add(j);
}
}
}
isHole[i] = parentsOfI.size() % 2 != 0;
}
int[] parent = new int[result.size()];
for (int i = 0; i < parent.length; i++) {
parent[i] = -1;
}
for (int i = 0; i < parents.size(); i++) {
List par = parents.get(i);
int max = 0;
int maxIndex = 0;
for (int pIndex : par) {
int pSize = parents.get(pIndex).size();
if (max < pSize) {
max = pSize;
maxIndex = pIndex;
}
}
parent[i] = maxIndex;
if (!isHole[maxIndex] && isHole[i]) {
List holes;
Optional> holesOpt = result.get(maxIndex).
getStorage().getValue(KEY_POLYGON_HOLES);
if (holesOpt.isPresent()) {
holes = holesOpt.get();
} else {
holes = new ArrayList<>();
result.get(maxIndex).getStorage().
set(KEY_POLYGON_HOLES, holes);
}
holes.add(result.get(i));
}
}
return result;
}
/**
* Returns a list of all boundary paths.
*
* @param boundaryEdges boundary edges (all paths must be closed)
* @return
*/
private static List boundaryPaths(List boundaryEdges) {
List result = new ArrayList<>();
boolean[] used = new boolean[boundaryEdges.size()];
int startIndex = 0;
Edge edge = boundaryEdges.get(startIndex);
used[startIndex] = true;
startIndex = 1;
while (startIndex > 0) {
List boundaryPath = new ArrayList<>();
while (true) {
Edge finalEdge = edge;
boundaryPath.add(finalEdge.p1.pos);
System.out.print("edge: " + edge.p2.pos);
Optional nextEdgeResult = boundaryEdges.stream().
filter(e -> finalEdge.p2.equals(e.p1)).findFirst();
if (!nextEdgeResult.isPresent()) {
System.out.println("ERROR: unclosed path:"
+ " no edge found with " + finalEdge.p2);
break;
}
Edge nextEdge = nextEdgeResult.get();
int nextEdgeIndex = boundaryEdges.indexOf(nextEdge);
if (used[nextEdgeIndex]) {
break;
}
edge = nextEdge;
System.out.println("-> edge: " + edge.p1.pos);
used[nextEdgeIndex] = true;
}
if (boundaryPath.size() < 3) {
break;
}
result.add(Polygon.fromPoints(boundaryPath));
startIndex = nextUnused(used);
if (startIndex > 0) {
edge = boundaryEdges.get(startIndex);
used[startIndex] = true;
}
}
System.out.println("paths: " + result.size());
return result;
}
/**
* Returns the next unused index as specified in the given boolean array.
*
* @param usage the usage array
* @return the next unused index or a value < 0 if all indices are used
*/
private static int nextUnused(boolean[] usage) {
for (int i = 0; i < usage.length; i++) {
if (usage[i] == false) {
return i;
}
}
return -1;
}
public static List _toPolygons(List boundaryEdges, Plane plane) {
List boundaryPath = new ArrayList<>();
boolean[] used = new boolean[boundaryEdges.size()];
Edge edge = boundaryEdges.get(0);
used[0] = true;
while (true) {
Edge finalEdge = edge;
boundaryPath.add(finalEdge.p1.pos);
int nextEdgeIndex = boundaryEdges.indexOf(boundaryEdges.stream().
filter(e -> finalEdge.p2.equals(e.p1)).findFirst().get());
if (used[nextEdgeIndex]) {
// System.out.println("nexIndex: " + nextEdgeIndex);
break;
}
// System.out.print("edge: " + edge.p2.pos);
edge = boundaryEdges.get(nextEdgeIndex);
// System.out.println("-> edge: " + edge.p1.pos);
used[nextEdgeIndex] = true;
}
List result = new ArrayList<>();
System.out.println("#bnd-path-length: " + boundaryPath.size());
result.add(toPolygon(boundaryPath, plane));
return result;
}
/**
* Determines whether the specified point lies on tthis edge.
*
* @param p point to check
* @param TOL tolerance
* @return true
if the specified point lies on this line
* segment; false
otherwise
*/
public boolean contains(Vector3d p, double TOL) {
double x = p.x;
double x1 = this.p1.pos.x;
double x2 = this.p2.pos.x;
double y = p.y;
double y1 = this.p1.pos.y;
double y2 = this.p2.pos.y;
double z = p.z;
double z1 = this.p1.pos.z;
double z2 = this.p2.pos.z;
double AB = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) + (z2 - z1) * (z2 - z1));
double AP = Math.sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1) + (z - z1) * (z - z1));
double PB = Math.sqrt((x2 - x) * (x2 - x) + (y2 - y) * (y2 - y) + (z2 - z) * (z2 - z));
return Math.abs(AB - (AP + PB)) < TOL;
}
/**
* Determines whether the specified point lies on tthis edge.
*
* @param p point to check
* @return true
if the specified point lies on this line
* segment; false
otherwise
*/
public boolean contains(Vector3d p) {
return contains(p, Plane.EPSILON);
}
@Override
public int hashCode() {
int hash = 7;
hash = 71 * hash + Objects.hashCode(this.p1);
hash = 71 * hash + Objects.hashCode(this.p2);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Edge other = (Edge) obj;
if (!(Objects.equals(this.p1, other.p1) || Objects.equals(this.p2, other.p1))) {
return false;
}
if (!(Objects.equals(this.p2, other.p2) || Objects.equals(this.p1, other.p2))) {
return false;
}
return true;
}
public Vector3d getDirection() {
return direction;
}
/**
* Returns the the point of this edge that is closest to the specified edge.
*
* NOTE: returns an empty optional if the edges are parallel
*
* @param e the edge to check
* @return the the point of this edge that is closest to the specified edge
*/
public Optional getClosestPoint(Edge e) {
// algorithm from:
// org.apache.commons.math3.geometry.euclidean.threed/Line.java.html
Vector3d ourDir = getDirection();
double cos = ourDir.dot(e.getDirection());
double n = 1 - cos * cos;
if (n < Plane.EPSILON) {
// the lines are parallel
return Optional.empty();
}
final Vector3d thisDelta = p2.pos.minus(p1.pos);
final double norm2This = thisDelta.magnitudeSq();
final Vector3d eDelta = e.p2.pos.minus(e.p1.pos);
final double norm2E = eDelta.magnitudeSq();
// line points above the origin
Vector3d thisZero = p1.pos.plus(thisDelta.times(-p1.pos.dot(thisDelta) / norm2This));
Vector3d eZero = e.p1.pos.plus(eDelta.times(-e.p1.pos.dot(eDelta) / norm2E));
final Vector3d delta0 = eZero.minus(thisZero);
final double a = delta0.dot(direction);
final double b = delta0.dot(e.direction);
Vector3d closestP = thisZero.plus(direction.times((a - b * cos) / n));
if (!contains(closestP)) {
if (closestP.minus(p1.pos).magnitudeSq()
< closestP.minus(p2.pos).magnitudeSq()) {
return Optional.of(p1.pos);
} else {
return Optional.of(p2.pos);
}
}
return Optional.of(closestP);
}
/**
* Returns the intersection point between this edge and the specified edge.
*
* NOTE: returns an empty optional if the edges are parallel or if
* the intersection point is not inside the specified edge segment
*
* @param e edge to intersect
* @return the intersection point between this edge and the specified edge
*/
public Optional getIntersection(Edge e) {
Optional closestPOpt = getClosestPoint(e);
if (!closestPOpt.isPresent()) {
// edges are parallel
return Optional.empty();
}
Vector3d closestP = closestPOpt.get();
if (e.contains(closestP)) {
return closestPOpt;
} else {
// intersection point outside of segment
return Optional.empty();
}
}
public static List boundaryPolygons(CSG csg) {
List result = new ArrayList<>();
for (List polygonGroup : searchPlaneGroups(csg.getPolygons())) {
result.addAll(boundaryPolygonsOfPlaneGroup(polygonGroup));
}
return result;
}
private static List boundaryEdgesOfPlaneGroup(List planeGroup) {
List edges = new ArrayList<>();
Stream pStream;
if (planeGroup.size() > 200) {
pStream = planeGroup.parallelStream();
} else {
pStream = planeGroup.stream();
}
pStream.map((p) -> Edge.fromPolygon(p)).forEach((pEdges) -> {
edges.addAll(pEdges);
});
Stream edgeStream;
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<>();
edgeStream.forEachOrdered((e) -> {
int count = Collections.frequency(edges, e);
if (count == 1) {
potentialBoundaryEdges.add(e);
}
});
// now find "false boundary" edges end remove them from the
// boundary-edge-list
//
// thanks to Susanne Höllbacher for the idea :)
Stream bndEdgeStream;
if (potentialBoundaryEdges.size() > 200) {
bndEdgeStream = potentialBoundaryEdges.parallelStream();
} else {
bndEdgeStream = potentialBoundaryEdges.stream();
}
List realBndEdges = bndEdgeStream.
filter(be -> edges.stream().filter(
e -> falseBoundaryEdgeSharedWithOtherEdge(be, e)
).count() == 0).collect(Collectors.toList());
//
// System.out.println("#bnd-edges: " + realBndEdges.size()
// + ",#edges: " + edges.size()
// + ", #del-bnd-edges: " + (boundaryEdges.size() - realBndEdges.size()));
return realBndEdges;
}
private static List boundaryPolygonsOfPlaneGroup(
List planeGroup) {
List polygons = boundaryPathsWithHoles(
boundaryPaths(boundaryEdgesOfPlaneGroup(planeGroup)));
System.out.println("polygons: " + polygons.size());
List result = new ArrayList<>(polygons.size());
for (Polygon p : polygons) {
Optional> holesOfPresult
= p.
getStorage().getValue(Edge.KEY_POLYGON_HOLES);
if (!holesOfPresult.isPresent()) {
result.add(p);
} else {
result.addAll(PolygonUtil.concaveToConvex(p));
}
}
return result;
}
private static boolean falseBoundaryEdgeSharedWithOtherEdge(Edge fbe, Edge e) {
// we don't consider edges with shared end-points since we are only
// interested in "false-boundary-edge"-cases
boolean sharedEndPoints = e.getP1().pos.equals(fbe.getP1().pos)
|| e.getP1().pos.equals(fbe.getP2().pos)
|| e.getP2().pos.equals(fbe.getP1().pos)
|| e.getP2().pos.equals(fbe.getP2().pos);
if (sharedEndPoints) {
return false;
}
return fbe.contains(e.getP1().pos) || fbe.contains(e.getP2().pos);
}
private static List> searchPlaneGroups(List polygons) {
List> planeGroups = new ArrayList<>();
boolean[] used = new boolean[polygons.size()];
System.out.println("#polys: " + polygons.size());
for (int pOuterI = 0; pOuterI < polygons.size(); pOuterI++) {
if (used[pOuterI]) {
continue;
}
Polygon pOuter = polygons.get(pOuterI);
List otherPolysInPlane = new ArrayList<>();
otherPolysInPlane.add(pOuter);
for (int pInnerI = 0; pInnerI < polygons.size(); pInnerI++) {
Polygon pInner = polygons.get(pInnerI);
if (pOuter.equals(pInner)) {
continue;
}
Vector3d nOuter = pOuter.plane.normal;
Vector3d nInner = pInner.plane.normal;
double angle = nOuter.angle(nInner);
// System.out.println("angle: " + angle + " between " + pOuterI+" -> " + pInnerI);
if (angle < 0.01 /*&& abs(pOuter.plane.dist - pInner.plane.dist) < 0.1*/) {
otherPolysInPlane.add(pInner);
used[pInnerI] = true;
System.out.println("used: " + pOuterI + " -> " + pInnerI);
}
}
if (!otherPolysInPlane.isEmpty()) {
planeGroups.add(otherPolysInPlane);
}
}
return planeGroups;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy