eu.mihosoft.vrl.v3d.Edge Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of JavaCad Show documentation
Show all versions of JavaCad Show documentation
A Java based CSG Cad library
/*
* 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.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;
/**
* The Class Edge.
*
* @author miho
*/
public class Edge {
/** The p1. */
private final Vertex p1;
/** The p2. */
private final Vertex p2;
/** The direction. */
private final Vector3d direction;
/**
* Instantiates a new edge.
*
* @param p1 the p1
* @param p2 the p2
*/
public Edge(Vertex p1, Vertex p2) {
this.p1 = p1;
this.p2 = p2;
direction = p2.pos.minus(p1.pos).normalized();
}
/**
* Gets the p1.
*
* @return the p1
*/
public Vertex getP1() {
return p1;
}
// /**
// * @param p1 the p1 to set
// */
// public void setP1(Vertex p1) {
// this.p1 = p1;
// }
/**
* Gets the p2.
*
* @return the p2
*/
public Vertex getP2() {
return p2;
}
// /**
// * @param p2 the p2 to set
// */
// public void setP2(Vertex p2) {
// this.p2 = p2;
/**
* From polygon.
*
* @param poly the poly
* @return the list
*/
// }
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;
}
/**
* To vertices.
*
* @param edges the edges
* @return the list
*/
public static List toVertices(List edges) {
return edges.stream().map(e -> e.p1).collect(Collectors.toList());
}
/**
* To points.
*
* @param edges the edges
* @return the list
*/
public static List toPoints(List edges) {
return edges.stream().map(e -> e.p1.pos).collect(Collectors.toList());
}
/**
* To polygon.
*
* @param points the points
* @param plane the plane
* @return the polygon
*/
public 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;
}
/**
* To polygons.
*
* @param boundaryEdges the boundary edges
* @param plane the plane
* @return the list
*/
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;
}
/**
* The Class Node.
*
* @param the generic type
*/
private static class Node {
/** The parent. */
private Node parent;
/** The children. */
private final List children = new ArrayList<>();
/** The index. */
private final int index;
/** The value. */
private final T value;
/** The is hole. */
private boolean isHole;
/**
* Instantiates a new node.
*
* @param index the index
* @param value the value
*/
public Node(int index, T value) {
this.index = index;
this.value = value;
}
/**
* Adds the child.
*
* @param index the index
* @param value the value
*/
public void addChild(int index, T value) {
children.add(new Node(index, value));
}
/**
* Gets the children.
*
* @return the children
*/
public List getChildren() {
return this.children;
}
/**
* Gets the parent.
*
* @return the parent
*/
public Node getParent() {
return parent;
}
/**
* Gets the index.
*
* @return the index
*/
public int getIndex() {
return index;
}
/**
* Gets the value.
*
* @return the value
*/
public T getValue() {
return value;
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int hash = 7;
hash = 67 * hash + this.index;
return hash;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@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;
}
/**
* Distance to root.
*
* @return the int
*/
public int distanceToRoot() {
int dist = 0;
Node pNode = getParent();
while (pNode != null) {
dist++;
pNode = getParent();
}
return dist;
}
/**
* Checks if is checks if is hole.
*
* @return the isHole
*/
public boolean isIsHole() {
return isHole;
}
/**
* Sets the checks if is hole.
*
* @param isHole the isHole to set
*/
public void setIsHole(boolean isHole) {
this.isHole = isHole;
}
}
/** The Constant KEY_POLYGON_HOLES. */
public static final String KEY_POLYGON_HOLES = "jcsg:edge:polygon-holes";
/**
* Boundary paths with holes.
*
* @param boundaryPaths the boundary paths
* @return the list
*/
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 the list
*/
public 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;
}
/**
* _to polygons.
*
* @param boundaryEdges the boundary edges
* @param plane the plane
* @return the list
*/
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 is colinear
*
* @param p point to check
* @return true
if the specified point lies on this line
* segment; false
otherwise
*/
public boolean colinear(Vector3d p) {
return colinear(p,Plane.EPSILON_Point);
}
public boolean colinear(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 slopeSelfxy = (x1-x2)/(y1-y2);
double slopeSelfxz = (x1-x2)/(z1-z2);
double slopeSelfyz = (y1-y2)/(z1-z2);
double slopeTestxy = (x-x2)/(y-y2);
double slopeTestxz = (x-x2)/(z-z2);
double slopeTestyz = (y-y2)/(z-z2);
return Math.abs(slopeSelfxy - slopeTestxy) < TOL &&
Math.abs(slopeSelfxz - slopeTestxz) < TOL&&
Math.abs(slopeSelfyz - slopeTestyz) < TOL;
}
/**
* 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);
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int hash = 7;
hash = 71 * hash + Objects.hashCode(this.p1);
hash = 71 * hash + Objects.hashCode(this.p2);
return hash;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Edge other = (Edge) obj;
if(this.p1.pos.test( other.p1.pos,Plane.EPSILON_Point) && this.p2.pos.test( other.p2.pos,Plane.EPSILON_Point)) {
return true;
}
if(this.p1.pos.test( other.p2.pos,Plane.EPSILON_Point) && this.p2.pos.test( other.p1.pos,Plane.EPSILON_Point)) {
return true;
}
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 boolean isThisPointOneOfMine(Vertex test) {
return p1.pos.test(test.pos, Plane.EPSILON_Point)||p2.pos.test(test.pos, Plane.EPSILON_Point);
}
@Override
public String toString()
{
return "[[" + this.p1.getX() + ", " + this.p1.getY() + ", " + this.p1.getZ() + "]" +
", [" + this.p2.getX() + ", " + this.p2.getY() + ", " + this.p2.getZ() + "]]";
}
/**
* Gets the direction.
*
* @return the direction
*/
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();
}
}
/**
* Boundary polygons.
*
* @param csg the csg
* @return the list
*/
public static List boundaryPolygons(CSG csg) {
List result = new ArrayList<>();
for (List polygonGroup : searchPlaneGroups(csg.getPolygons())) {
result.addAll(boundaryPolygonsOfPlaneGroup(polygonGroup));
}
return result;
}
/**
* Boundary edges of plane group.
*
* @param planeGroup the plane group
* @return the list
*/
public 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;
}
/**
* Boundary polygons of plane group.
*
* @param planeGroup the plane group
* @return the list
*/
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;
}
/**
* False boundary edge shared with other edge.
*
* @param fbe the fbe
* @param e the e
* @return true, if successful
*/
public 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);
}
/**
* Search plane groups.
*
* @param polygons the polygons
* @return the list
*/
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;
}
public double length() {
// TODO Auto-generated method stub
return p1.pos.minus(p2.pos).length();
}
}