mirror of https://gitee.com/cf-fz/WebCAD.git
fix #IKXMP !295 新的CSG实现
parent
a96d358d0f
commit
c6f01897ab
@ -1,15 +1,17 @@
|
|||||||
// 将设置放入此文件中以覆盖默认值和用户设置。
|
// 将设置放入此文件中以覆盖默认值和用户设置。
|
||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||||
//格式化设置
|
//格式化设置
|
||||||
"editor.tabSize": 4,
|
"editor.tabSize": 4,
|
||||||
"editor.formatOnPaste": true,
|
"editor.formatOnPaste": true,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
//格式设置
|
"editor.insertSpaces": true,
|
||||||
"typescript.format.placeOpenBraceOnNewLineForFunctions": true,
|
"editor.detectIndentation": false,
|
||||||
"typescript.format.placeOpenBraceOnNewLineForControlBlocks": true,
|
//格式设置
|
||||||
"typescript.referencesCodeLens.enabled": true,
|
"typescript.format.placeOpenBraceOnNewLineForFunctions": true,
|
||||||
"javascript.format.placeOpenBraceOnNewLineForFunctions": true,
|
"typescript.format.placeOpenBraceOnNewLineForControlBlocks": true,
|
||||||
"javascript.format.enable": true,
|
"typescript.referencesCodeLens.enabled": true,
|
||||||
"files.insertFinalNewline": true,
|
"javascript.format.placeOpenBraceOnNewLineForFunctions": true,
|
||||||
|
"javascript.format.enable": true,
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
}
|
}
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
import * as THREE from 'three';
|
|
||||||
import { app } from '../ApplicationServices/Application';
|
|
||||||
import { Command } from '../Editor/CommandMachine';
|
|
||||||
import { PromptSsgetResult, PromptStatus } from '../Editor/PromptResult';
|
|
||||||
import { CSG } from '../Geometry/ThreeCSG';
|
|
||||||
import { RenderType } from '../GraphicsSystem/RenderType';
|
|
||||||
|
|
||||||
async function GetEntity(msg: string)
|
|
||||||
{
|
|
||||||
let enRes: PromptSsgetResult
|
|
||||||
enRes = await app.m_Editor.GetSelection({
|
|
||||||
Msg: msg
|
|
||||||
});
|
|
||||||
if (enRes.Status === PromptStatus.OK)
|
|
||||||
return enRes.SelectSet.SelectEntityList[0];
|
|
||||||
else if (enRes.Status === PromptStatus.Cancel)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class CSGOperation implements Command
|
|
||||||
{
|
|
||||||
async exec()
|
|
||||||
{
|
|
||||||
let en1 = await GetEntity("请选择第一个对象");
|
|
||||||
let en2 = await GetEntity("请选择第二个对象");
|
|
||||||
|
|
||||||
if (!en1 || !en2) return;
|
|
||||||
let mesh1 = en1.GetDrawObjectFromRenderType(RenderType.Wireframe) as THREE.Mesh;
|
|
||||||
let mesh2 = en2.GetDrawObjectFromRenderType(RenderType.Wireframe) as THREE.Mesh;
|
|
||||||
|
|
||||||
let csg1 = new CSG(mesh1);
|
|
||||||
let csg2 = new CSG(mesh2);
|
|
||||||
|
|
||||||
let csg3 = this.Operate(csg1, csg2);
|
|
||||||
|
|
||||||
let mesh = csg3.toMesh(mesh1.material);
|
|
||||||
app.m_Viewer.Scene.add(mesh);
|
|
||||||
en1.Erase();
|
|
||||||
en2.Erase();
|
|
||||||
}
|
|
||||||
protected Operate(csg1: CSG, csg2: CSG): CSG
|
|
||||||
{
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CSGUnion extends CSGOperation
|
|
||||||
{
|
|
||||||
protected Operate(csg1: CSG, csg2: CSG)
|
|
||||||
{
|
|
||||||
return csg1.union(csg2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CSGSubtraction extends CSGOperation
|
|
||||||
{
|
|
||||||
protected Operate(csg1: CSG, csg2: CSG)
|
|
||||||
{
|
|
||||||
return csg1.subtract(csg2);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,53 @@
|
|||||||
|
import { Face3, Geometry, Mesh, Object3D, Vector3 } from "three";
|
||||||
|
import { RenderType } from "../GraphicsSystem/RenderType";
|
||||||
|
import { CADFiler } from "./CADFiler";
|
||||||
|
import { Entity } from "./Entity";
|
||||||
|
import { Factory } from "./CADFactory";
|
||||||
|
|
||||||
|
@Factory
|
||||||
|
export class FaceEntity extends Entity
|
||||||
|
{
|
||||||
|
constructor(private p1: Vector3 = new Vector3(), private p2: Vector3 = new Vector3(), private p3: Vector3 = new Vector3(), private normal: Vector3 = new Vector3())
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected InitDrawObject(renderType: RenderType = RenderType.Wireframe): Object3D
|
||||||
|
{
|
||||||
|
let g = new Geometry();
|
||||||
|
|
||||||
|
g.vertices.push(this.p1, this.p2, this.p3);
|
||||||
|
g.faces.push(new Face3(0, 1, 2));
|
||||||
|
|
||||||
|
return new Mesh(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//对象从文件中读取数据,初始化自身
|
||||||
|
ReadFile(file: CADFiler)
|
||||||
|
{
|
||||||
|
let ver = file.Read();
|
||||||
|
super.ReadFile(file);
|
||||||
|
|
||||||
|
this.p1.fromArray(file.Read());
|
||||||
|
this.p2.fromArray(file.Read());
|
||||||
|
this.p3.fromArray(file.Read());
|
||||||
|
|
||||||
|
this.normal.fromArray(file.Read());
|
||||||
|
|
||||||
|
}
|
||||||
|
//对象将自身数据写入到文件.
|
||||||
|
WriteFile(file: CADFiler)
|
||||||
|
{
|
||||||
|
file.Write(1);
|
||||||
|
super.WriteFile(file);
|
||||||
|
|
||||||
|
file.Write(this.p1.toArray());
|
||||||
|
file.Write(this.p2.toArray());
|
||||||
|
file.Write(this.p3.toArray());
|
||||||
|
|
||||||
|
file.Write(this.normal.toArray());
|
||||||
|
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
import { Shape, Vector2, EllipseCurve } from "three";
|
||||||
|
import { equalv2 } from "../Geometry/GeUtils";
|
||||||
|
|
||||||
|
export class Shape2 extends Shape
|
||||||
|
{
|
||||||
|
getPoints(divisions: number = 12)
|
||||||
|
{
|
||||||
|
divisions = divisions || 12;
|
||||||
|
let points = [], last: Vector2;
|
||||||
|
for (let i = 0, curves = this.curves; i < curves.length; i++)
|
||||||
|
{
|
||||||
|
let curve = curves[i];
|
||||||
|
//@ts-ignore
|
||||||
|
let resolution = (curve && curve.isEllipseCurve) ? divisions * 2
|
||||||
|
//@ts-ignore
|
||||||
|
: (curve && (curve.isLineCurve || curve.isLineCurve3)) ? 1
|
||||||
|
//@ts-ignore
|
||||||
|
: (curve && curve.isSplineCurve) ? divisions * curve.points.length
|
||||||
|
: divisions;
|
||||||
|
|
||||||
|
let pts = curve.getPoints(resolution);
|
||||||
|
|
||||||
|
for (let j = 0; j < pts.length; j++)
|
||||||
|
{
|
||||||
|
let point = pts[j];
|
||||||
|
if (last && equalv2(last, point, 1e-4)) continue; // ensures no consecutive points are duplicates
|
||||||
|
points.push(point);
|
||||||
|
last = point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.autoClose && points.length > 1 && !points[points.length - 1].equals(points[0]))
|
||||||
|
{
|
||||||
|
points.push(points[0]);
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
absellipse(aX: number, aY: number, xRadius: number, yRadius: number, aStartAngle: number, aEndAngle: number, aClockwise: boolean, aRotation: number)
|
||||||
|
{
|
||||||
|
let curve = new EllipseCurve(aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation);
|
||||||
|
|
||||||
|
/*
|
||||||
|
if (this.curves.length > 0)
|
||||||
|
{
|
||||||
|
// if a previous curve is present, attempt to join
|
||||||
|
let firstPoint = curve.getPoint(0);
|
||||||
|
if (!equalv2(firstPoint, this.currentPoint))
|
||||||
|
{
|
||||||
|
this.lineTo(firstPoint.x, firstPoint.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
this.curves.push(curve);
|
||||||
|
|
||||||
|
let lastPoint = curve.getPoint(1);
|
||||||
|
this.currentPoint.copy(lastPoint);
|
||||||
|
}
|
||||||
|
}
|
@ -1,506 +0,0 @@
|
|||||||
import { BufferGeometry, Face3, Geometry, Material, Matrix4, Mesh, Vector2, Vector3 } from "three";
|
|
||||||
|
|
||||||
//ref: https://github.com/chandlerprall/ThreeCSG/blob/master/threeCSG.es6
|
|
||||||
|
|
||||||
|
|
||||||
const EPSILON = 1e-5;
|
|
||||||
|
|
||||||
enum Side
|
|
||||||
{
|
|
||||||
Coplanar = 0,
|
|
||||||
Front = 1,
|
|
||||||
Back = 2,
|
|
||||||
Spanning = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CSG
|
|
||||||
{
|
|
||||||
tree: Node;
|
|
||||||
matrix: Matrix4;
|
|
||||||
constructor(obj: Geometry | Mesh | Node, matrix?: Matrix4)
|
|
||||||
{
|
|
||||||
let geometry: Geometry;
|
|
||||||
if (obj instanceof Geometry)
|
|
||||||
{
|
|
||||||
geometry = obj;
|
|
||||||
this.matrix = matrix || new Matrix4();
|
|
||||||
}
|
|
||||||
else if (obj instanceof Mesh)
|
|
||||||
{
|
|
||||||
// #todo: add hierarchy support
|
|
||||||
this.matrix = obj.matrix.clone();
|
|
||||||
let geo = obj.geometry;
|
|
||||||
if (geo instanceof BufferGeometry)
|
|
||||||
geometry = new Geometry().fromBufferGeometry(geo);
|
|
||||||
else
|
|
||||||
geometry = geo;
|
|
||||||
}
|
|
||||||
else if (obj instanceof Node)
|
|
||||||
{
|
|
||||||
this.tree = obj;
|
|
||||||
this.matrix = matrix || new Matrix4();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw '未支持的类型';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let polgons: Polygon[] = [];
|
|
||||||
for (let i = 0; i < geometry.faces.length; i++)
|
|
||||||
{
|
|
||||||
let face = geometry.faces[i];
|
|
||||||
let faceVertexUvs = geometry.faceVertexUvs[0][i];
|
|
||||||
let polygon = new Polygon();
|
|
||||||
|
|
||||||
if (face instanceof Face3)
|
|
||||||
{
|
|
||||||
let uvs = faceVertexUvs ? faceVertexUvs[0].clone() : null;
|
|
||||||
let vertex1 = new Vertex(geometry.vertices[face.a], face.vertexNormals[0], uvs);
|
|
||||||
vertex1.applyMatrix4(this.matrix);
|
|
||||||
polygon.vertices.push(vertex1);
|
|
||||||
|
|
||||||
uvs = faceVertexUvs ? faceVertexUvs[1].clone() : null;
|
|
||||||
let vertex2 = new Vertex(geometry.vertices[face.b], face.vertexNormals[1], uvs);
|
|
||||||
vertex2.applyMatrix4(this.matrix);
|
|
||||||
polygon.vertices.push(vertex2);
|
|
||||||
|
|
||||||
uvs = faceVertexUvs ? faceVertexUvs[2].clone() : null;
|
|
||||||
let vertex3 = new Vertex(geometry.vertices[face.c], face.vertexNormals[2], uvs);
|
|
||||||
vertex3.applyMatrix4(this.matrix);
|
|
||||||
polygon.vertices.push(vertex3);
|
|
||||||
}
|
|
||||||
|
|
||||||
polygon.calculateProperties();
|
|
||||||
polgons.push(polygon);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tree = new Node(polgons);
|
|
||||||
}
|
|
||||||
|
|
||||||
subtract(other_tree: CSG)
|
|
||||||
{
|
|
||||||
let a = this.tree.clone(),
|
|
||||||
b = other_tree.tree.clone();
|
|
||||||
|
|
||||||
a.invert();
|
|
||||||
a.clipTo(b);
|
|
||||||
b.clipTo(a);
|
|
||||||
b.invert();
|
|
||||||
b.clipTo(a);
|
|
||||||
b.invert();
|
|
||||||
a.build(b.allPolygons());
|
|
||||||
a.invert();
|
|
||||||
return new CSG(a, this.matrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
union(other_tree: CSG)
|
|
||||||
{
|
|
||||||
let a = this.tree.clone(),
|
|
||||||
b = other_tree.tree.clone();
|
|
||||||
|
|
||||||
a.clipTo(b);
|
|
||||||
b.clipTo(a);
|
|
||||||
b.invert();
|
|
||||||
b.clipTo(a);
|
|
||||||
b.invert();
|
|
||||||
a.build(b.allPolygons());
|
|
||||||
return new CSG(a, this.matrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
intersect(other_tree: CSG)
|
|
||||||
{
|
|
||||||
let a = this.tree.clone(),
|
|
||||||
b = other_tree.tree.clone();
|
|
||||||
|
|
||||||
a.invert();
|
|
||||||
b.clipTo(a);
|
|
||||||
b.invert();
|
|
||||||
a.clipTo(b);
|
|
||||||
b.clipTo(a);
|
|
||||||
a.build(b.allPolygons());
|
|
||||||
a.invert();
|
|
||||||
return new CSG(a, this.matrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
toGeometry()
|
|
||||||
{
|
|
||||||
let matrix = new Matrix4().getInverse(this.matrix),
|
|
||||||
geometry = new Geometry(),
|
|
||||||
polygons = this.tree.allPolygons(),
|
|
||||||
polygon_count = polygons.length,
|
|
||||||
vertice_dict = {},
|
|
||||||
vertex_idx_a: number, vertex_idx_b: number, vertex_idx_c: number;
|
|
||||||
|
|
||||||
let m0 = matrix.clone().setPosition(new Vector3());
|
|
||||||
|
|
||||||
for (let i = 0; i < polygon_count; i++)
|
|
||||||
{
|
|
||||||
let polygon = polygons[i];
|
|
||||||
let polygon_vertice_count = polygon.vertices.length;
|
|
||||||
|
|
||||||
let normal = polygon.normal.clone().applyMatrix4(m0);
|
|
||||||
|
|
||||||
for (let j = 2; j < polygon_vertice_count; j++)
|
|
||||||
{
|
|
||||||
let verticeUvs = [];
|
|
||||||
|
|
||||||
let vertex = polygon.vertices[0];
|
|
||||||
verticeUvs.push(new Vector2(vertex.uv.x, vertex.uv.y));
|
|
||||||
let vertex1 = new Vector3(vertex.x, vertex.y, vertex.z);
|
|
||||||
vertex1.applyMatrix4(matrix);
|
|
||||||
|
|
||||||
if (typeof vertice_dict[vertex1.x + ',' + vertex1.y + ',' + vertex1.z] !== 'undefined')
|
|
||||||
{
|
|
||||||
vertex_idx_a = vertice_dict[vertex1.x + ',' + vertex1.y + ',' + vertex1.z];
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
geometry.vertices.push(vertex1);
|
|
||||||
vertex_idx_a = vertice_dict[vertex1.x + ',' + vertex1.y + ',' + vertex1.z] = geometry.vertices.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
vertex = polygon.vertices[j - 1];
|
|
||||||
verticeUvs.push(new Vector2(vertex.uv.x, vertex.uv.y));
|
|
||||||
let vertex2 = new Vector3(vertex.x, vertex.y, vertex.z);
|
|
||||||
vertex2.applyMatrix4(matrix);
|
|
||||||
if (typeof vertice_dict[vertex2.x + ',' + vertex2.y + ',' + vertex2.z] !== 'undefined')
|
|
||||||
{
|
|
||||||
vertex_idx_b = vertice_dict[vertex2.x + ',' + vertex2.y + ',' + vertex2.z];
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
geometry.vertices.push(vertex2);
|
|
||||||
vertex_idx_b = vertice_dict[vertex2.x + ',' + vertex2.y + ',' + vertex2.z] = geometry.vertices.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
vertex = polygon.vertices[j];
|
|
||||||
verticeUvs.push(new Vector2(vertex.uv.x, vertex.uv.y));
|
|
||||||
let vertex3 = new Vector3(vertex.x, vertex.y, vertex.z);
|
|
||||||
vertex3.applyMatrix4(matrix);
|
|
||||||
if (typeof vertice_dict[vertex3.x + ',' + vertex3.y + ',' + vertex3.z] !== 'undefined')
|
|
||||||
{
|
|
||||||
vertex_idx_c = vertice_dict[vertex3.x + ',' + vertex3.y + ',' + vertex3.z];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
geometry.vertices.push(vertex3);
|
|
||||||
vertex_idx_c = vertice_dict[vertex3.x + ',' + vertex3.y + ',' + vertex3.z] = geometry.vertices.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let face = new Face3(
|
|
||||||
vertex_idx_a,
|
|
||||||
vertex_idx_b,
|
|
||||||
vertex_idx_c,
|
|
||||||
normal
|
|
||||||
);
|
|
||||||
|
|
||||||
geometry.faces.push(face);
|
|
||||||
geometry.faceVertexUvs[0].push(verticeUvs);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return geometry;
|
|
||||||
}
|
|
||||||
|
|
||||||
toMesh(material?: Material | Material[])
|
|
||||||
{
|
|
||||||
let geometry = this.toGeometry(),
|
|
||||||
mesh = new Mesh(geometry, material);
|
|
||||||
|
|
||||||
mesh.applyMatrix(this.matrix)
|
|
||||||
return mesh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Polygon
|
|
||||||
{
|
|
||||||
constructor(
|
|
||||||
public vertices: Vertex[] = [],
|
|
||||||
public normal?: Vector3,
|
|
||||||
public w?: number)
|
|
||||||
{
|
|
||||||
if (vertices.length > 0 && !normal)
|
|
||||||
this.calculateProperties();
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateProperties()
|
|
||||||
{
|
|
||||||
let a = this.vertices[0],
|
|
||||||
b = this.vertices[1],
|
|
||||||
c = this.vertices[2];
|
|
||||||
|
|
||||||
this.normal = b.clone().sub(a)
|
|
||||||
.cross(c.clone().sub(a))
|
|
||||||
.normalize();
|
|
||||||
|
|
||||||
this.w = this.normal.dot(a);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
clone()
|
|
||||||
{
|
|
||||||
return new Polygon(
|
|
||||||
this.vertices.map(v => v.clone()),
|
|
||||||
this.normal.clone(),
|
|
||||||
this.w
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
flip()
|
|
||||||
{
|
|
||||||
this.normal.multiplyScalar(-1);
|
|
||||||
this.w *= -1;
|
|
||||||
this.vertices.reverse();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
classifyVertex(vertex: Vector3 | Vertex): Side
|
|
||||||
{
|
|
||||||
let side_value = this.normal.dot(vertex) - this.w;
|
|
||||||
|
|
||||||
if (side_value < -EPSILON)
|
|
||||||
{
|
|
||||||
return Side.Back;
|
|
||||||
}
|
|
||||||
else if (side_value > EPSILON)
|
|
||||||
{
|
|
||||||
return Side.Front;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return Side.Coplanar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
classifySide(polygon: Polygon): Side
|
|
||||||
{
|
|
||||||
let num_positive = 0,
|
|
||||||
num_negative = 0,
|
|
||||||
vertice_count = polygon.vertices.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < vertice_count; i++)
|
|
||||||
{
|
|
||||||
let vertex = polygon.vertices[i];
|
|
||||||
let classification = this.classifyVertex(vertex);
|
|
||||||
if (classification === Side.Front)
|
|
||||||
num_positive++;
|
|
||||||
else if (classification === Side.Back)
|
|
||||||
num_negative++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (num_positive > 0 && num_negative === 0)
|
|
||||||
return Side.Front;
|
|
||||||
else if (num_positive === 0 && num_negative > 0)
|
|
||||||
return Side.Back;
|
|
||||||
else if (num_positive === 0 && num_negative === 0)
|
|
||||||
return Side.Coplanar;
|
|
||||||
else
|
|
||||||
return Side.Spanning;
|
|
||||||
}
|
|
||||||
|
|
||||||
splitPolygon(polygon: Polygon, coplanar_front: Polygon[], coplanar_back: Polygon[], front: Polygon[], back: Polygon[])
|
|
||||||
{
|
|
||||||
let classification = this.classifySide(polygon);
|
|
||||||
|
|
||||||
if (classification === Side.Coplanar)
|
|
||||||
{
|
|
||||||
(this.normal.dot(polygon.normal) > 0 ? coplanar_front : coplanar_back).push(polygon);
|
|
||||||
}
|
|
||||||
else if (classification === Side.Front)
|
|
||||||
{
|
|
||||||
front.push(polygon);
|
|
||||||
}
|
|
||||||
else if (classification === Side.Back)
|
|
||||||
{
|
|
||||||
back.push(polygon);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
let f = [];
|
|
||||||
let b = [];
|
|
||||||
|
|
||||||
for (let i = 0, vertice_count = polygon.vertices.length; i < vertice_count; i++)
|
|
||||||
{
|
|
||||||
let j = (i + 1) % vertice_count;
|
|
||||||
let vi = polygon.vertices[i];
|
|
||||||
let vj = polygon.vertices[j];
|
|
||||||
let ti = this.classifyVertex(vi);
|
|
||||||
let tj = this.classifyVertex(vj);
|
|
||||||
|
|
||||||
if (ti != Side.Back) f.push(vi);
|
|
||||||
if (ti != Side.Front) b.push(vi);
|
|
||||||
if ((ti | tj) === Side.Spanning)
|
|
||||||
{
|
|
||||||
let t = (this.w - this.normal.dot(vi)) / this.normal.dot(vj.clone().sub(vi));
|
|
||||||
let v = vi.clone().lerp(vj, t);
|
|
||||||
f.push(v);
|
|
||||||
b.push(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (f.length >= 3) front.push(new Polygon(f).calculateProperties());
|
|
||||||
if (b.length >= 3) back.push(new Polygon(b).calculateProperties());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Vertex extends Vector3
|
|
||||||
{
|
|
||||||
constructor(
|
|
||||||
pos: Vector3,
|
|
||||||
public normal = new Vector3(),
|
|
||||||
public uv = new Vector2())
|
|
||||||
{
|
|
||||||
super(pos.x, pos.y, pos.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
clone(): Vertex
|
|
||||||
{
|
|
||||||
return new Vertex(this, this.normal.clone(), this.uv.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
lerp(v: Vertex, alpha: number)
|
|
||||||
{
|
|
||||||
super.lerp(v, alpha);
|
|
||||||
this.normal.lerp(v.normal, alpha);
|
|
||||||
this.uv.lerp(v.uv, alpha);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Node
|
|
||||||
{
|
|
||||||
divider: Polygon;
|
|
||||||
back: Node;
|
|
||||||
front: Node;
|
|
||||||
constructor(public polygons: Polygon[] = [])
|
|
||||||
{
|
|
||||||
let front: Polygon[] = [],
|
|
||||||
back: Polygon[] = [];
|
|
||||||
|
|
||||||
this.front = this.back = undefined;
|
|
||||||
|
|
||||||
if (polygons.length === 0) return;
|
|
||||||
|
|
||||||
this.divider = polygons[0].clone();
|
|
||||||
|
|
||||||
for (let i = 0, polygon_count = polygons.length; i < polygon_count; i++)
|
|
||||||
{
|
|
||||||
this.divider.splitPolygon(polygons[i], this.polygons, this.polygons, front, back);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (front.length > 0)
|
|
||||||
{
|
|
||||||
this.front = new Node(front);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (back.length > 0)
|
|
||||||
{
|
|
||||||
this.back = new Node(back);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isConvex(polygons: Polygon[])
|
|
||||||
{
|
|
||||||
for (let i = 0; i < polygons.length; i++)
|
|
||||||
{
|
|
||||||
for (let j = 0; j < polygons.length; j++)
|
|
||||||
{
|
|
||||||
if (i !== j && polygons[i].classifySide(polygons[j]) !== Side.Back)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
build(polygons: Polygon[])
|
|
||||||
{
|
|
||||||
let front: Polygon[] = [],
|
|
||||||
back: Polygon[] = [];
|
|
||||||
|
|
||||||
if (!this.divider)
|
|
||||||
{
|
|
||||||
this.divider = polygons[0].clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0, polygon_count = polygons.length; i < polygon_count; i++)
|
|
||||||
{
|
|
||||||
this.divider.splitPolygon(polygons[i], this.polygons, this.polygons, front, back);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (front.length > 0)
|
|
||||||
{
|
|
||||||
if (!this.front) this.front = new Node();
|
|
||||||
this.front.build(front);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (back.length > 0)
|
|
||||||
{
|
|
||||||
if (!this.back) this.back = new Node();
|
|
||||||
this.back.build(back);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allPolygons()
|
|
||||||
{
|
|
||||||
let polygons = this.polygons.slice();
|
|
||||||
if (this.front) polygons = polygons.concat(this.front.allPolygons());
|
|
||||||
if (this.back) polygons = polygons.concat(this.back.allPolygons());
|
|
||||||
return polygons;
|
|
||||||
}
|
|
||||||
|
|
||||||
clone()
|
|
||||||
{
|
|
||||||
let node = new Node();
|
|
||||||
|
|
||||||
node.divider = this.divider.clone();
|
|
||||||
node.polygons = this.polygons.map(p => p.clone());
|
|
||||||
node.front = this.front && this.front.clone();
|
|
||||||
node.back = this.back && this.back.clone();
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
invert()
|
|
||||||
{
|
|
||||||
for (let p of this.polygons)
|
|
||||||
p.flip();
|
|
||||||
|
|
||||||
this.divider.flip();
|
|
||||||
if (this.front) this.front.invert();
|
|
||||||
if (this.back) this.back.invert();
|
|
||||||
|
|
||||||
let temp = this.front;
|
|
||||||
this.front = this.back;
|
|
||||||
this.back = temp;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
clipPolygons(polygons: Polygon[])
|
|
||||||
{
|
|
||||||
if (!this.divider) return polygons.slice();
|
|
||||||
|
|
||||||
let front: Polygon[] = [];
|
|
||||||
let back: Polygon[] = [];
|
|
||||||
|
|
||||||
for (let polygon of polygons)
|
|
||||||
this.divider.splitPolygon(polygon, front, back, front, back);
|
|
||||||
|
|
||||||
if (this.front) front = this.front.clipPolygons(front);
|
|
||||||
if (this.back) back = this.back.clipPolygons(back);
|
|
||||||
else back = [];
|
|
||||||
|
|
||||||
return front.concat(back);
|
|
||||||
}
|
|
||||||
|
|
||||||
clipTo(node: Node)
|
|
||||||
{
|
|
||||||
this.polygons = node.clipPolygons(this.polygons);
|
|
||||||
if (this.front) this.front.clipTo(node);
|
|
||||||
if (this.back) this.back.clipTo(node);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,100 @@
|
|||||||
|
import { Matrix4 } from "three";
|
||||||
|
import { CSG } from "./CSG";
|
||||||
|
import { Polygon } from "./math/Polygon3";
|
||||||
|
import { Side } from "./math/Side";
|
||||||
|
import { Vector2D } from "./math/Vector2";
|
||||||
|
import { Vector3D } from "./math/Vector3";
|
||||||
|
import { Vertex3D } from "./math/Vertex3";
|
||||||
|
import { Tree } from "./trees";
|
||||||
|
import { canonicalizeCAG } from "./utils/canonicalize";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class CAG
|
||||||
|
* Holds a solid area geometry like CSG but 2D.
|
||||||
|
* Each area consists of a number of sides.
|
||||||
|
* Each side is a line between 2 points.
|
||||||
|
*/
|
||||||
|
export class CAG
|
||||||
|
{
|
||||||
|
isCanonicalized: boolean = false;
|
||||||
|
constructor(public sides: Side[] = [])
|
||||||
|
{
|
||||||
|
}
|
||||||
|
flipped()
|
||||||
|
{
|
||||||
|
let newsides = this.sides.map(side => side.flipped());
|
||||||
|
newsides.reverse();
|
||||||
|
return new CAG(newsides);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBounds()
|
||||||
|
{
|
||||||
|
let minpoint: Vector2D;
|
||||||
|
if (this.sides.length === 0)
|
||||||
|
minpoint = new Vector2D(0, 0);
|
||||||
|
else
|
||||||
|
minpoint = this.sides[0].vertex0.pos;
|
||||||
|
let maxpoint = minpoint.clone();
|
||||||
|
this.sides.forEach(side =>
|
||||||
|
{
|
||||||
|
minpoint.min(side.vertex0.pos);
|
||||||
|
minpoint.min(side.vertex1.pos);
|
||||||
|
maxpoint.max(side.vertex0.pos);
|
||||||
|
maxpoint.max(side.vertex1.pos);
|
||||||
|
});
|
||||||
|
return [minpoint, maxpoint];
|
||||||
|
}
|
||||||
|
|
||||||
|
canonicalized(): CAG
|
||||||
|
{
|
||||||
|
if (this.isCanonicalized) return this;
|
||||||
|
|
||||||
|
return canonicalizeCAG(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
extrude(offsetVector: Vector3D): CSG
|
||||||
|
{
|
||||||
|
//bottom
|
||||||
|
let polygons = this.toPolygons(true);
|
||||||
|
//top
|
||||||
|
let moveMtx4 = new Matrix4().setPosition(offsetVector);
|
||||||
|
polygons.push(
|
||||||
|
...polygons.map(poly => poly.flipped().transform(moveMtx4)),
|
||||||
|
...this.toCSGWall(0, offsetVector.z).polygons
|
||||||
|
);
|
||||||
|
|
||||||
|
return new CSG(polygons);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toCSGWall(z0: number, z1: number)
|
||||||
|
{
|
||||||
|
let polygons = this.sides.map(side => side.toPolygon3D(z0, z1));
|
||||||
|
return new CSG(polygons);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toPolygons(bottom = false)
|
||||||
|
{
|
||||||
|
let bounds = this.getBounds();
|
||||||
|
bounds[0].sub(new Vector2D(1, 1));
|
||||||
|
bounds[1].add(new Vector2D(1, 1));
|
||||||
|
let csgShell = this.toCSGWall(-1, 1);
|
||||||
|
let csgPlane = new CSG([
|
||||||
|
new Polygon([
|
||||||
|
new Vertex3D(new Vector3D(bounds[0].x, bounds[0].y, 0)),
|
||||||
|
new Vertex3D(new Vector3D(bounds[1].x, bounds[0].y, 0)),
|
||||||
|
new Vertex3D(new Vector3D(bounds[1].x, bounds[1].y, 0)),
|
||||||
|
new Vertex3D(new Vector3D(bounds[0].x, bounds[1].y, 0))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
if (bottom)
|
||||||
|
csgPlane = csgPlane.invert();
|
||||||
|
|
||||||
|
//简化代码
|
||||||
|
let a = new Tree(csgPlane.polygons);
|
||||||
|
let b = new Tree(csgShell.polygons);
|
||||||
|
b.invert();
|
||||||
|
a.clipTo(b);
|
||||||
|
|
||||||
|
return a.allPolygons();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import { CAG } from "./CAG";
|
||||||
|
import { CSG } from "./CSG";
|
||||||
|
import { Side } from "./math/Side";
|
||||||
|
import { Vector2D } from "./math/Vector2";
|
||||||
|
import { Vertex2D } from "./math/Vertex2";
|
||||||
|
|
||||||
|
// Converts a CSG to a The CSG must consist of polygons with only z coordinates +1 and -1
|
||||||
|
// as constructed by _toCSGWall(-1, 1). This is so we can use the 3D union(), intersect() etc
|
||||||
|
export function fromFakeCSG(csg: CSG)
|
||||||
|
{
|
||||||
|
let sides = csg.polygons
|
||||||
|
.map(p => Side._fromFakePolygon(p))
|
||||||
|
.filter(s => s !== null);
|
||||||
|
return new CAG(sides);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Construct a CAG from a list of points (a polygon).
|
||||||
|
* Like fromPoints() but does not check if the result is a valid polygon.
|
||||||
|
* The points MUST rotate counter clockwise.
|
||||||
|
* The points can define a convex or a concave polygon.
|
||||||
|
* The polygon must not self intersect.
|
||||||
|
*/
|
||||||
|
export function fromPointsNoCheck(points: Vector2D[]): CAG
|
||||||
|
{
|
||||||
|
let sides: Side[] = [];
|
||||||
|
let prevpoint = points[points.length - 1];
|
||||||
|
let prevvertex = new Vertex2D(prevpoint);
|
||||||
|
for (let point of points)
|
||||||
|
{
|
||||||
|
let vertex = new Vertex2D(point);
|
||||||
|
let side = new Side(prevvertex, vertex);
|
||||||
|
sides.push(side);
|
||||||
|
prevvertex = vertex;
|
||||||
|
}
|
||||||
|
return new CAG(sides);
|
||||||
|
}
|
@ -0,0 +1,314 @@
|
|||||||
|
import { Matrix4 } from "three";
|
||||||
|
import { IsMirror } from "./math/IsMirrot";
|
||||||
|
import { Plane } from "./math/Plane";
|
||||||
|
import { Polygon } from "./math/Polygon3";
|
||||||
|
import { Vector3D } from "./math/Vector3";
|
||||||
|
import { Vertex3D } from "./math/Vertex3";
|
||||||
|
import { Tree } from "./trees";
|
||||||
|
import { canonicalizeCSG } from "./utils/canonicalize";
|
||||||
|
import { bounds } from "./utils/csgMeasurements";
|
||||||
|
import { reTesselate } from "./utils/retesellate";
|
||||||
|
|
||||||
|
/** Class CSG
|
||||||
|
* Holds a binary space partition tree representing a 3D solid. Two solids can
|
||||||
|
* be combined using the `union()`, `subtract()`, and `intersect()` methods.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export class CSG
|
||||||
|
{
|
||||||
|
isCanonicalized: boolean = false;
|
||||||
|
isRetesselated: boolean = false;
|
||||||
|
cachedBoundingBox: Vector3D[];
|
||||||
|
constructor(public polygons: Polygon[] = [])
|
||||||
|
{
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return a new CSG solid representing the space in either this solid or
|
||||||
|
* in the given solids. Neither this solid nor the given solids are modified.
|
||||||
|
* @param {CSG[]} csg - list of CSG objects
|
||||||
|
* @returns {CSG} new CSG object
|
||||||
|
* @example
|
||||||
|
* let C = A.union(B)
|
||||||
|
* @example
|
||||||
|
* +-------+ +-------+
|
||||||
|
* | | | |
|
||||||
|
* | A | | |
|
||||||
|
* | +--+----+ = | +----+
|
||||||
|
* +----+--+ | +----+ |
|
||||||
|
* | B | | |
|
||||||
|
* | | | |
|
||||||
|
* +-------+ +-------+
|
||||||
|
*/
|
||||||
|
union(csg: CSG | CSG[]): CSG
|
||||||
|
{
|
||||||
|
let csgs: CSG[];
|
||||||
|
if (csg instanceof Array)
|
||||||
|
{
|
||||||
|
csgs = csg.slice(0);
|
||||||
|
csgs.push(this);
|
||||||
|
}
|
||||||
|
else csgs = [this, csg];
|
||||||
|
|
||||||
|
let i: number;
|
||||||
|
// combine csg pairs in a way that forms a balanced binary tree pattern
|
||||||
|
for (i = 1; i < csgs.length; i += 2)
|
||||||
|
{
|
||||||
|
csgs.push(csgs[i - 1].unionSub(csgs[i]));
|
||||||
|
}
|
||||||
|
return csgs[i - 1].reTesselated().canonicalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
unionSub(csg: CSG, retesselate = false, canonicalize = false): CSG
|
||||||
|
{
|
||||||
|
if (!this.mayOverlap(csg))
|
||||||
|
return this.unionForNonIntersecting(csg);
|
||||||
|
|
||||||
|
let a = new Tree(this.polygons);
|
||||||
|
let b = new Tree(csg.polygons);
|
||||||
|
a.clipTo(b);
|
||||||
|
|
||||||
|
// b.clipTo(a, true); // ERROR: this doesn't work
|
||||||
|
b.clipTo(a);
|
||||||
|
b.invert();
|
||||||
|
b.clipTo(a);
|
||||||
|
b.invert();
|
||||||
|
|
||||||
|
let newpolygons = [...a.allPolygons(), ...b.allPolygons()];
|
||||||
|
let resultCSG = new CSG(newpolygons);
|
||||||
|
if (retesselate) resultCSG = resultCSG.reTesselated();
|
||||||
|
if (canonicalize) resultCSG = resultCSG.canonicalized();
|
||||||
|
return resultCSG;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like union, but when we know that the two solids are not intersecting
|
||||||
|
// Do not use if you are not completely sure that the solids do not intersect!
|
||||||
|
unionForNonIntersecting(csg: CSG): CSG
|
||||||
|
{
|
||||||
|
let newpolygons = [...this.polygons, ...csg.polygons];
|
||||||
|
let result = new CSG(newpolygons);
|
||||||
|
result.isCanonicalized = this.isCanonicalized && csg.isCanonicalized;
|
||||||
|
result.isRetesselated = this.isRetesselated && csg.isRetesselated;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new CSG solid representing space in this solid but
|
||||||
|
* not in the given solids. Neither this solid nor the given solids are modified.
|
||||||
|
* @returns new CSG object
|
||||||
|
* @example
|
||||||
|
* let C = A.subtract(B)
|
||||||
|
* @example
|
||||||
|
* +-------+ +-------+
|
||||||
|
* | | | |
|
||||||
|
* | A | | |
|
||||||
|
* | +--+----+ = | +--+
|
||||||
|
* +----+--+ | +----+
|
||||||
|
* | B |
|
||||||
|
* | |
|
||||||
|
* +-------+
|
||||||
|
*/
|
||||||
|
subtract(csg: CSG | CSG[]): CSG
|
||||||
|
{
|
||||||
|
let csgs: CSG[];
|
||||||
|
if (csg instanceof Array)
|
||||||
|
csgs = csg;
|
||||||
|
else
|
||||||
|
csgs = [csg];
|
||||||
|
let result: CSG = this;
|
||||||
|
for (let i = 0; i < csgs.length; i++)
|
||||||
|
{
|
||||||
|
let islast = i === csgs.length - 1;
|
||||||
|
result = result.subtractSub(csgs[i], islast, islast);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
subtractSub(csg: CSG, retesselate = false, canonicalize = false): CSG
|
||||||
|
{
|
||||||
|
let a = new Tree(this.polygons);
|
||||||
|
let b = new Tree(csg.polygons);
|
||||||
|
a.invert();
|
||||||
|
a.clipTo(b);
|
||||||
|
b.clipTo(a, true);
|
||||||
|
a.addPolygons(b.allPolygons());
|
||||||
|
a.invert();
|
||||||
|
let result = new CSG(a.allPolygons());
|
||||||
|
// if (retesselate) result = result.reTesselated();
|
||||||
|
// if (canonicalize) result = result.canonicalized();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new CSG solid representing space in both this solid and
|
||||||
|
* in the given solids. Neither this solid nor the given solids are modified.
|
||||||
|
* let C = A.intersect(B)
|
||||||
|
* @returns new CSG object
|
||||||
|
* @example
|
||||||
|
* +-------+
|
||||||
|
* | |
|
||||||
|
* | A |
|
||||||
|
* | +--+----+ = +--+
|
||||||
|
* +----+--+ | +--+
|
||||||
|
* | B |
|
||||||
|
* | |
|
||||||
|
* +-------+
|
||||||
|
*/
|
||||||
|
intersect(csg: CSG | CSG[]): CSG
|
||||||
|
{
|
||||||
|
let csgs: CSG[];
|
||||||
|
if (csg instanceof Array)
|
||||||
|
csgs = csg;
|
||||||
|
else
|
||||||
|
csgs = [csg];
|
||||||
|
let result: CSG = this;
|
||||||
|
for (let i = 0; i < csgs.length; i++)
|
||||||
|
{
|
||||||
|
let islast = i === csgs.length - 1;
|
||||||
|
result = result.intersectSub(csgs[i], islast, islast);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
intersectSub(csg: CSG, retesselate = false, canonicalize = false): CSG
|
||||||
|
{
|
||||||
|
let a = new Tree(this.polygons);
|
||||||
|
let b = new Tree(csg.polygons);
|
||||||
|
a.invert();
|
||||||
|
b.clipTo(a);
|
||||||
|
b.invert();
|
||||||
|
a.clipTo(b);
|
||||||
|
b.clipTo(a);
|
||||||
|
a.addPolygons(b.allPolygons());
|
||||||
|
a.invert();
|
||||||
|
let result = new CSG(a.allPolygons());
|
||||||
|
if (retesselate) result = result.reTesselated();
|
||||||
|
if (canonicalize) result = result.canonicalized();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new CSG solid with solid and empty space switched.
|
||||||
|
* This solid is not modified.
|
||||||
|
*/
|
||||||
|
invert(): CSG
|
||||||
|
{
|
||||||
|
let flippedpolygons = this.polygons.map(p => p.flipped());
|
||||||
|
return new CSG(flippedpolygons);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affine transformation of CSG object. Returns a new CSG object
|
||||||
|
transform1(matrix4x4: Matrix4)
|
||||||
|
{
|
||||||
|
let newpolygons = this.polygons.map(p =>
|
||||||
|
{
|
||||||
|
return p.transform(matrix4x4);
|
||||||
|
});
|
||||||
|
let result = new CSG(newpolygons);
|
||||||
|
result.isCanonicalized = this.isCanonicalized;
|
||||||
|
result.isRetesselated = this.isRetesselated;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new CSG solid that is transformed using the given Matrix.
|
||||||
|
* Several matrix transformations can be combined before transforming this solid.
|
||||||
|
* @param {CSG.Matrix4x4} matrix4x4 - matrix to be applied
|
||||||
|
* @returns {CSG} new CSG object
|
||||||
|
* @example
|
||||||
|
* var m = new CSG.Matrix4x4()
|
||||||
|
* m = m.multiply(CSG.Matrix4x4.rotationX(40))
|
||||||
|
* m = m.multiply(CSG.Matrix4x4.translation([-.5, 0, 0]))
|
||||||
|
* let B = A.transform(m)
|
||||||
|
*/
|
||||||
|
transform(matrix4x4: Matrix4): this
|
||||||
|
{
|
||||||
|
let ismirror = IsMirror(matrix4x4);
|
||||||
|
let transformedvertices = {};
|
||||||
|
let transformedplanes = {};
|
||||||
|
let newpolygons = this.polygons.map(p =>
|
||||||
|
{
|
||||||
|
let newplane: Plane;
|
||||||
|
let plane = p.plane;
|
||||||
|
let planetag = plane.getTag();
|
||||||
|
if (planetag in transformedplanes)
|
||||||
|
{
|
||||||
|
newplane = transformedplanes[planetag];
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
newplane = plane.transform(matrix4x4);
|
||||||
|
transformedplanes[planetag] = newplane;
|
||||||
|
}
|
||||||
|
let newvertices = p.vertices.map(v =>
|
||||||
|
{
|
||||||
|
let newvertex: Vertex3D;
|
||||||
|
let vertextag = v.getTag();
|
||||||
|
if (vertextag in transformedvertices)
|
||||||
|
{
|
||||||
|
newvertex = transformedvertices[vertextag];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newvertex = v.transform(matrix4x4);
|
||||||
|
transformedvertices[vertextag] = newvertex;
|
||||||
|
}
|
||||||
|
return newvertex;
|
||||||
|
});
|
||||||
|
if (ismirror) newvertices.reverse();
|
||||||
|
return new Polygon(newvertices, newplane);
|
||||||
|
});
|
||||||
|
let result = new CSG(newpolygons);
|
||||||
|
result.isRetesselated = this.isRetesselated;
|
||||||
|
result.isCanonicalized = this.isCanonicalized;
|
||||||
|
return result as this;
|
||||||
|
}
|
||||||
|
canonicalized()
|
||||||
|
{
|
||||||
|
if (this.isCanonicalized) return this;
|
||||||
|
return canonicalizeCSG(this);
|
||||||
|
}
|
||||||
|
reTesselated()
|
||||||
|
{
|
||||||
|
if (this.isRetesselated) return this;
|
||||||
|
return reTesselate(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
//如果两个实体有可能重叠,返回true
|
||||||
|
mayOverlap(csg: CSG): boolean
|
||||||
|
{
|
||||||
|
if (this.polygons.length === 0 || csg.polygons.length === 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
let mybounds = bounds(this);
|
||||||
|
let otherbounds = bounds(csg);
|
||||||
|
if (mybounds[1].x < otherbounds[0].x) return false;
|
||||||
|
if (mybounds[0].x > otherbounds[1].x) return false;
|
||||||
|
if (mybounds[1].y < otherbounds[0].y) return false;
|
||||||
|
if (mybounds[0].y > otherbounds[1].y) return false;
|
||||||
|
if (mybounds[1].z < otherbounds[0].z) return false;
|
||||||
|
if (mybounds[0].z > otherbounds[1].z) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
toTriangles(): Polygon[]
|
||||||
|
{
|
||||||
|
let polygons: Polygon[] = [];
|
||||||
|
for (let poly of this.polygons)
|
||||||
|
{
|
||||||
|
let firstVertex = poly.vertices[0];
|
||||||
|
for (let i = poly.vertices.length - 3; i >= 0; i--)
|
||||||
|
{
|
||||||
|
polygons.push(
|
||||||
|
new Polygon(
|
||||||
|
[
|
||||||
|
firstVertex,
|
||||||
|
poly.vertices[i + 1],
|
||||||
|
poly.vertices[i + 2]
|
||||||
|
],
|
||||||
|
poly.plane
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return polygons;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
|
||||||
|
// //////////////////////////////
|
||||||
|
// ## class fuzzyFactory
|
||||||
|
// This class acts as a factory for objects. We can search for an object with approximately
|
||||||
|
// the desired properties (say a rectangle with width 2 and height 1)
|
||||||
|
// The lookupOrCreate() method looks for an existing object (for example it may find an existing rectangle
|
||||||
|
// with width 2.0001 and height 0.999. If no object is found, the user supplied callback is
|
||||||
|
// called, which should generate a new object. The new object is inserted into the database
|
||||||
|
// so it can be found by future lookupOrCreate() calls.
|
||||||
|
// Constructor:
|
||||||
|
// numdimensions: the number of parameters for each object
|
||||||
|
// for example for a 2D rectangle this would be 2
|
||||||
|
// tolerance: The maximum difference for each parameter allowed to be considered a match
|
||||||
|
export class FuzzyFactory
|
||||||
|
{
|
||||||
|
lookuptable: {};
|
||||||
|
multiplier: number;
|
||||||
|
constructor(numdimensions: number, tolerance: number)
|
||||||
|
{
|
||||||
|
this.lookuptable = {};
|
||||||
|
this.multiplier = 1.0 / tolerance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// let obj = f.lookupOrCreate([el1, el2, el3], function(elements) {/* create the new object */});
|
||||||
|
// Performs a fuzzy lookup of the object with the specified elements.
|
||||||
|
// If found, returns the existing object
|
||||||
|
// If not found, calls the supplied callback function which should create a new object with
|
||||||
|
// the specified properties. This object is inserted in the lookup database.
|
||||||
|
lookupOrCreate<T>(els: number[], object: T): T
|
||||||
|
{
|
||||||
|
let hash = "";
|
||||||
|
let multiplier = this.multiplier;
|
||||||
|
els.forEach(el =>
|
||||||
|
{
|
||||||
|
let valueQuantized = Math.round(el * multiplier);
|
||||||
|
hash += valueQuantized + "/";
|
||||||
|
});
|
||||||
|
if (hash in this.lookuptable) return this.lookuptable[hash];
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let hashparts = els.map(el =>
|
||||||
|
{
|
||||||
|
let q0 = Math.floor(el * multiplier);
|
||||||
|
let q1 = q0 + 1;
|
||||||
|
return ["" + q0 + "/", "" + q1 + "/"];
|
||||||
|
});
|
||||||
|
let numelements = els.length;
|
||||||
|
let numhashes = 1 << numelements;
|
||||||
|
for (let hashmask = 0; hashmask < numhashes; ++hashmask)
|
||||||
|
{
|
||||||
|
let hashmaskShifted = hashmask;
|
||||||
|
hash = "";
|
||||||
|
hashparts.forEach(hashpart =>
|
||||||
|
{
|
||||||
|
hash += hashpart[hashmaskShifted & 1];
|
||||||
|
hashmaskShifted >>= 1;
|
||||||
|
});
|
||||||
|
this.lookuptable[hash] = object;
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { EPS } from "./constants";
|
||||||
|
import { FuzzyFactory } from "./FuzzyFactory";
|
||||||
|
import { Side } from "./math/Side";
|
||||||
|
import { Vertex2D } from "./math/Vertex2";
|
||||||
|
|
||||||
|
export class FuzzyCAGFactory
|
||||||
|
{
|
||||||
|
vertexfactory = new FuzzyFactory(2, EPS);
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
getVertex(sourcevertex: Vertex2D)
|
||||||
|
{
|
||||||
|
let elements = [sourcevertex.pos.x, sourcevertex.pos.y];
|
||||||
|
let result = this.vertexfactory.lookupOrCreate(elements, sourcevertex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSide(sourceside: Side)
|
||||||
|
{
|
||||||
|
let vertex0 = this.getVertex(sourceside.vertex0);
|
||||||
|
let vertex1 = this.getVertex(sourceside.vertex1);
|
||||||
|
return new Side(vertex0, vertex1);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
import { FuzzyFactory } from "./FuzzyFactory";
|
||||||
|
import { EPS } from "./constants";
|
||||||
|
import { Polygon } from "./math/Polygon3";
|
||||||
|
import { Plane } from "./math/Plane";
|
||||||
|
import { Vertex3D } from "./math/Vertex3";
|
||||||
|
|
||||||
|
export class FuzzyCSGFactory
|
||||||
|
{
|
||||||
|
vertexfactory = new FuzzyFactory(3, EPS);
|
||||||
|
planefactory = new FuzzyFactory(4, EPS);
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
getVertex(sourcevertex: Vertex3D): Vertex3D
|
||||||
|
{
|
||||||
|
let elements = [sourcevertex.pos.x, sourcevertex.pos.y, sourcevertex.pos.z];
|
||||||
|
let result = this.vertexfactory.lookupOrCreate(elements, sourcevertex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlane(sourceplane: Plane): Plane
|
||||||
|
{
|
||||||
|
let elements: number[] = [sourceplane.normal.x, sourceplane.normal.y, sourceplane.normal.z, sourceplane.w];
|
||||||
|
let result = this.planefactory.lookupOrCreate(elements, sourceplane);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPolygon(sourcePolygon: Polygon, outputPolygon = sourcePolygon): Polygon
|
||||||
|
{
|
||||||
|
let newPlane = this.getPlane(sourcePolygon.plane);
|
||||||
|
let newVertices = sourcePolygon.vertices.map(vertex => this.getVertex(vertex));
|
||||||
|
// two vertices that were originally very close may now have become
|
||||||
|
// truly identical (referring to the same Vertex object).
|
||||||
|
// Remove duplicate vertices:
|
||||||
|
let newVerticesDedup: Vertex3D[] = [];//新的顶点列表(已过滤重复)
|
||||||
|
if (newVertices.length > 0)
|
||||||
|
{
|
||||||
|
let prevVertexTag = newVertices[newVertices.length - 1].getTag();
|
||||||
|
for (let vertex of newVertices)
|
||||||
|
{
|
||||||
|
let vertextag = vertex.getTag();
|
||||||
|
if (vertextag !== prevVertexTag)
|
||||||
|
newVerticesDedup.push(vertex);
|
||||||
|
prevVertexTag = vertextag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If it's degenerate, remove all vertices:
|
||||||
|
if (newVerticesDedup.length < 3)
|
||||||
|
newVerticesDedup = [];
|
||||||
|
|
||||||
|
outputPolygon.vertices = newVertices;
|
||||||
|
outputPolygon.plane = newPlane;
|
||||||
|
return outputPolygon;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
import { Geometry, Face3, Vector3, Vector2 } from "three";
|
||||||
|
import { CSG } from "./CSG";
|
||||||
|
import { Polygon } from "./math/Polygon3";
|
||||||
|
import { Vertex3D } from "./math/Vertex3";
|
||||||
|
import { Vector2D } from "./math/Vector2";
|
||||||
|
import { Vector3D } from "./math/Vector3";
|
||||||
|
|
||||||
|
export function Geometry2CSG(geometry: Geometry): CSG
|
||||||
|
{
|
||||||
|
let polygons: Polygon[] = [];
|
||||||
|
for (let i = 0; i < geometry.faces.length; i++)
|
||||||
|
{
|
||||||
|
let face = geometry.faces[i];
|
||||||
|
let faceVertexUvs = geometry.faceVertexUvs[0][i];
|
||||||
|
let vertices: Vertex3D[] = [];
|
||||||
|
|
||||||
|
if (face instanceof Face3)
|
||||||
|
{
|
||||||
|
let uv = faceVertexUvs ? faceVertexUvs[0].clone() : null;
|
||||||
|
let vertex1 = new Vertex3D(Vector3ToVector3D(geometry.vertices[face.a]), new Vector2D(uv.x, uv.y));
|
||||||
|
vertices.push(vertex1);
|
||||||
|
|
||||||
|
uv = faceVertexUvs ? faceVertexUvs[1].clone() : null;
|
||||||
|
let vertex2 = new Vertex3D(Vector3ToVector3D(geometry.vertices[face.b]), new Vector2D(uv.x, uv.y));
|
||||||
|
vertices.push(vertex2);
|
||||||
|
|
||||||
|
uv = faceVertexUvs ? faceVertexUvs[2].clone() : null;
|
||||||
|
let vertex3 = new Vertex3D(Vector3ToVector3D(geometry.vertices[face.c]), new Vector2D(uv.x, uv.y));
|
||||||
|
vertices.push(vertex3);
|
||||||
|
}
|
||||||
|
|
||||||
|
let polygon = new Polygon(vertices);
|
||||||
|
if (!isNaN(polygon.plane.w))
|
||||||
|
polygons.push(polygon);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CSG(polygons);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CSG2Geometry(csg: CSG): Geometry
|
||||||
|
{
|
||||||
|
let geo = new Geometry;
|
||||||
|
let uvs: Vector2[][] = geo.faceVertexUvs[0];
|
||||||
|
|
||||||
|
for (let poly of csg.polygons)
|
||||||
|
{
|
||||||
|
for (let v of poly.vertices)
|
||||||
|
{
|
||||||
|
v.tag = geo.vertices.length;
|
||||||
|
geo.vertices.push(Vector3DToVector3(v.pos));
|
||||||
|
}
|
||||||
|
let normal = Vector3DToVector3(poly.plane.normal);
|
||||||
|
|
||||||
|
let firstVertex = poly.vertices[0];
|
||||||
|
|
||||||
|
for (let i = poly.vertices.length - 3; i >= 0; i--)
|
||||||
|
{
|
||||||
|
let [a, b, c] = [
|
||||||
|
firstVertex.tag,
|
||||||
|
poly.vertices[i + 1].tag,
|
||||||
|
poly.vertices[i + 2].tag
|
||||||
|
];
|
||||||
|
let f = new Face3(a, b, c, normal);
|
||||||
|
|
||||||
|
geo.faces.push(f);
|
||||||
|
uvs.push([
|
||||||
|
Vector2DToVector2(firstVertex.uv),
|
||||||
|
Vector2DToVector2(poly.vertices[i + 1].uv),
|
||||||
|
Vector2DToVector2(poly.vertices[i + 2].uv)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return geo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Vector3ToVector3D(v: Vector3): Vector3D
|
||||||
|
{
|
||||||
|
return new Vector3D(v.x, v.y, v.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Vector3DToVector3(v: Vector3D): Vector3
|
||||||
|
{
|
||||||
|
return new Vector3(v.x, v.y, v.z);
|
||||||
|
}
|
||||||
|
function Vector2DToVector2(v: Vector2D): Vector2
|
||||||
|
{
|
||||||
|
return new Vector2(v.x, v.y);
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
export const _CSGDEBUG = false;
|
||||||
|
|
||||||
|
/** Epsilon used during determination of near zero distances.
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
export const EPS = 1e-5;
|
||||||
|
|
||||||
|
// Tag factory: we can request a unique tag through CSG.getTag()
|
||||||
|
export let staticTag = 1;
|
||||||
|
export const getTag = () => staticTag++;
|
@ -0,0 +1,15 @@
|
|||||||
|
import { Matrix4, Vector3 } from "three";
|
||||||
|
|
||||||
|
export function IsMirror(mtx: Matrix4)
|
||||||
|
{
|
||||||
|
let x = new Vector3();
|
||||||
|
let y = new Vector3();
|
||||||
|
let z = new Vector3();
|
||||||
|
mtx.extractBasis(x, y, z);
|
||||||
|
|
||||||
|
// for a true orthogonal, non-mirrored base, u.cross(v) == w
|
||||||
|
// If they have an opposite direction then we are mirroring
|
||||||
|
const mirrorvalue = x.cross(y).dot(z);
|
||||||
|
const ismirror = (mirrorvalue < 0);
|
||||||
|
return ismirror;
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Vector2D } from "./Vector2";
|
||||||
|
|
||||||
|
/** class Line2D
|
||||||
|
* Represents a directional line in 2D space
|
||||||
|
* A line is parametrized by its normal vector (perpendicular to the line, rotated 90 degrees counter clockwise)
|
||||||
|
* and w. The line passes through the point <normal>.times(w).
|
||||||
|
* Equation: p is on line if normal.dot(p)==w
|
||||||
|
*/
|
||||||
|
export class Line2D
|
||||||
|
{
|
||||||
|
normal: Vector2D;
|
||||||
|
w: number;
|
||||||
|
constructor(normal: Vector2D, w: number)
|
||||||
|
{
|
||||||
|
this.normal = normal.clone();
|
||||||
|
let l = this.normal.length();
|
||||||
|
w *= l;
|
||||||
|
this.normal.normalize();
|
||||||
|
this.w = w;
|
||||||
|
}
|
||||||
|
|
||||||
|
direction()
|
||||||
|
{
|
||||||
|
return this.normal;
|
||||||
|
}
|
||||||
|
static fromPoints(p1: Vector2D, p2: Vector2D)
|
||||||
|
{
|
||||||
|
let direction = p2.clone().sub(p1);
|
||||||
|
let normal = direction
|
||||||
|
.normal()
|
||||||
|
.negate()
|
||||||
|
.normalize();
|
||||||
|
let w = p1.dot(normal);
|
||||||
|
return new Line2D(normal, w);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
import { Plane } from "./Plane";
|
||||||
|
import { Vector2D } from "./Vector2";
|
||||||
|
import { Vector3D } from "./Vector3";
|
||||||
|
|
||||||
|
/** class OrthoNormalBasis
|
||||||
|
* Reprojects points on a 3D plane onto a 2D plane
|
||||||
|
* or from a 2D plane back onto the 3D plane
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class OrthoNormalBasis
|
||||||
|
{
|
||||||
|
v: Vector3D;
|
||||||
|
u: Vector3D;
|
||||||
|
plane: Plane;
|
||||||
|
planeorigin: Vector3D;
|
||||||
|
constructor(plane: Plane, rightVector: Vector3D = plane.normal.randomNonParallelVector())
|
||||||
|
{
|
||||||
|
this.v = plane.normal.clone().cross(rightVector).normalize();
|
||||||
|
this.u = this.v.clone().cross(plane.normal);
|
||||||
|
this.plane = plane;
|
||||||
|
this.planeorigin = plane.normal.clone().multiplyScalar(plane.w);
|
||||||
|
}
|
||||||
|
to2D(vec3: Vector3D)
|
||||||
|
{
|
||||||
|
return new Vector2D(vec3.dot(this.u), vec3.dot(this.v));
|
||||||
|
}
|
||||||
|
|
||||||
|
to3D(vec2: Vector2D)
|
||||||
|
{
|
||||||
|
return this.planeorigin.clone()
|
||||||
|
.add(this.u.clone().multiplyScalar(vec2.x))
|
||||||
|
.add(this.v.clone().multiplyScalar(vec2.y));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
import { Matrix4 } from "three";
|
||||||
|
import { getTag } from "../constants";
|
||||||
|
import { IsMirror } from "./IsMirrot";
|
||||||
|
import { Vector3D } from "./Vector3";
|
||||||
|
import { Vertex3D } from "./Vertex3";
|
||||||
|
|
||||||
|
// # class Plane
|
||||||
|
// Represents a plane in 3D space.
|
||||||
|
export class Plane
|
||||||
|
{
|
||||||
|
normal: Vector3D;
|
||||||
|
w: number;
|
||||||
|
tag: number;
|
||||||
|
constructor(normal: Vector3D, w: number)
|
||||||
|
{
|
||||||
|
this.normal = normal;
|
||||||
|
this.w = w;
|
||||||
|
}
|
||||||
|
|
||||||
|
flipped()
|
||||||
|
{
|
||||||
|
return new Plane(this.normal.clone().negate(), -this.w);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTag()
|
||||||
|
{
|
||||||
|
if (!this.tag)
|
||||||
|
this.tag = getTag();
|
||||||
|
return this.tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
equals(plane: Plane)
|
||||||
|
{
|
||||||
|
return this.normal.equals(plane.normal) && this.w === plane.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
transform(matrix4x4: Matrix4)
|
||||||
|
{
|
||||||
|
// get two vectors in the plane:
|
||||||
|
let r = this.normal.randomNonParallelVector();
|
||||||
|
let u = this.normal.clone().cross(r);
|
||||||
|
let v = this.normal.clone().cross(u);
|
||||||
|
// get 3 points in the plane:
|
||||||
|
let point1 = this.normal.clone().multiplyScalar(this.w);
|
||||||
|
let point2 = u.add(point1);
|
||||||
|
let point3 = v.add(point1);
|
||||||
|
// transform the points:
|
||||||
|
point1.applyMatrix4(matrix4x4);
|
||||||
|
point2.applyMatrix4(matrix4x4);
|
||||||
|
point3.applyMatrix4(matrix4x4);
|
||||||
|
// and create a new plane from the transformed points:
|
||||||
|
let newplane = Plane.fromVector3Ds(point1, point2, point3);
|
||||||
|
if (IsMirror(matrix4x4))
|
||||||
|
{
|
||||||
|
// the transform is mirroring
|
||||||
|
// We should mirror the plane:
|
||||||
|
newplane = newplane.flipped();
|
||||||
|
}
|
||||||
|
return newplane;
|
||||||
|
}
|
||||||
|
|
||||||
|
splitLineBetweenPoints(p1: Vertex3D, p2: Vertex3D): Vertex3D
|
||||||
|
{
|
||||||
|
let direction = p2.pos.clone().sub(p1.pos);
|
||||||
|
let labda = (this.w - this.normal.dot(p1.pos)) / this.normal.dot(direction);
|
||||||
|
if (isNaN(labda)) labda = 0;
|
||||||
|
if (labda > 1) labda = 1;
|
||||||
|
if (labda < 0) labda = 0;
|
||||||
|
let pos = p1.pos.clone().add(direction.multiplyScalar(labda));
|
||||||
|
let uv = p1.uv.clone().lerp(p2.uv, labda);
|
||||||
|
return new Vertex3D(pos, uv);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromVector3Ds(a: Vector3D, b: Vector3D, c: Vector3D)
|
||||||
|
{
|
||||||
|
let n = b.clone()
|
||||||
|
.sub(a)
|
||||||
|
.cross(c.clone().sub(a))
|
||||||
|
.normalize();
|
||||||
|
return new Plane(n, n.dot(a));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
import { Polygon } from "./Polygon3";
|
||||||
|
import { Vector2D } from "./Vector2";
|
||||||
|
import { Vertex2D } from "./Vertex2";
|
||||||
|
import { Vertex3D } from "./Vertex3";
|
||||||
|
|
||||||
|
export class Side
|
||||||
|
{
|
||||||
|
constructor(public vertex0: Vertex2D, public vertex1: Vertex2D) { }
|
||||||
|
toPolygon3D(z0: number, z1: number)
|
||||||
|
{
|
||||||
|
// console.log(this.vertex0.pos)
|
||||||
|
const vertices = [
|
||||||
|
new Vertex3D(this.vertex0.pos.toVector3D(z0)),
|
||||||
|
new Vertex3D(this.vertex1.pos.toVector3D(z0)),
|
||||||
|
new Vertex3D(this.vertex1.pos.toVector3D(z1)),
|
||||||
|
new Vertex3D(this.vertex0.pos.toVector3D(z1))
|
||||||
|
];
|
||||||
|
return new Polygon(vertices);
|
||||||
|
}
|
||||||
|
|
||||||
|
flipped()
|
||||||
|
{
|
||||||
|
return new Side(this.vertex1, this.vertex0);
|
||||||
|
}
|
||||||
|
|
||||||
|
length()
|
||||||
|
{
|
||||||
|
return this.vertex0.pos.distanceTo(this.vertex1.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
//wall polygon 展平成side
|
||||||
|
static _fromFakePolygon(polygon: Polygon): Side
|
||||||
|
{
|
||||||
|
// this can happen based on union, seems to be residuals -
|
||||||
|
// return null and handle in caller
|
||||||
|
if (polygon.vertices.length < 4)
|
||||||
|
return null;
|
||||||
|
const vert1Indices: number[] = [];
|
||||||
|
const pts2d = polygon.vertices
|
||||||
|
.filter((v, i) =>
|
||||||
|
{
|
||||||
|
if (v.pos.z > 0)
|
||||||
|
{
|
||||||
|
vert1Indices.push(i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.map(v => new Vector2D(v.pos.x, v.pos.y));
|
||||||
|
if (pts2d.length !== 2)
|
||||||
|
{
|
||||||
|
throw new Error(
|
||||||
|
"Assertion failed: _fromFakePolygon: not enough points found"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const d = vert1Indices[1] - vert1Indices[0];
|
||||||
|
if (d === 1 || d === 3)
|
||||||
|
{
|
||||||
|
if (d === 1)
|
||||||
|
pts2d.reverse();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Error(
|
||||||
|
"Assertion failed: _fromFakePolygon: unknown index ordering"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const result = new Side(new Vertex2D(pts2d[0]), new Vertex2D(pts2d[1]));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { Vector3D } from "./Vector3";
|
||||||
|
import { Vector2 } from "three";
|
||||||
|
|
||||||
|
export class Vector2D extends Vector2
|
||||||
|
{
|
||||||
|
// extend to a 3D vector by adding a z coordinate:
|
||||||
|
toVector3D(z: number)
|
||||||
|
{
|
||||||
|
return new Vector3D(this.x, this.y, z);
|
||||||
|
}
|
||||||
|
clone()
|
||||||
|
{
|
||||||
|
return new Vector2D(this.x, this.y);
|
||||||
|
}
|
||||||
|
// returns the vector rotated by 90 degrees clockwise
|
||||||
|
normal()
|
||||||
|
{
|
||||||
|
return new Vector2D(this.y, -this.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
cross(a: Vector2)
|
||||||
|
{
|
||||||
|
return this.x * a.y - this.y * a.x;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
import { Vector3 } from "three";
|
||||||
|
/** Class Vector3D
|
||||||
|
* Represents a 3D vector with X, Y, Z coordinates.
|
||||||
|
*/
|
||||||
|
export class Vector3D extends Vector3
|
||||||
|
{
|
||||||
|
clone()
|
||||||
|
{
|
||||||
|
return new Vector3D(this.x, this.y, this.z);
|
||||||
|
}
|
||||||
|
// find a vector that is somewhat perpendicular to this one
|
||||||
|
randomNonParallelVector()
|
||||||
|
{
|
||||||
|
let x = Math.abs(this.x);
|
||||||
|
let y = Math.abs(this.y);
|
||||||
|
let z = Math.abs(this.z);
|
||||||
|
|
||||||
|
if (x <= y && x <= z)
|
||||||
|
return new Vector3D(1, 0, 0);
|
||||||
|
else if (y <= x && y <= z)
|
||||||
|
return new Vector3D(0, 1, 0);
|
||||||
|
else
|
||||||
|
return new Vector3D(0, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
"(" +
|
||||||
|
this.x.toFixed(5) +
|
||||||
|
", " +
|
||||||
|
this.y.toFixed(5) +
|
||||||
|
", " +
|
||||||
|
this.z.toFixed(5) +
|
||||||
|
")"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { getTag } from "../constants";
|
||||||
|
import { Vector2D } from "./Vector2";
|
||||||
|
|
||||||
|
export class Vertex2D
|
||||||
|
{
|
||||||
|
tag: any;
|
||||||
|
pos: Vector2D;
|
||||||
|
constructor(pos: Vector2D)
|
||||||
|
{
|
||||||
|
this.pos = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return "(" + this.pos.x.toFixed(5) + "," + this.pos.y.toFixed(5) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
getTag()
|
||||||
|
{
|
||||||
|
let result = this.tag;
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
result = getTag();
|
||||||
|
this.tag = result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
import { getTag } from "../constants";
|
||||||
|
import { Vector2D } from "./Vector2";
|
||||||
|
import { Vector3D } from "./Vector3";
|
||||||
|
import { Matrix4 } from "three";
|
||||||
|
|
||||||
|
// # class Vertex
|
||||||
|
// Represents a vertex of a polygon. Use your own vertex class instead of this
|
||||||
|
// one to provide additional features like texture coordinates and vertex
|
||||||
|
// colors. Custom vertex classes need to provide a `pos` property
|
||||||
|
// `flipped()`, and `interpolate()` methods that behave analogous to the ones
|
||||||
|
// FIXME: And a lot MORE (see plane.fromVector3Ds for ex) ! This is fragile code
|
||||||
|
// defined by `Vertex`.
|
||||||
|
export class Vertex3D
|
||||||
|
{
|
||||||
|
tag: number;
|
||||||
|
constructor(public pos: Vector3D, public uv = new Vector2D()) { }
|
||||||
|
|
||||||
|
// Return a vertex with all orientation-specific data (e.g. vertex normal) flipped. Called when the
|
||||||
|
// orientation of a polygon is flipped.
|
||||||
|
flipped()
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTag()
|
||||||
|
{
|
||||||
|
let result = this.tag;
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
result = getTag();
|
||||||
|
this.tag = result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new vertex between this vertex and `other` by linearly
|
||||||
|
// interpolating all properties using a parameter of `t`. Subclasses should
|
||||||
|
// override this to interpolate additional properties.
|
||||||
|
interpolate(other: Vertex3D, t: number)
|
||||||
|
{
|
||||||
|
let pos = this.pos.clone().lerp(other.pos, t);
|
||||||
|
let uv = this.uv.clone().lerp(other.uv, t);
|
||||||
|
return new Vertex3D(pos, uv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affine transformation of vertex. Returns a new Vertex
|
||||||
|
|
||||||
|
transform(matrix4x4: Matrix4)
|
||||||
|
{
|
||||||
|
const newpos = this.pos.clone().applyMatrix4(matrix4x4);
|
||||||
|
return new Vertex3D(newpos, this.uv);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import { solve2Linear } from "../utils";
|
||||||
|
import { EPS } from "../constants";
|
||||||
|
import { Vector2D } from "./Vector2";
|
||||||
|
|
||||||
|
// see if the line between p0start and p0end intersects with the line between p1start and p1end
|
||||||
|
// returns true if the lines strictly intersect, the end points are not counted!
|
||||||
|
export function linesIntersect(p0start: Vector2D, p0end: Vector2D, p1start: Vector2D, p1end: Vector2D)
|
||||||
|
{
|
||||||
|
if (p0end.equals(p1start) || p1end.equals(p0start))
|
||||||
|
{
|
||||||
|
let d = p1end
|
||||||
|
.sub(p1start)
|
||||||
|
.normalize()
|
||||||
|
.add(p0end.sub(p0start).normalize())
|
||||||
|
.length();
|
||||||
|
if (d < EPS) return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let d0 = p0end.sub(p0start);
|
||||||
|
let d1 = p1end.sub(p1start);
|
||||||
|
// FIXME These epsilons need review and testing
|
||||||
|
if (Math.abs(d0.cross(d1)) < 1e-9)
|
||||||
|
return false; // lines are parallel
|
||||||
|
let alphas = solve2Linear(-d0.x, d1.x, -d0.y, d1.y, p0start.x - p1start.x, p0start.y - p1start.y);
|
||||||
|
if (
|
||||||
|
alphas[0] > 1e-6 &&
|
||||||
|
alphas[0] < 0.999999 &&
|
||||||
|
alphas[1] > 1e-5 &&
|
||||||
|
alphas[1] < 0.999999
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
// if( (alphas[0] >= 0) && (alphas[0] <= 1) && (alphas[1] >= 0) && (alphas[1] <= 1) ) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
@ -0,0 +1,61 @@
|
|||||||
|
import { Vector2D } from "./math/Vector2";
|
||||||
|
|
||||||
|
export function fnNumberSort(a, b)
|
||||||
|
{
|
||||||
|
return a - b;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const solve2Linear = function (a: number, b: number, c: number, d: number, u: number, v: number)
|
||||||
|
{
|
||||||
|
let det = a * d - b * c;
|
||||||
|
let invdet = 1.0 / det;
|
||||||
|
let x = u * d - b * v;
|
||||||
|
let y = -u * c + a * v;
|
||||||
|
x *= invdet;
|
||||||
|
y *= invdet;
|
||||||
|
return [x, y];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function insertSorted<T>(array: T[], element: T, comparefunc: (a: T, b: T) => number)
|
||||||
|
{
|
||||||
|
let leftbound = 0;
|
||||||
|
let rightbound = array.length;
|
||||||
|
while (rightbound > leftbound)
|
||||||
|
{
|
||||||
|
let testindex = Math.floor((leftbound + rightbound) / 2);
|
||||||
|
let testelement = array[testindex];
|
||||||
|
let compareresult = comparefunc(element, testelement);
|
||||||
|
if (compareresult > 0)
|
||||||
|
// element > testelement
|
||||||
|
leftbound = testindex + 1;
|
||||||
|
else
|
||||||
|
rightbound = testindex;
|
||||||
|
}
|
||||||
|
array.splice(leftbound, 0, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the x coordinate of a point with a certain y coordinate, interpolated between two
|
||||||
|
// points (CSG.Vector2D).
|
||||||
|
// Interpolation is robust even if the points have the same y coordinate
|
||||||
|
export function interpolateBetween2DPointsForY(point1: Vector2D, point2: Vector2D, y: number)
|
||||||
|
{
|
||||||
|
let f1 = y - point1.y;
|
||||||
|
let f2 = point2.y - point1.y;
|
||||||
|
if (f2 < 0)
|
||||||
|
{
|
||||||
|
f1 = -f1;
|
||||||
|
f2 = -f2;
|
||||||
|
}
|
||||||
|
let t: number;
|
||||||
|
if (f1 <= 0)
|
||||||
|
t = 0.0;
|
||||||
|
else if (f1 >= f2)
|
||||||
|
t = 1.0;
|
||||||
|
else if (f2 < 1e-10)
|
||||||
|
// FIXME Should this be CSG.EPS?
|
||||||
|
t = 0.5;
|
||||||
|
else
|
||||||
|
t = f1 / f2;
|
||||||
|
let result = point1.x + t * (point2.x - point1.x);
|
||||||
|
return result;
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { CAG } from "../CAG";
|
||||||
|
import { linesIntersect } from "../math/lineUtils";
|
||||||
|
|
||||||
|
export function isSelfIntersecting(cag: CAG, debug = false)
|
||||||
|
{
|
||||||
|
let numsides = cag.sides.length;
|
||||||
|
for (let i = 0; i < numsides; i++)
|
||||||
|
{
|
||||||
|
let side0 = cag.sides[i];
|
||||||
|
for (let ii = i + 1; ii < numsides; ii++)
|
||||||
|
{
|
||||||
|
let side1 = cag.sides[ii];
|
||||||
|
if (
|
||||||
|
linesIntersect(side0.vertex0.pos, side0.vertex1.pos,
|
||||||
|
side1.vertex0.pos, side1.vertex1.pos)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (debug)
|
||||||
|
{
|
||||||
|
console.log("side " + i + ": " + side0);
|
||||||
|
console.log("side " + ii + ": " + side1);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
@ -0,0 +1,47 @@
|
|||||||
|
import { FuzzyCSGFactory } from "../FuzzyFactory3d";
|
||||||
|
import { FuzzyCAGFactory } from "../FuzzyFactory2d";
|
||||||
|
import { CSG } from "../CSG";
|
||||||
|
import { CAG } from "../CAG";
|
||||||
|
import { EPS } from "../constants";
|
||||||
|
import { Polygon } from "../math/Polygon3";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a cannoicalized version of the input csg : ie every very close
|
||||||
|
* points get deduplicated
|
||||||
|
*
|
||||||
|
* 返回删除重复点的csg,重复点将被合并
|
||||||
|
*/
|
||||||
|
export function canonicalizeCSG(csg: CSG): CSG
|
||||||
|
{
|
||||||
|
const factory = new FuzzyCSGFactory();
|
||||||
|
let result = CSGFromCSGFuzzyFactory(factory, csg);
|
||||||
|
result.isCanonicalized = true;
|
||||||
|
result.isRetesselated = csg.isRetesselated;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canonicalizeCAG(cag: CAG)
|
||||||
|
{
|
||||||
|
let factory = new FuzzyCAGFactory();
|
||||||
|
let result = CAGFromCAGFuzzyFactory(factory, cag);
|
||||||
|
result.isCanonicalized = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CSGFromCSGFuzzyFactory(factory: FuzzyCSGFactory, sourcecsg: CSG)
|
||||||
|
{
|
||||||
|
let newpolygons: Polygon[] = sourcecsg.polygons.filter(poly =>
|
||||||
|
{
|
||||||
|
return factory.getPolygon(poly).vertices.length >= 3;
|
||||||
|
});
|
||||||
|
return new CSG(newpolygons);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CAGFromCAGFuzzyFactory(factory: FuzzyCAGFactory, sourcecag: CAG)
|
||||||
|
{
|
||||||
|
let newsides = sourcecag.sides
|
||||||
|
.map(side => factory.getSide(side))
|
||||||
|
// remove bad sides (mostly a user input issue)
|
||||||
|
.filter((side) => side.length() > EPS);
|
||||||
|
return new CAG(newsides);
|
||||||
|
};
|
@ -0,0 +1,37 @@
|
|||||||
|
import { Vector3D } from "../math/Vector3";
|
||||||
|
import { CSG } from "../CSG";
|
||||||
|
/**
|
||||||
|
* Returns an array of Vector3D, providing minimum coordinates and maximum coordinates
|
||||||
|
* of this solid.
|
||||||
|
* @example
|
||||||
|
* let bounds = A.getBounds()
|
||||||
|
* let minX = bounds[0].x
|
||||||
|
*/
|
||||||
|
export function bounds(csg: CSG): Vector3D[]
|
||||||
|
{
|
||||||
|
if (!csg.cachedBoundingBox)
|
||||||
|
{
|
||||||
|
let minpoint: Vector3D;
|
||||||
|
let maxpoint: Vector3D;
|
||||||
|
let polygons = csg.polygons;
|
||||||
|
let numpolygons = polygons.length;
|
||||||
|
for (let i = 0; i < numpolygons; i++)
|
||||||
|
{
|
||||||
|
let polygon = polygons[i];
|
||||||
|
let bounds = polygon.boundingBox();
|
||||||
|
if (i === 0)
|
||||||
|
{
|
||||||
|
minpoint = bounds[0].clone();
|
||||||
|
maxpoint = bounds[1].clone();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
minpoint.min(bounds[0]);
|
||||||
|
maxpoint.max(bounds[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FIXME: not ideal, we are mutating the input, we need to move some of it out
|
||||||
|
csg.cachedBoundingBox = [minpoint, maxpoint];
|
||||||
|
}
|
||||||
|
return csg.cachedBoundingBox;
|
||||||
|
};
|
@ -0,0 +1,41 @@
|
|||||||
|
import { FuzzyCSGFactory } from "../FuzzyFactory3d";
|
||||||
|
import { reTesselateCoplanarPolygons } from "../math/reTesselateCoplanarPolygons";
|
||||||
|
import { CSG } from "../CSG";
|
||||||
|
import { Polygon } from "../math/Polygon3";
|
||||||
|
|
||||||
|
export function reTesselate(csg: CSG): CSG
|
||||||
|
{
|
||||||
|
if (csg.isRetesselated) return csg;
|
||||||
|
|
||||||
|
let polygonsPerPlane: { [key: number]: Polygon[] } = {};
|
||||||
|
let isCanonicalized = csg.isCanonicalized;
|
||||||
|
let fuzzyfactory = new FuzzyCSGFactory();
|
||||||
|
|
||||||
|
for (let polygon of csg.polygons)
|
||||||
|
{
|
||||||
|
let plane = polygon.plane;
|
||||||
|
if (!isCanonicalized)
|
||||||
|
{
|
||||||
|
// in order to identify polygons having the same plane, we need to canonicalize the planes
|
||||||
|
// We don't have to do a full canonizalization (including vertices), to save time only do the planes and the shared data:
|
||||||
|
plane = fuzzyfactory.getPlane(plane);
|
||||||
|
}
|
||||||
|
let tag = plane.getTag();
|
||||||
|
if (!(tag in polygonsPerPlane)) polygonsPerPlane[tag] = [polygon];
|
||||||
|
else polygonsPerPlane[tag].push(polygon);
|
||||||
|
}
|
||||||
|
|
||||||
|
let destpolygons: Polygon[] = [];
|
||||||
|
for (let planetag in polygonsPerPlane)
|
||||||
|
{
|
||||||
|
let sourcepolygons = polygonsPerPlane[planetag];
|
||||||
|
if (sourcepolygons.length < 2)
|
||||||
|
destpolygons.push(...sourcepolygons);
|
||||||
|
else
|
||||||
|
reTesselateCoplanarPolygons(sourcepolygons, destpolygons);
|
||||||
|
}
|
||||||
|
let resultCSG = new CSG(destpolygons);
|
||||||
|
resultCSG.isRetesselated = true;
|
||||||
|
// result = result.canonicalized();
|
||||||
|
return resultCSG;
|
||||||
|
};
|
Loading…
Reference in new issue