From d89ba309b87b7f727510b6e174fb63015e6f51df Mon Sep 17 00:00:00 2001 From: Val Erastov Date: Thu, 16 Mar 2017 00:33:28 -0700 Subject: [PATCH] improving BREP boolean algorithm / sector analysis --- web/app/3d/debug.js | 20 ++- web/app/brep/brep-builder.js | 10 ++ web/app/brep/geom/impl/plane.js | 6 + web/app/brep/operations/boolean.js | 204 ++++++++++++++--------------- web/app/brep/topo/Edge.js | 63 ++++++++- web/app/brep/topo/shell.js | 13 +- web/app/brep/topo/vertex.js | 12 +- web/app/math/bbox.js | 27 +++- web/app/math/vector.js | 12 +- 9 files changed, 250 insertions(+), 117 deletions(-) diff --git a/web/app/3d/debug.js b/web/app/3d/debug.js index 233121a3..be770c34 100644 --- a/web/app/3d/debug.js +++ b/web/app/3d/debug.js @@ -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()); diff --git a/web/app/brep/brep-builder.js b/web/app/brep/brep-builder.js index cdfc14a0..2face889 100644 --- a/web/app/brep/brep-builder.js +++ b/web/app/brep/brep-builder.js @@ -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); +} diff --git a/web/app/brep/geom/impl/plane.js b/web/app/brep/geom/impl/plane.js index cfe93fd0..a74c409d 100644 --- a/web/app/brep/geom/impl/plane.js +++ b/web/app/brep/geom/impl/plane.js @@ -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); diff --git a/web/app/brep/operations/boolean.js b/web/app/brep/operations/boolean.js index 6bf9fc59..3a52f88e 100644 --- a/web/app/brep/operations/boolean.js +++ b/web/app/brep/operations/boolean.js @@ -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(); + + //__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); + + 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; + } + } + analized.set(e1, e2); + analized.set(e2, e1); + paired.add(e1); + paired.add(e2); } - - 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); - - 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); - } - } 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); + } + + 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) { @@ -487,9 +479,12 @@ function merge(face, newEdges) { nullifyOppositeEdges(newEdges); for (let e of newEdges) { - if (e == null) continue; - if (findCoincidentEdge(e, allEdges) == null) { + if (e == null) continue; + 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) { diff --git a/web/app/brep/topo/Edge.js b/web/app/brep/topo/Edge.js index 9b63f2d5..958f410d 100644 --- a/web/app/brep/topo/Edge.js +++ b/web/app/brep/topo/Edge.js @@ -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); -} \ No newline at end of file + new Edge(curve).link(halfEdge1, halfEdge2); + return halfEdge1; +}; \ No newline at end of file diff --git a/web/app/brep/topo/shell.js b/web/app/brep/topo/shell.js index a6d5a935..00e8ec27 100644 --- a/web/app/brep/topo/shell.js +++ b/web/app/brep/topo/shell.js @@ -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; diff --git a/web/app/brep/topo/vertex.js b/web/app/brep/topo/vertex.js index 922ef0b5..afa21232 100644 --- a/web/app/brep/topo/vertex.js +++ b/web/app/brep/topo/vertex.js @@ -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; } - } \ No newline at end of file diff --git a/web/app/math/bbox.js b/web/app/math/bbox.js index e4b230f5..ba0e3da8 100644 --- a/web/app/math/bbox.js +++ b/web/app/math/bbox.js @@ -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() { diff --git a/web/app/math/vector.js b/web/app/math/vector.js index 478ac474..d786269d 100644 --- a/web/app/math/vector.js +++ b/web/app/math/vector.js @@ -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; \ No newline at end of file