improving BREP boolean algorithm / sector analysis

This commit is contained in:
Val Erastov 2017-03-16 00:33:28 -07:00
parent 8f1e847afc
commit d89ba309b8
9 changed files with 250 additions and 117 deletions

View file

@ -36,6 +36,24 @@ function addGlobalDebugActions(app) {
AddVertex: (v) => {
window.__DEBUG__.AddPoint(v.point);
},
AddPolygon: (vertices, color) => {
for (let i = 0; i < vertices.length; i ++) {
__DEBUG__.AddSegment(vertices[i].point, vertices[(i + 1) % vertices.length].point, color);
}
},
AddPlane: (plane) => {
const geo = new THREE.PlaneBufferGeometry(2000, 2000, 8, 8);
const coplanarPoint = plane.normal.multiply(plane.w);
const focalPoint = coplanarPoint.plus(plane.normal);
geo.lookAt(focalPoint.three());
geo.translate(coplanarPoint.x, coplanarPoint.y, coplanarPoint.z);
const mat = new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.DoubleSide,
transparent: true,
opacity: 0.3, });
const planeObj = new THREE.Mesh(geo, mat);
debugGroup.add(planeObj);
app.viewer.render();
},
AddHalfEdge: (he, color) => {
window.__DEBUG__.AddSegment(he.vertexA.point, he.vertexB.point, color);
},
@ -93,7 +111,7 @@ function clearGroup(g) {
function createLine(a, b, color) {
color = color || 0xFA8072;
const debugLineMaterial = new THREE.LineBasicMaterial({color, linewidth: 3});
const debugLineMaterial = new THREE.LineBasicMaterial({color, linewidth: 10});
const lg = new THREE.Geometry();
lg.vertices.push(a.three());
lg.vertices.push(b.three());

View file

@ -148,3 +148,13 @@ export function iterateSegments(items, callback) {
callback(items[i], items[j], i, j);
}
}
export function invertLoop(loop) {
for (let halfEdge of loop.halfEdges) {
const t = halfEdge.vertexA;
halfEdge.vertexA = halfEdge.vertexB;
halfEdge.vertexB = t;
}
loop.halfEdges.reverse();
linkSegments(loop.halfEdges);
}

View file

@ -49,6 +49,12 @@ export class Plane extends Surface {
//TODO: store this.normal.multiply(this.w) in a field since it's constant value
}
equals(other, tol) {
return other instanceof Plane &&
math.areVectorsEqual(this.normal, other.normal, tol) &&
math.areEqual(this.w, other.w, tol);
}
toParametricForm() {
const basis = BasisForPlane(this.normal);
return new ParametricPlane(this.normal.multiply(this.w), basis.x, basis.y);

View file

@ -48,19 +48,15 @@ export function subtract( shell1, shell2 ) {
export function invert( shell ) {
for (let face of shell.faces) {
face.surface = face.surface.invert();
invertLoop(face.outerLoop);
for (let loop of face.loops) {
invertLoop(loop);
}
}
BREPValidator.validateToConsole(shell);
}
function invertLoop(loop) {
for (let halfEdge of loop.halfEdges) {
const t = halfEdge.vertexA;
halfEdge.vertexA = halfEdge.vertexB;
halfEdge.vertexB = t;
}
loop.halfEdges.reverse();
BREPBuilder.linkSegments(loop.halfEdges);
BREPBuilder.invertLoop(loop);
}
export function BooleanAlgorithm( shell1, shell2, type ) {
@ -204,11 +200,22 @@ function sew(allFaces) {
if (sewed.has(h1)) {
continue;
}
const neighbors = findNeighborhood(allFaces, face, h1);
if (neighbors.length == 0) {
const neighborhood = findNeighborhood(allFaces, face, h1);
if (neighborhood.all.length == 1) {
continue FACES;
}
let h2 = neighborhoodAnalysis(h1, neighbors, analyzedNeighbors);
let h2;
if (neighborhood.all.length == 2 && neighborhood.side2.length == 1) {
h2 = neighborhood.side2[0];
} else {
h2 = analyzedNeighbors.get(h1);
if (h2 === undefined) { // null indicates edge can't be sewed
neighborhoodAnalysis(neighborhood, analyzedNeighbors);
}
h2 = analyzedNeighbors.get(h1);
}
if (h2 == null) {
continue FACES;
}
@ -230,98 +237,82 @@ function sew(allFaces) {
return sewedFaces;
}
function neighborhoodAnalysis(edge, neighbors, analized) {
if (neighbors.opposite.length > 1 || neighbors.other != null) {
function edgeV(edge) {
return edge.vertexB.point.minus(edge.vertexA.point)._normalize();
}
let paired = analized.get(edge);
if (paired) {
return paired;
}
function neighborhoodAnalysis(neighborhood, analized) {
let a1 = neighbors.opposite[0];
let a2 = neighbors.opposite[1];
let b1 = edge;
let b2 = neighbors.other;
if (!a1 || !a2) {
a1 = edge;
a2 = neighbors.other;
b1 = neighbors.opposite[0];
b2 = neighbors.opposite[1];
}
function encloses(e1, e2, testeeE) {
const f1 = e1.loop.face;
const f2 = e2.loop.face;
const testee = testeeE.loop.face;
if (!a2) {
throw 'unsupported neighborhood case'
}
const normal = edgeV(e1);
const t1 = f1.surface.normal.cross(normal)._normalize();
const t2 = f2.surface.normal.cross(edgeV(e2))._normalize();
const t3 = testee.surface.normal.cross(edgeV(testeeE))._normalize();
const a1N = a1.loop.face.surface.normal;
const a2N = a2.loop.face.surface.normal;
const b1N = b1.loop.face.surface.normal;
const normal = a1N.cross(b1N);
//__DEBUG__.AddSegment(e1.vertexA.point, e1.vertexA.point.plus(normal.multiply(100)), 0xffffff);
//__DEBUG__.AddSegment(e1.vertexA.point, e1.vertexA.point.plus(t1.multiply(100)), 0x00ff00);
//__DEBUG__.AddSegment(e1.vertexA.point, e1.vertexA.point.plus(t2.multiply(100)), 0x00ffff);
//__DEBUG__.AddSegment(e1.vertexA.point, e1.vertexA.point.plus(t3.multiply(100)), 0xff0000);
if (b2 == null) {
const dist1 = leftTurningMeasure(b1N, a1N.negate(), normal);
const dist2 = leftTurningMeasure(b1N, a2N.negate(), normal);
if (dist1 > dist2) {
analized.set(b1, a1);
analized.set(a1, b1);
analized.set(a2, null);
} else {
analized.set(b1, a2);
analized.set(a2, b1);
analized.set(a1, null);
const angle = leftTurningMeasure(t1, t2, normal);
const testAngle = leftTurningMeasure(t1, t3, normal);
return testAngle > angle;
}
let paired = new Set();
for (let e1 of neighborhood.side1) {
SIDE_2:
for (let e2 of neighborhood.side2) {
if (analized.has(e2)) continue;
for (let t of neighborhood.all) {
if (t == e1 || t == e2) {
continue;
}
if (encloses(e1, e2, t)) {
continue SIDE_2;
}
}
} else {
const b2N = b2.loop.face.surface.normal;
const dist1 = leftTurningMeasure(b1N, a1N.negate(), normal);
const dist2 = leftTurningMeasure(b1N, a2N.negate(), normal);
let closestDist, closestOption1, closestOption2, leftOver1, leftOver2;
if (dist1 > dist2) {
closestOption1 = a1;
leftOver1 = a2;
closestDist = dist1;
} else {
closestOption1 = a2;
leftOver1 = a1;
closestDist = dist2;
}
// concurrent in between
if (leftTurningMeasure(b1N, b2N, normal) > closestDist) {
closestOption2 = b2;
leftOver2 = b1;
} else {
closestOption2 = b1;
leftOver2 = b2;
}
analized.set(closestOption1, closestOption2);
analized.set(closestOption2, closestOption1);
analized.set(leftOver1, leftOver2);
analized.set(leftOver2, leftOver1);
return analized.get(edge);
analized.set(e1, e2);
analized.set(e2, e1);
paired.add(e1);
paired.add(e2);
}
}
for (let e of neighborhood.all) {
if (!paired.has(e)) {
analized.set(e, null);
}
} else {
return neighbors.opposite[0];
}
}
function findNeighborhood(allFaces, skipFace, forEdge) {
const result = {
opposite: [],
other: null
side1: [forEdge],
side2: [],
all: [forEdge]
};
for (let face of allFaces) {
if (face == skipFace) continue;
for (let e of face.edges) {
if (areEdgesOpposite(e, forEdge)) {
result.opposite.push(e)
result.side2.push(e);
result.all.push(e);
} else if (e != forEdge && areEdgesEqual(e, forEdge)) {
result.other = e;
result.side1.push(e);
result.all.push(e);
}
}
}
return result;
}
function mergeVertices(shell1, shell2) {
export function mergeVertices(shell1, shell2) {
const toSwap = new Map();
for (let v1 of shell1.vertices) {
for (let v2 of shell2.vertices) {
@ -443,13 +434,14 @@ function areEdgesOpposite(e1, e2) {
function splitNewEdgesIfNeeded(faceData) {
for (let oe of faceData.face.edges) {
for (let ne of faceData.newEdges) {
if (math.areEqual(Math.abs(ne.edge.curve.v.dot(oe.edge.curve.v)), 1, TOLERANCE_SQ) &&
math.areEqual(Math.abs(ne.edge.curve.v.dot(ne.vertexA.point.minus(oe.vertexA.point)._normalize())), 1, TOLERANCE_SQ)) {
const line = Line.fromSegment(ne.vertexA.point, ne.vertexB.point);
const length = math.distanceAB3(ne.vertexA.point, ne.vertexB.point);
for (let i = 0; i < faceData.newEdges.length; ++i) {
let ne = faceData.newEdges[i];
if ( math.areEqual(Math.abs(ne.edge.curve.v.dot(oe.edge.curve.v)), 1, TOLERANCE) &&
math.areEqual(Math.abs(ne.edge.curve.v.dot(ne.vertexA.point.minus(oe.vertexA.point)._normalize())), 1, TOLERANCE)) {
function check(vertex) {
const line = Line.fromSegment(ne.vertexA.point, ne.vertexB.point);
const length = math.distanceAB3(ne.vertexA.point, ne.vertexB.point);
if (ne.vertexA != vertex && ne.vertexB != vertex) {
const t = line.t(vertex.point);
if (t >= 0 && t <= length) {
@ -488,8 +480,11 @@ function merge(face, newEdges) {
for (let e of newEdges) {
if (e == null) continue;
if (findCoincidentEdge(e, allEdges) == null) {
const existingEdge = findCoincidentEdge(e, allEdges);
if (existingEdge == null) {
allEdges.push(e);
} else {
EdgeSolveData.createIfEmpty(existingEdge).newEdgeFlag = true
}
}
@ -498,7 +493,7 @@ function merge(face, newEdges) {
allEdges = allEdges.filter(e => e != null);
//put new edges to the tail
bringNewEdgesToTheTail(allEdges);
squash(face, allEdges)
squash(face, allEdges);
if (DEBUG.EDGE_MERGING) {
for (let e of allEdges) __DEBUG__.AddHalfEdge(e, 0xffff00);
}
@ -620,7 +615,7 @@ function traverseFaces(face, validFaces, callback) {
}
}
function loopsToFaces(originFace, loops, out) {
export function loopsToFaces(originFace, loops, out) {
function createFaces(nestedLoop, surface) {
const loop = nestedLoop.loop;
const newFace = new Face(surface);
@ -703,25 +698,27 @@ function cleanUpSolveData(shell) {
}
}
function findMaxTurningLeft(edge, edges, normal) {
function findMaxTurningLeft(pivotEdge, edges, normal) {
edges = edges.slice();
function edgeVector(edge) {
return edge.vertexB.point.minus(edge.vertexA.point)._normalize();
}
const edgeV = edgeVector(edge);
const pivot = pivotEdge.vertexA.point.minus(pivotEdge.vertexB.point)._normalize();
edges.sort((e1, e2) => {
return leftTurningMeasure(edgeV, edgeVector(e1), normal) - leftTurningMeasure(edgeV, edgeVector(e2), normal);
return leftTurningMeasure(pivot, edgeVector(e1), normal) - leftTurningMeasure(pivot, edgeVector(e2), normal);
});
return edges[0];
return edges[edges.length - 1];
}
function leftTurningMeasure(v1, v2, normal) {
let measure = v1.dot(v2);
if (v1.cross(v2).dot(normal) < 0) {
measure *= -1;
measure += 2;
measure = -(2 + measure);
}
return measure
measure -= 1;//shift to the zero
//make it positive all the way
return -measure;
}
function intersectFaces(shell1, shell2, inverseCrossEdgeDirection) {
@ -957,9 +954,9 @@ function findCloserOnCurve(nodes, toNode, curve) {
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
if (node == null) continue;
let inward = toNode.normal * node.normal < 0;
let distance = Math.abs(origin - curve.t(node.point));
if (inward && distance < heroDistance) {
let distance = (origin - curve.t(node.point)) * node.normal;
if (distance < 0) continue;
if (distance < heroDistance) {
hero = i;
heroDistance = distance;
}
@ -993,7 +990,7 @@ function intersectFaceWithEdge(face, edge, result) {
const length = ab.length();
const v = ab._multiply(1 / length);
if (math.areEqual(edge.edge.curve.v.dot(face.surface.normal), 0, TOLERANCE_SQ)) {
if (math.areEqual(edge.edge.curve.v.dot(face.surface.normal), 0, TOLERANCE)) {
if (math.areEqual(face.surface.normal.dot(edge.vertexA.point), face.surface.w, TOLERANCE)) {
classifyAndAdd(edge.vertexA.point, true, false);
classifyAndAdd(edge.vertexB.point, false, true);
@ -1012,7 +1009,7 @@ function intersectFaceWithEdge(face, edge, result) {
classifyAndAdd(pointOfIntersection, coiA, coiB)
}
}
function classifyAndAdd(pointOfIntersection, coiA, coiB, dir) {
function classifyAndAdd(pointOfIntersection, coiA, coiB) {
const classRes = classifyPointToFace(pointOfIntersection, face);
if (classRes.inside) {
let vertexOfIntersection;
@ -1029,9 +1026,6 @@ function intersectFaceWithEdge(face, edge, result) {
}
const node = new Node(vertexOfIntersection, edge);
if (dir) {
node.dir = dir;
}
result.push(node);
if (classRes.edge) {

View file

@ -8,6 +8,13 @@ export class Edge extends TopoObject {
this.halfEdge1 = null;
this.halfEdge2 = null;
}
link(halfEdge1, halfEdge2) {
halfEdge1.edge = this;
halfEdge2.edge = this;
this.halfEdge1 = halfEdge1;
this.halfEdge2 = halfEdge2;
}
}
export class HalfEdge extends TopoObject {
@ -22,8 +29,62 @@ export class HalfEdge extends TopoObject {
this.prev = null;
}
setAB(a, b) {
this.vertexA = a;
this.vertexB = b;
}
twin() {
return this.edge.halfEdge1 == this ? this.edge.halfEdge2 : this.edge.halfEdge1;
}
split(vertex) {
function splitHalfEdge(h) {
const newEdge = new HalfEdge();
newEdge.vertexA = vertex;
newEdge.vertexB = h.vertexB;
h.vertexB = newEdge.vertexA;
h.vertexA.edges.add(newEdge);
h.vertexA.edges.remove(h);
vertex.edges.add(newEdge);
return newEdge;
}
const orig = this;
const twin = orig.twin();
if (orig.vertexA == vertex || orig.vertexB == vertex) {
return;
}
const newOrig = splitHalfEdge(orig);
const newTwin = splitHalfEdge(twin);
orig.edge.link(orig, newTwin);
new Edge(orig.edge.curve).link(twin, newOrig);
orig.loop.halfEdges.splice(orig.loop.halfEdges.indexOf(orig) + 1, 0, newOrig);
twin.loop.halfEdges.splice(twin.loop.halfEdges.indexOf(twin) + 1, 0, newTwin);
orig.next = newOrig;
twin.next = newTwin;
newOrig.loop = orig.loop;
newTwin.loop = twin.loop;
}
}
HalfEdge.fromVertices = function(a, b, curve) {
const halfEdge1 = new HalfEdge();
const halfEdge2 = new HalfEdge();
halfEdge1.setAB(a, b);
halfEdge2.setAB(b, a);
new Edge(curve).link(halfEdge1, halfEdge2);
return halfEdge1;
};

View file

@ -7,12 +7,23 @@ export class Shell extends TopoObject {
this.defineIterable('vertices', () => verticesGenerator(this));
this.defineIterable('edges', () => edges(this))
}
reindexVertices() {
for (let e of this.edges) {
e.halfEdge1.vertexA.edges.clear();
e.halfEdge1.vertexB.edges.clear();
}
for (let e of this.edges) {
e.halfEdge1.vertexA.edges.add(e.halfEdge1);
e.halfEdge2.vertexA.edges.add(e.halfEdge2);
}
}
}
export function* verticesGenerator(shell) {
const seen = new Set();
for (let face of shell.faces) {
for (let edge of face.outerLoop.halfEdges) {
for (let edge of face.edges) {
if (!seen.has(edge.vertexA)) {
seen.add(edge.vertexA);
yield edge.vertexA;

View file

@ -5,7 +5,15 @@ export class Vertex extends TopoObject {
constructor(point) {
super();
this.point = point;
this.edges = [];
this.edges = new Set();
}
edgeFor(other) {
for (let e of this.edges) {
if (e.vertexB == other) {
return e;
}
}
return null;
}
}

View file

@ -5,23 +5,38 @@ export default class BBox {
constructor() {
this.minX = Number.MAX_VALUE;
this.minY = Number.MAX_VALUE;
this.minZ = Number.MAX_VALUE;
this.maxX = -Number.MAX_VALUE;
this.maxY = -Number.MAX_VALUE;
this.maxZ = -Number.MAX_VALUE;
}
checkBounds(x, y) {
checkBounds(x, y, z) {
z = z || 0;
this.minX = Math.min(this.minX, x);
this.minY = Math.min(this.minY, y);
this.minZ = Math.min(this.minZ, z);
this.maxX = Math.max(this.maxX, x);
this.maxY = Math.max(this.maxY, y);
this.maxZ = Math.max(this.maxZ, z);
}
checkPoint(p) {
this.checkBounds(p.x, p.y);
this.checkBounds(p.x, p.y, p.z);
}
center() {
return new Vector(this.minX + (this.maxX - this.minX) / 2, this.minY + (this.maxY - this.minY) / 2, 0)
return new Vector(this.minX + (this.maxX - this.minX) / 2,
this.minY + (this.maxY - this.minY) / 2,
this.minZ + (this.maxZ - this.minZ) / 2)
}
min() {
return new Vector(this.minX, this.minY, this.minZ)
}
max() {
return new Vector(this.maxX, this.maxY, this.maxZ)
}
width() {
@ -32,11 +47,17 @@ export default class BBox {
return this.maxY - this.minY;
}
depth() {
return this.maxZ - this.minZ;
}
expand(delta) {
this.minX -= delta;
this.minY -= delta;
this.minZ -= delta;
this.maxX += delta;
this.maxY += delta;
this.maxZ += delta;
}
toPolygon() {

View file

@ -48,6 +48,14 @@ Vector.prototype.length = function() {
return Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z);
};
Vector.prototype.lengthSquared = function() {
return this.dot(this);
};
Vector.prototype.distanceToSquared = function(a) {
return this.minus(a).lengthSquared();
};
Vector.prototype.minus = function(vector) {
return new Vector(this.x - vector.x, this.y - vector.y, this.z - vector.z);
};
@ -118,8 +126,4 @@ Vector.prototype.three = function() {
return new THREE.Vector3(this.x, this.y, this.z);
};
Vector.prototype.csg = function() {
return new CSG.Vector3D(this.x, this.y, this.z);
};
export default Vector;