mirror of
https://github.com/xibyte/jsketcher
synced 2025-12-24 01:15:25 +01:00
BREP boolean algorithm
This commit is contained in:
parent
f0e3061147
commit
47958ac14d
5 changed files with 282 additions and 39 deletions
|
|
@ -5,4 +5,11 @@ export class Curve {
|
|||
|
||||
}
|
||||
|
||||
intersectCurve(curve) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
|
||||
parametricEquation(t) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,43 @@ import {Curve} from '../curve'
|
|||
|
||||
export class Line extends Curve {
|
||||
|
||||
constructor() {
|
||||
constructor(p0, v) {
|
||||
super();
|
||||
this.p0 = p0;
|
||||
this.v = v;
|
||||
}
|
||||
|
||||
}
|
||||
intersectEdge(edge) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
|
||||
intersectSurface(surface) {
|
||||
//assume surface is plane
|
||||
const s0 = surface.normal.multiply(surface.w);
|
||||
return surface.normal(s0.minus(this.p0)) / surface.normal(this.v); // 4.7.4
|
||||
}
|
||||
|
||||
intersectCurve(curve, surface) {
|
||||
if (curve instanceof Line) {
|
||||
const otherNormal = surface.normal.cross(curve.v)._normalize();
|
||||
return otherNormal.dot(curve.p0.minus(this.p0)) / otherNormal.dot(this.v); // (4.8.3)
|
||||
}
|
||||
return super.intersectCurve(curve);
|
||||
}
|
||||
|
||||
parametricEquation(t) {
|
||||
return this.p0.plus(this.v.multiply(t));
|
||||
}
|
||||
}
|
||||
|
||||
Line.fromTwoPlanesIntersection = function(plane1, plane2) {
|
||||
const v = plane1.normal.cross(plane2.normal);
|
||||
const plane1Vec = plane1.normal.multiply(plane1.w);
|
||||
const plane2Vec = plane2.normal.multiply(plane2.w);
|
||||
const p0 = plane1Vec.plus(plane2Vec);
|
||||
return new Line(p0, v);
|
||||
};
|
||||
|
||||
Line.fromSegment = function(a, b) {
|
||||
return new Line(a, b.minus(a)._normalize());
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {Surface} from '../surface'
|
||||
import {Line} from './line'
|
||||
import {AXIS} from '../../../math/l3space'
|
||||
|
||||
export class Plane extends Surface {
|
||||
|
|
@ -21,4 +22,12 @@ export class Plane extends Surface {
|
|||
x = y.cross(normal);
|
||||
return [x, y, normal];
|
||||
}
|
||||
|
||||
intersect(other) {
|
||||
if (other instanceof Plane) {
|
||||
return new Line.fromTwoPlanesIntersection(this, other);
|
||||
}
|
||||
return super.intersect();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,5 +4,9 @@ export class Surface {
|
|||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
intersect() {
|
||||
throw 'not implemented';
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -1,28 +1,28 @@
|
|||
import * as BREPBuilder from '../brep-builder';
|
||||
import {HalfEdge} from '../topo/edge';
|
||||
import {HalfEdge, Edge} from '../topo/edge';
|
||||
import {Loop} from '../topo/loop';
|
||||
import {Face} from '../topo/face';
|
||||
import {Shell} from '../topo/shell';
|
||||
import {Vertex} from '../topo/vertex';
|
||||
import {Line} from '../geom/impl/line';
|
||||
import Vector from '../../math/vector';
|
||||
|
||||
export function union( shell1, shell2 ) {
|
||||
|
||||
|
||||
const faces = shell1.faces.concat(shell2.faces);
|
||||
const facesData = [];
|
||||
|
||||
const solveData = intersectFaces(shell1, shell2);
|
||||
initSolveData(shell1, facesData);
|
||||
initSolveData(shell2, facesData);
|
||||
|
||||
intersectFaces(shell1, shell2);
|
||||
|
||||
const result = new Shell();
|
||||
|
||||
for (let faceData of solveData.shell1.faceData) {
|
||||
for (let faceData of facesData) {
|
||||
|
||||
const seen = new Set();
|
||||
|
||||
//intersection edges must go first
|
||||
faceData.edges.sort((a, b) => {
|
||||
const aX = solveData.newEdges.has(a);
|
||||
const bX = solveData.newEdges.has(b);
|
||||
return aX ? (bX ? 0 : -1) : 1;
|
||||
});
|
||||
|
||||
while (true) {
|
||||
let edge = faceData.edges.pop();
|
||||
if (edge == undefined || seen.has(edge)) {
|
||||
|
|
@ -32,7 +32,7 @@ export function union( shell1, shell2 ) {
|
|||
while (edge) {
|
||||
loop.halfEdges.push(edge);
|
||||
seen.add(edge);
|
||||
let candidates = this.vertexToEdge.get(edge.vertexB);
|
||||
let candidates = faceData.vertexToEdge.get(edge.vertexB);
|
||||
edge = findMaxTurningLeft(candidates);
|
||||
if (seen.has(edge)) {
|
||||
break;
|
||||
|
|
@ -48,37 +48,215 @@ export function union( shell1, shell2 ) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function intersectFaces(shell1, shell2) {
|
||||
const data = new SolveData();
|
||||
for (let i = 0; i < shell1.faces.length; i++) {
|
||||
for (var j = 0; j < shell2.faces.length; j++) {
|
||||
if (i == j) continue;
|
||||
|
||||
const face1 = shell1.faces[i];
|
||||
const face2 = shell2.faces[j];
|
||||
const curve = face1.surface.intersect(face2.surface);
|
||||
|
||||
const face1Segments = split(curve, face1.outerLoop);
|
||||
const face2Segments = split(curve, face2.outerLoop);
|
||||
const segments = merge(face1Segments, face2Segments);
|
||||
|
||||
|
||||
function initSolveData(shell, facesData) {
|
||||
for (let face of shell.faces) {
|
||||
const solveData = new FaceSolveData(face);
|
||||
facesData.push(solveData);
|
||||
face.__faceSolveData = solveData;
|
||||
for (let he of face.outerLoop) {
|
||||
EdgeSolveData.clear(he);
|
||||
solveData.vertexToEdge.set(he.vertexA, [he]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findMaxTurningLeft() {
|
||||
|
||||
}
|
||||
|
||||
class SolveData {
|
||||
constructor() {
|
||||
this.shell1 = new ShellSolveData();
|
||||
this.shell2 = new ShellSolveData();
|
||||
this.newEdges = new Set();
|
||||
function intersectFaces(shell1, shell2) {
|
||||
for (let i = 0; i < shell1.faces.length; i++) {
|
||||
for (let j = 0; j < shell2.faces.length; j++) {
|
||||
const face1 = shell1.faces[i];
|
||||
const face2 = shell2.faces[j];
|
||||
|
||||
const curve = face1.surface.intersect(face2.surface);
|
||||
|
||||
const newEdges = [];
|
||||
const direction = face1.surface.normal.cross(face2.surface.normal);
|
||||
split(face2, face1.outerLoop, newEdges, curve, direction);
|
||||
split(face1, face2.outerLoop, newEdges, curve, direction);
|
||||
|
||||
newEdges.forEach(e => {
|
||||
face1.__faceSolveData.newEdges.push(e.halfEdge1);
|
||||
addToListInMap(face1.__faceSolveData.vertexToEdge, e.halfEdge1.vertexA, e.halfEdge1);
|
||||
});
|
||||
newEdges.forEach(e => {
|
||||
face2.__faceSolveData.newEdges.push(e.halfEdge2);
|
||||
addToListInMap(face2.__faceSolveData.vertexToEdge, e.halfEdge2.vertexA, e.halfEdge2);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShellSolveData {
|
||||
function split(face, loop, result, onCurve, direction) {
|
||||
const nodes = [];
|
||||
for (let edge of loop.halfEdges) {
|
||||
const edgeSolveData = EdgeSolveData.get(edge);
|
||||
if (edgeSolveData.skipFace.has(face)) {
|
||||
continue;
|
||||
}
|
||||
const preExistVertex = edgeSolveData.splitByFace.get(face);
|
||||
if (preExistVertex) {
|
||||
nodes.push(new Node(preExistVertex, edgeNormal(edge), edge));
|
||||
continue
|
||||
}
|
||||
intersectSurfaceWithEdge(face.surface, edge, nodes);
|
||||
}
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
let inNode = nodes[i];
|
||||
nodes[i] = null;
|
||||
let closestIdx = findCloserProjection(nodes, inNode.point);
|
||||
if (closestIdx == -1) {
|
||||
throw 'consider me';
|
||||
}
|
||||
let outNode = nodes[closestIdx];
|
||||
|
||||
if (outNode.normal.dot(inNode.normal) > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
nodes[closestIdx] = null;
|
||||
|
||||
const halfEdge1 = new HalfEdge();
|
||||
halfEdge1.vertexA = inNode.vertex;
|
||||
halfEdge1.vertexB = outNode.vertex;
|
||||
|
||||
const halfEdge2 = new HalfEdge();
|
||||
halfEdge2.vertexB = halfEdge1.vertexA;
|
||||
halfEdge2.vertexA = halfEdge1.vertexB;
|
||||
|
||||
splitEdgeByVertex(inNode.edge, halfEdge1.vertexA);
|
||||
splitEdgeByVertex(outNode.edge, halfEdge1.vertexB);
|
||||
|
||||
const sameDirection = direction.dot(outNode.point.minus(inNode.point)) > 0;
|
||||
|
||||
const halfEdgeSameDir = sameDirection ? halfEdge1 : halfEdge2;
|
||||
const halfEdgeNegativeDir = sameDirection ? halfEdge2 : halfEdge1;
|
||||
|
||||
// cross edge should go with negative dir for the first face and positive for the second
|
||||
const edge = new Edge(onCurve);
|
||||
edge.halfEdge1 = halfEdgeNegativeDir;
|
||||
edge.halfEdge2 = halfEdgeSameDir;
|
||||
|
||||
result.push(edge);
|
||||
}
|
||||
}
|
||||
|
||||
function splitEdgeByVertex(originHalfEdge, vertex) {
|
||||
const halfEdge1 = new HalfEdge();
|
||||
halfEdge1.vertexA = vertex;
|
||||
halfEdge1.vertexB = originHalfEdge.vertexB;
|
||||
|
||||
const halfEdge2 = new HalfEdge();
|
||||
halfEdge2.vertexA = halfEdge1.vertexB;
|
||||
halfEdge2.vertexB = halfEdge1.vertexA;
|
||||
|
||||
const newEdge = new Edge(originHalfEdge.edge);
|
||||
BREPBuilder.linkHalfEdges(newEdge, halfEdge1, halfEdge2);
|
||||
|
||||
const twin = originHalfEdge.twin();
|
||||
originHalfEdge.vertexB = vertex;
|
||||
twin.vertexA = vertex;
|
||||
|
||||
originHalfEdge.loop.push(halfEdge1);
|
||||
twin.loop.push(halfEdge2);
|
||||
|
||||
halfEdge1.loop = originHalfEdge.loop;
|
||||
halfEdge2.loop = twin.loop;
|
||||
|
||||
EdgeSolveData.transfer(originHalfEdge, halfEdge1);
|
||||
EdgeSolveData.transfer(twin, halfEdge2);
|
||||
|
||||
EdgeSolveData.createIfEmpty(twin).splitByFace.set(originHalfEdge.loop.face, vertex);
|
||||
EdgeSolveData.createIfEmpty(halfEdge2).skipFace.add(originHalfEdge.loop.face);
|
||||
|
||||
//addToListInMap(originHalfEdge.loop.face.__faceSolveData.vertexToEdge,
|
||||
}
|
||||
|
||||
function findCloserProjection(nodes, point) {
|
||||
let hero = -1;
|
||||
let heroDistance = Number.MAX_VALUE;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
let node = nodes[i];
|
||||
if (node == null)
|
||||
let projectionDistance = node.normal.dot(node.point.minus(point));
|
||||
if (hero == -1 || (projectionDistance > 0 && projectionDistance < heroDistance)) {
|
||||
hero = i;
|
||||
heroDistance = projectionDistance;
|
||||
}
|
||||
}
|
||||
return hero;
|
||||
}
|
||||
|
||||
function intersectSurfaceWithEdge(surface, edge, result) {
|
||||
const p0 = edge.vertexA.point;
|
||||
const ab = edge.vertexB.point.minus(p0);
|
||||
const length = ab.length();
|
||||
const v = ab._multiply(1 / length);
|
||||
const edgeLine = new Line(p0, v);
|
||||
const t = edgeLine.intersectSurface(surface);
|
||||
if (t >= 0 && t <= length) {
|
||||
const pointOfIntersection = edgeLine.parametricEquation(t);
|
||||
const edgeNormal = edge.loop.face.surface.normal.cross(v)._normalize() ;
|
||||
result.push(new Node(new Vertex(pointOfIntersection), edgeNormal, edge));
|
||||
}
|
||||
}
|
||||
|
||||
function edgeNormal(edge) {
|
||||
return edge.loop.face.surface.normal.cross( edge.vertexB.point.minus(edge.vertexA.point) )._normalize();
|
||||
}
|
||||
|
||||
function intersectCurveWithEdge(curve, edge, surface, result) {
|
||||
const p0 = edge.vertexA.point;
|
||||
const ab = edge.vertexB.point.minus(p0);
|
||||
const length = ab.length();
|
||||
const v = ab._multiply(1 / length);
|
||||
const edgeLine = new Line(p0, v);
|
||||
const t = edgeLine.intersectCurve(curve, surface);
|
||||
if (t >= 0 && t <= length) {
|
||||
const pointOfIntersection = edgeLine.parametricEquation(t);
|
||||
const edgeNormal = surface.normal.cross(v)._normalize() ;
|
||||
result.push(new Node(pointOfIntersection, edgeNormal, edge));
|
||||
}
|
||||
}
|
||||
|
||||
function EdgeSolveData() {
|
||||
this.splitByFace = new Map();
|
||||
this.skipFace = new Set();
|
||||
}
|
||||
|
||||
EdgeSolveData.EMPTY = new EdgeSolveData();
|
||||
|
||||
EdgeSolveData.get = function(edge) {
|
||||
if (!edge.__edgeSolveData) {
|
||||
return EdgeSolveData.EMPTY;
|
||||
}
|
||||
return edge.__edgeSolveData;
|
||||
};
|
||||
|
||||
EdgeSolveData.createIfEmpty = function(edge) {
|
||||
if (!edge.__edgeSolveData) {
|
||||
edge.__edgeSolveData = new EdgeSolveData();
|
||||
}
|
||||
return edge.__edgeSolveData;
|
||||
};
|
||||
|
||||
EdgeSolveData.clear = function(edge) {
|
||||
delete edge.__edgeSolveData;
|
||||
};
|
||||
|
||||
EdgeSolveData.transfer = function(from, to) {
|
||||
to.__edgeSolveData = from.__edgeSolveData;
|
||||
};
|
||||
|
||||
function Node(vertex, normal, splitsEdge) {
|
||||
this.vertex = vertex;
|
||||
this.normal = normal;
|
||||
this.point = vertex.point;
|
||||
}
|
||||
|
||||
class SolveData {
|
||||
constructor() {
|
||||
this.faceData = [];
|
||||
}
|
||||
|
|
@ -87,7 +265,17 @@ class ShellSolveData {
|
|||
class FaceSolveData {
|
||||
constructor(face) {
|
||||
this.face = face;
|
||||
this.edges = [];
|
||||
this.newEdges = [];
|
||||
this.newChunks = [];
|
||||
this.vertexToEdge = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
function addToListInMap(map, key, value) {
|
||||
let list = map.get(key);
|
||||
if (!list) {
|
||||
list = [];
|
||||
map.set(key, list);
|
||||
}
|
||||
list.add(value);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue