From c6f01897abb71de7e2701b77a400ef8e7dc515f6 Mon Sep 17 00:00:00 2001 From: ChenX Date: Wed, 8 May 2019 17:58:00 +0800 Subject: [PATCH] =?UTF-8?q?fix=20#IKXMP=20!295=20=E6=96=B0=E7=9A=84CSG?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 26 +- __test__/Geometry/EdgeGeometry.test.ts | 2 +- src/Add-on/CSGOperation.ts | 61 --- src/Add-on/Erase.ts | 10 +- src/Add-on/Move.ts | 7 + src/ApplicationServices/mesh/createBoard.ts | 36 +- src/Common/ArrayExt.ts | 4 +- src/Common/Utils.ts | 14 +- src/DatabaseServices/Extrude.ts | 29 +- src/DatabaseServices/FaceEntity.ts | 53 ++ src/DatabaseServices/Shape2.ts | 60 +++ src/Editor/CommandRegister.ts | 5 - src/Geometry/BSPGroupParse.ts | 8 +- src/Geometry/EdgeGeometry.ts | 24 +- src/Geometry/Orbit.ts | 10 +- src/Geometry/ThreeCSG.ts | 506 ------------------ src/csg/core/CAG.ts | 100 ++++ src/csg/core/CAGFactories.ts | 37 ++ src/csg/core/CSG.ts | 314 +++++++++++ src/csg/core/FuzzyFactory.ts | 63 +++ src/csg/core/FuzzyFactory2d.ts | 24 + src/csg/core/FuzzyFactory3d.ts | 54 ++ src/csg/core/Geometry2CSG.ts | 88 +++ src/csg/core/constants.ts | 10 + src/csg/core/math/IsMirrot.ts | 15 + src/csg/core/math/Line2.ts | 36 ++ src/csg/core/math/OrthoNormalBasis.ts | 34 ++ src/csg/core/math/Plane.ts | 82 +++ src/csg/core/math/Polygon3.ts | 248 +++++++++ src/csg/core/math/Side.ts | 71 +++ src/csg/core/math/Vector2.ts | 25 + src/csg/core/math/Vector3.ts | 38 ++ src/csg/core/math/Vertex2.ts | 28 + src/csg/core/math/Vertex3.ts | 53 ++ src/csg/core/math/lineUtils.ts | 36 ++ .../core/math/reTesselateCoplanarPolygons.ts | 458 ++++++++++++++++ src/csg/core/trees.ts | 482 +++++++++++++++++ src/csg/core/utils.ts | 61 +++ src/csg/core/utils/cagValidation.ts | 28 + src/csg/core/utils/canonicalize.ts | 47 ++ src/csg/core/utils/csgMeasurements.ts | 37 ++ src/csg/core/utils/retesellate.ts | 41 ++ 42 files changed, 2718 insertions(+), 647 deletions(-) delete mode 100644 src/Add-on/CSGOperation.ts create mode 100644 src/DatabaseServices/FaceEntity.ts create mode 100644 src/DatabaseServices/Shape2.ts delete mode 100644 src/Geometry/ThreeCSG.ts create mode 100644 src/csg/core/CAG.ts create mode 100644 src/csg/core/CAGFactories.ts create mode 100644 src/csg/core/CSG.ts create mode 100644 src/csg/core/FuzzyFactory.ts create mode 100644 src/csg/core/FuzzyFactory2d.ts create mode 100644 src/csg/core/FuzzyFactory3d.ts create mode 100644 src/csg/core/Geometry2CSG.ts create mode 100644 src/csg/core/constants.ts create mode 100644 src/csg/core/math/IsMirrot.ts create mode 100644 src/csg/core/math/Line2.ts create mode 100644 src/csg/core/math/OrthoNormalBasis.ts create mode 100644 src/csg/core/math/Plane.ts create mode 100644 src/csg/core/math/Polygon3.ts create mode 100644 src/csg/core/math/Side.ts create mode 100644 src/csg/core/math/Vector2.ts create mode 100644 src/csg/core/math/Vector3.ts create mode 100644 src/csg/core/math/Vertex2.ts create mode 100644 src/csg/core/math/Vertex3.ts create mode 100644 src/csg/core/math/lineUtils.ts create mode 100644 src/csg/core/math/reTesselateCoplanarPolygons.ts create mode 100644 src/csg/core/trees.ts create mode 100644 src/csg/core/utils.ts create mode 100644 src/csg/core/utils/cagValidation.ts create mode 100644 src/csg/core/utils/canonicalize.ts create mode 100644 src/csg/core/utils/csgMeasurements.ts create mode 100644 src/csg/core/utils/retesellate.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index f1dbfc32a..01da211e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,17 @@ // 将设置放入此文件中以覆盖默认值和用户设置。 { - "typescript.tsdk": "node_modules\\typescript\\lib", - //格式化设置 - "editor.tabSize": 4, - "editor.formatOnPaste": true, - "editor.formatOnSave": true, - //格式设置 - "typescript.format.placeOpenBraceOnNewLineForFunctions": true, - "typescript.format.placeOpenBraceOnNewLineForControlBlocks": true, - "typescript.referencesCodeLens.enabled": true, - "javascript.format.placeOpenBraceOnNewLineForFunctions": true, - "javascript.format.enable": true, - "files.insertFinalNewline": true, + "typescript.tsdk": "node_modules\\typescript\\lib", + //格式化设置 + "editor.tabSize": 4, + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.insertSpaces": true, + "editor.detectIndentation": false, + //格式设置 + "typescript.format.placeOpenBraceOnNewLineForFunctions": true, + "typescript.format.placeOpenBraceOnNewLineForControlBlocks": true, + "typescript.referencesCodeLens.enabled": true, + "javascript.format.placeOpenBraceOnNewLineForFunctions": true, + "javascript.format.enable": true, + "files.insertFinalNewline": true, } diff --git a/__test__/Geometry/EdgeGeometry.test.ts b/__test__/Geometry/EdgeGeometry.test.ts index 2e0a7a950..005e303c6 100644 --- a/__test__/Geometry/EdgeGeometry.test.ts +++ b/__test__/Geometry/EdgeGeometry.test.ts @@ -17,5 +17,5 @@ test('EdgeGeometry生成', () => let geo = line.geometry; //@ts-ignore - expect(geo.attributes.position.length).toBe(240); + expect(geo.attributes.position.length).toBe(246); }); diff --git a/src/Add-on/CSGOperation.ts b/src/Add-on/CSGOperation.ts deleted file mode 100644 index 4ae1ba40a..000000000 --- a/src/Add-on/CSGOperation.ts +++ /dev/null @@ -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); - } -} diff --git a/src/Add-on/Erase.ts b/src/Add-on/Erase.ts index 1728b1905..0af1dc683 100644 --- a/src/Add-on/Erase.ts +++ b/src/Add-on/Erase.ts @@ -1,4 +1,5 @@ import { app } from '../ApplicationServices/Application'; +import { GetEntity } from '../Common/Utils'; import { Command } from '../Editor/CommandMachine'; import { PromptStatus } from '../Editor/PromptResult'; @@ -9,6 +10,13 @@ export class Command_Erase implements Command let ssRes = await app.m_Editor.GetSelection({ UseSelect: true }); if (ssRes.Status != PromptStatus.OK) return; - ssRes.SelectSet.SelectEntityList.forEach(e => { e.Erase() }); + for (let obj of ssRes.SelectSet.SelectObjectList) + { + let ent = GetEntity(obj); + if (ent) + ent.Erase(); + else + obj.visible = false; + } } } diff --git a/src/Add-on/Move.ts b/src/Add-on/Move.ts index 6c999958a..44ae58eb3 100644 --- a/src/Add-on/Move.ts +++ b/src/Add-on/Move.ts @@ -4,6 +4,7 @@ import { Command } from '../Editor/CommandMachine'; import { PromptStatus } from '../Editor/PromptResult'; import { MoveMatrix } from '../Geometry/GeUtils'; import { JigUtils } from '../Editor/JigUtils'; +// import { IsEntity } from '../Common/Utils'; export class Command_Move implements Command { @@ -30,6 +31,12 @@ export class Command_Move implements Command let moveMatrix = MoveMatrix(p.clone().sub(ptLast)); ensClone.forEach(e => e.ApplyMatrix(moveMatrix)); ptLast.copy(p); + // //对调试实体进行移动 + // for (let obj of ssRes.SelectSet.SelectObjectList) + // { + // if (!IsEntity(obj)) + // obj.applyMatrix(moveMatrix); + // } }, BasePoint: ptBase, AllowDrawRubberBand: true diff --git a/src/ApplicationServices/mesh/createBoard.ts b/src/ApplicationServices/mesh/createBoard.ts index 9af8ccac1..9c1d9b83d 100644 --- a/src/ApplicationServices/mesh/createBoard.ts +++ b/src/ApplicationServices/mesh/createBoard.ts @@ -1,7 +1,7 @@ -import * as THREE from 'three'; - +import { ExtrudeGeometry, Matrix4, Mesh, Shape, Vector2, Vector3 } from 'three'; import { data } from '../../Add-on/data'; -import { polar, equaln } from '../../Geometry/GeUtils'; +import { Shape2 } from '../../DatabaseServices/Shape2'; +import { equaln, polar } from '../../Geometry/GeUtils'; import { RotateUVs } from '../../Geometry/RotateUV'; export namespace CreateBoardUtil @@ -11,16 +11,16 @@ export namespace CreateBoardUtil { m_StartAn: number; m_EndAn: number; - m_StartPoint: THREE.Vector2; - m_EndPoint: THREE.Vector2; - m_Center: THREE.Vector2; + m_StartPoint: Vector2; + m_EndPoint: Vector2; + m_Center: Vector2; m_Radius: number; - constructor(p1: THREE.Vector2, p2: THREE.Vector2, bul: number) + constructor(p1: Vector2, p2: Vector2, bul: number) { this.m_StartPoint = p1.clone(); this.m_EndPoint = p2.clone(); - let vec: THREE.Vector2 = p2.clone().sub(p1); + let vec: Vector2 = p2.clone().sub(p1); let len = vec.length(); let an = vec.angle(); this.m_Radius = len / Math.sin(2 * Math.atan(bul)) / 2; @@ -47,9 +47,9 @@ export namespace CreateBoardUtil //创建轮廓 通过点表和凸度 - export function createPath(pts: THREE.Vector2[], buls: number[], shapeOut?: THREE.Shape): THREE.Shape + export function createPath(pts: Vector2[], buls: number[]): Shape { - let shape = shapeOut || new THREE.Shape(); + let shape = new Shape2(); if (pts.length === 0) return shape; let firstPt = pts[0]; @@ -74,24 +74,24 @@ export namespace CreateBoardUtil } return shape; } - export function getVec(data: object): THREE.Vector3 + export function getVec(data: object): Vector3 { - return new THREE.Vector3(data["x"], data["y"], data["z"]); + return new Vector3(data["x"], data["y"], data["z"]); } //创建板件 暂时这么写 export function createBoard(boardData: object) { - var pts: THREE.Vector2[] = new Array(); + var pts: Vector2[] = new Array(); var buls: number[] = new Array(); var boardPts = boardData["Pts"]; var boardBuls = boardData["Buls"]; let boardHeight = boardData["H"]; - var boardMat = new THREE.Matrix4(); - var matInv: THREE.Matrix4 = new THREE.Matrix4(); + var boardMat = new Matrix4(); + var matInv: Matrix4 = new Matrix4(); //InitBoardMat { @@ -109,7 +109,7 @@ export namespace CreateBoardUtil { var pt = getVec(boardPts[i]).multiplyScalar(0.001); pt.applyMatrix4(matInv); - pts.push(new THREE.Vector2(pt.x, pt.y)); + pts.push(new Vector2(pt.x, pt.y)); buls.push(boardBuls[i]); } @@ -120,7 +120,7 @@ export namespace CreateBoardUtil amount: boardHeight * 0.001 }; - var ext = new THREE.ExtrudeGeometry(sp, extrudeSettings); + var ext = new ExtrudeGeometry(sp, extrudeSettings); ext.translate(0, 0, -boardHeight * 0.001) ext.applyMatrix(boardMat); @@ -129,7 +129,7 @@ export namespace CreateBoardUtil RotateUVs(ext); } - var mesh = new THREE.Mesh(ext); + var mesh = new Mesh(ext); return mesh; } diff --git a/src/Common/ArrayExt.ts b/src/Common/ArrayExt.ts index 038aa13a1..2c5449191 100644 --- a/src/Common/ArrayExt.ts +++ b/src/Common/ArrayExt.ts @@ -128,12 +128,12 @@ export function arrayMap(arr: Array, mapFunc: (v: T) => T): Array return arr; } -function sortNumberCompart(e1, e2) +function sortNumberCompart(e1: any, e2: any) { return e1 - e2; } -function checkEqual(e1, e2): boolean +function checkEqual(e1: any, e2: any): boolean { return e1 === e2; } diff --git a/src/Common/Utils.ts b/src/Common/Utils.ts index 059932e79..1ae7ef9c5 100644 --- a/src/Common/Utils.ts +++ b/src/Common/Utils.ts @@ -116,16 +116,26 @@ function fallbackCopyTextToClipboard(text) } document.body.removeChild(textArea); } -export async function copyTextToClipboard(text) +export async function copyTextToClipboard(text: string) { if (!navigator["clipboard"]) { fallbackCopyTextToClipboard(text); return; } - await navigator["clipboard"].writeText(text); + return await navigator["clipboard"].writeText(text); } +window["copy"] = (text: string) => +{ + console.log("你有2秒的时间让页面聚焦,否则将无法拷贝!"); + setTimeout(async () => + { + await copyTextToClipboard(text); + console.log("拷贝成功!"); + }, 2000); +}; + //ref: https://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript diff --git a/src/DatabaseServices/Extrude.ts b/src/DatabaseServices/Extrude.ts index 95f195cea..1d891ad2a 100644 --- a/src/DatabaseServices/Extrude.ts +++ b/src/DatabaseServices/Extrude.ts @@ -5,13 +5,14 @@ import { ColorMaterial } from "../Common/ColorPalette"; import { equalCurve } from "../Common/CurveUtils"; import { DisposeThreeObj } from "../Common/Dispose"; import { Status, UpdateDraw } from "../Common/Status"; +import { CSG } from "../csg/core/CSG"; +import { CSG2Geometry, Geometry2CSG } from "../csg/core/Geometry2CSG"; import { ObjectSnapMode } from "../Editor/ObjectSnapMode"; import { boardUVGenerator } from "../Geometry/BoardUVGenerator"; import { BSPGroupParse } from "../Geometry/BSPGroupParse"; import { CreateWireframe } from "../Geometry/CreateWireframe"; import { EdgesGeometry } from "../Geometry/EdgeGeometry"; import { cZeroVec, equaln, equalv2, equalv3, isIntersect, isParallelTo, MoveMatrix } from "../Geometry/GeUtils"; -import { CSG } from "../Geometry/ThreeCSG"; import { ScaleUV } from "../Geometry/UVUtils"; import { RenderType } from "../GraphicsSystem/RenderType"; import { BlockTableRecord } from "./BlockTableRecord"; @@ -502,10 +503,11 @@ export class ExtureSolid extends Entity let splitEntitys: this[] = []; this.GrooveCheckAll(splitEntitys); - let ms = this.m_Owner.Object as BlockTableRecord; - for (let e of splitEntitys) + if (splitEntitys.length > 0 && this.m_Owner) { - ms.Append(e); + let ms = this.m_Owner.Object as BlockTableRecord; + for (let e of splitEntitys) + ms.Append(e); } } this.Update(); @@ -654,7 +656,8 @@ export class ExtureSolid extends Entity if (this.csg) return this.csg; - this.GetDrawObjectFromRenderType(); + //使用这个方法 而不是得到MeshGeometry,以保证实体更新时CSG对象能被更新. + this.GetDrawObjectFromRenderType(RenderType.Physical); if (!this.csg) this.Update(UpdateDraw.Geometry); return this.csg; @@ -681,9 +684,9 @@ export class ExtureSolid extends Entity let zv = this.Normal; let xv = yv.clone().cross(zv); let m = new Matrix4().makeBasis(xv, yv, zv).copyPosition(this.OCS); - let mi = new Matrix4().getInverse(m); + let mi = new Matrix4().getInverse(m).multiply(this.OCS); - let interBSP = this.CSG.intersect(target.CSG); + let interBSP = this.CSG.intersect(target.CSG.transform1(this.OCSInv.multiply(target.OCS))); let topology = new BSPGroupParse(interBSP); let grooves: ExtureSolid[] = []; for (let pts of topology.Parse()) @@ -995,6 +998,7 @@ export class ExtureSolid extends Entity this._MeshGeometry = this.GeneralGeometry(); return this._MeshGeometry; } + private _EdgeGeometry: EdgesGeometry; private get EdgeGeometry() { @@ -1015,20 +1019,21 @@ export class ExtureSolid extends Entity }; let geo = new ExtrudeGeometry(this.ContourCurve.Shape, extrudeSettings); geo.applyMatrix(this.contourCurve.OCS); + this.csg = Geometry2CSG(geo); - this.csg = new CSG(geo, this.m_Matrix); - - ScaleUV(geo); if (this.grooves.length === 0) + { + ScaleUV(geo); return geo; + } for (let g of this.grooves) { if (g.thickness > 0) - this.csg = this.csg.subtract(g.CSG); + this.csg = this.csg.subtract(g.CSG.transform1(this.OCSInv.multiply(g.OCS))); } - let bgeo = this.csg.toGeometry(); + let bgeo = CSG2Geometry(this.csg); ScaleUV(bgeo); return bgeo; } diff --git a/src/DatabaseServices/FaceEntity.ts b/src/DatabaseServices/FaceEntity.ts new file mode 100644 index 000000000..7445b7d49 --- /dev/null +++ b/src/DatabaseServices/FaceEntity.ts @@ -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 +} diff --git a/src/DatabaseServices/Shape2.ts b/src/DatabaseServices/Shape2.ts new file mode 100644 index 000000000..c61d24933 --- /dev/null +++ b/src/DatabaseServices/Shape2.ts @@ -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); + } +} diff --git a/src/Editor/CommandRegister.ts b/src/Editor/CommandRegister.ts index 2b604908a..f6d105c3b 100644 --- a/src/Editor/CommandRegister.ts +++ b/src/Editor/CommandRegister.ts @@ -16,7 +16,6 @@ import { Command_Copy } from '../Add-on/Copy'; import { CopyClip } from '../Add-on/CopyClip'; import { Command_CopyPoint } from '../Add-on/CopyPoint'; import { CustomUcs } from '../Add-on/CostumUCS'; -import { CSGSubtraction, CSGUnion } from '../Add-on/CSGOperation'; import { CMD_Divide } from '../Add-on/Divide'; import { DrawArc } from '../Add-on/DrawArc'; import { DrawBehindBoard } from '../Add-on/DrawBoard/DrawBehindBoard'; @@ -160,10 +159,6 @@ export function registerCommand() commandMachine.RegisterCommand("hus", new Command_HideUnselected()) commandMachine.RegisterCommand("show", new Command_ShowAll) - //CSG Operate - commandMachine.RegisterCommand("csgunion", new CSGUnion()) - commandMachine.RegisterCommand("csgsub", new CSGSubtraction()) - commandMachine.RegisterCommand("save", new Save()); commandMachine.RegisterCommand("open", new Open()); diff --git a/src/Geometry/BSPGroupParse.ts b/src/Geometry/BSPGroupParse.ts index 5d907cbac..f94cb8b37 100644 --- a/src/Geometry/BSPGroupParse.ts +++ b/src/Geometry/BSPGroupParse.ts @@ -1,6 +1,7 @@ import { Vector3 } from "three"; import { ToFixed } from "../Common/Utils"; -import { Polygon, CSG } from "./ThreeCSG"; +import { CSG } from "../csg/core/CSG"; +import { Polygon } from "../csg/core/math/Polygon3"; interface Vec3 { x: number; @@ -8,7 +9,6 @@ interface Vec3 z: number; } - /** * 解决 THREEBSP(CSG) 产生的结果没有办法得到分裂的个数. * 本类分析了THREEBSP的组合情况. @@ -23,12 +23,12 @@ export class BSPGroupParse constructor(bsp?: CSG, public fractionDigits = 1) { if (bsp) - for (let poly of bsp.tree.allPolygons()) + for (let poly of bsp.polygons) this.Add(poly); } Add(poly: Polygon) { - let strs = poly.vertices.map(p => this.GenerateP(p)); + let strs = poly.vertices.map(p => this.GenerateP(p.pos)); let str0 = strs[0]; let s0 = this.Get(str0); for (let i = 1; i < strs.length; i++) diff --git a/src/Geometry/EdgeGeometry.ts b/src/Geometry/EdgeGeometry.ts index 31fce516c..f29ebd6f4 100644 --- a/src/Geometry/EdgeGeometry.ts +++ b/src/Geometry/EdgeGeometry.ts @@ -1,5 +1,6 @@ import { BufferGeometry, Face3, Float32BufferAttribute, Geometry, Line3, Triangle, Vector3 } from "three"; import { arraySortByNumber } from "../Common/ArrayExt"; +import { equalv3 } from "./GeUtils"; //ref: https://github.com/mrdoob/js/issues/10517 const keys = ['a', 'b', 'c']; @@ -9,7 +10,6 @@ export class EdgesGeometry extends BufferGeometry constructor(geometry, thresholdAngle: number = 1) { super(); - let geometry2: Geometry; if (geometry.isBufferGeometry) @@ -23,19 +23,18 @@ export class EdgesGeometry extends BufferGeometry let vertices = geometry2.vertices; let faces = geometry2.faces; - let faceHash = new Set(); + let count = faces.length; for (let i = 0; i < faces.length; i++) { - if (i > 1000)//出错 + if (faces.length > count * 2) + { + console.warn("EdgeGeometry的分裂已经到达2倍!"); break; - + } let face = faces[i]; - //fix CSG produces duplicate faces - let faceStr = `${face.a},${face.b},${face.c}`; - if (faceHash.has(faceStr)) + if (FaceArea(face, vertices) < 1e-5) continue; - faceHash.add(faceStr); for (let j = 0; j < 3; j++) { @@ -53,17 +52,14 @@ export class EdgesGeometry extends BufferGeometry let p = vertices[e]; let closestPoint = line.closestPointToPoint(p, true, new Vector3()); - if (closestPoint.equals(vertices[e1]) || closestPoint.equals(vertices[e2])) + if (equalv3(closestPoint, vertices[e1], 1e-5) || equalv3(closestPoint, vertices[e2])) continue; - if (closestPoint.distanceTo(p) < 1e-5) + if (equalv3(closestPoint, p, 1e-5)) { face["splitted"] = true; let f1 = new Face3(e, e3, e1, face.normal); let f2 = new Face3(e, e3, e2, face.normal); - - if (FaceArea(f1, vertices) > 1 - && FaceArea(f2, vertices) > 1) - faces.push(f1, f2); + faces.push(f1, f2); break; } } diff --git a/src/Geometry/Orbit.ts b/src/Geometry/Orbit.ts index 5b5e80a50..e401df30e 100644 --- a/src/Geometry/Orbit.ts +++ b/src/Geometry/Orbit.ts @@ -63,18 +63,10 @@ export class Orbit } /** - * * 参考任意轴坐标系算法. * http://help.autodesk.com/view/ACD/2017/CHS/?guid=GUID-E19E5B42-0CC7-4EBA-B29F-5E1D595149EE - * - * @static - * @param {Vector3} n 法线 - * @param {Vector3} [ay] - * @param {Vector3} [ax] - * @returns {Vector3} ay =>up - * @memberof Orbit */ - static ComputUpDirection(n: Vector3, ay = new Vector3(), ax = new Vector3()): Vector3 + static ComputUpDirection(n: Vector3, ay: Vector3 = new Vector3(), ax: Vector3 = new Vector3()): Vector3 { if (Math.abs(n.x) < 0.015625 && Math.abs(n.y) < 0.015625) ax.crossVectors(cYAxis, n); diff --git a/src/Geometry/ThreeCSG.ts b/src/Geometry/ThreeCSG.ts deleted file mode 100644 index 92c7bba18..000000000 --- a/src/Geometry/ThreeCSG.ts +++ /dev/null @@ -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); - } -} diff --git a/src/csg/core/CAG.ts b/src/csg/core/CAG.ts new file mode 100644 index 000000000..b5738743c --- /dev/null +++ b/src/csg/core/CAG.ts @@ -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(); + } +} diff --git a/src/csg/core/CAGFactories.ts b/src/csg/core/CAGFactories.ts new file mode 100644 index 000000000..ffe2f9196 --- /dev/null +++ b/src/csg/core/CAGFactories.ts @@ -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); +} diff --git a/src/csg/core/CSG.ts b/src/csg/core/CSG.ts new file mode 100644 index 000000000..96fdc07ad --- /dev/null +++ b/src/csg/core/CSG.ts @@ -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; + } +} diff --git a/src/csg/core/FuzzyFactory.ts b/src/csg/core/FuzzyFactory.ts new file mode 100644 index 000000000..5d29b2f96 --- /dev/null +++ b/src/csg/core/FuzzyFactory.ts @@ -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(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; + } + } +} diff --git a/src/csg/core/FuzzyFactory2d.ts b/src/csg/core/FuzzyFactory2d.ts new file mode 100644 index 000000000..8f8cc90ec --- /dev/null +++ b/src/csg/core/FuzzyFactory2d.ts @@ -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); + } +} diff --git a/src/csg/core/FuzzyFactory3d.ts b/src/csg/core/FuzzyFactory3d.ts new file mode 100644 index 000000000..3a3a50bb3 --- /dev/null +++ b/src/csg/core/FuzzyFactory3d.ts @@ -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; + } +} diff --git a/src/csg/core/Geometry2CSG.ts b/src/csg/core/Geometry2CSG.ts new file mode 100644 index 000000000..b2dcd4042 --- /dev/null +++ b/src/csg/core/Geometry2CSG.ts @@ -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); +} diff --git a/src/csg/core/constants.ts b/src/csg/core/constants.ts new file mode 100644 index 000000000..269b0a965 --- /dev/null +++ b/src/csg/core/constants.ts @@ -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++; diff --git a/src/csg/core/math/IsMirrot.ts b/src/csg/core/math/IsMirrot.ts new file mode 100644 index 000000000..10accd45b --- /dev/null +++ b/src/csg/core/math/IsMirrot.ts @@ -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; +} diff --git a/src/csg/core/math/Line2.ts b/src/csg/core/math/Line2.ts new file mode 100644 index 000000000..7f9382aeb --- /dev/null +++ b/src/csg/core/math/Line2.ts @@ -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 .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); + } +} diff --git a/src/csg/core/math/OrthoNormalBasis.ts b/src/csg/core/math/OrthoNormalBasis.ts new file mode 100644 index 000000000..67157c46b --- /dev/null +++ b/src/csg/core/math/OrthoNormalBasis.ts @@ -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)); + } +} diff --git a/src/csg/core/math/Plane.ts b/src/csg/core/math/Plane.ts new file mode 100644 index 000000000..2b6907f13 --- /dev/null +++ b/src/csg/core/math/Plane.ts @@ -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)); + } +} diff --git a/src/csg/core/math/Polygon3.ts b/src/csg/core/math/Polygon3.ts new file mode 100644 index 000000000..bbe124cfc --- /dev/null +++ b/src/csg/core/math/Polygon3.ts @@ -0,0 +1,248 @@ +import { _CSGDEBUG, EPS } from "../constants"; +import { Plane } from "./Plane"; +import { Vector3D } from "./Vector3"; +import { Vertex3D } from "./Vertex3"; +import { arrayRemoveDuplicateBySort } from "../../../Common/ArrayExt"; +import { Matrix4 } from "three"; +import { IsMirror } from "./IsMirrot"; + +export enum Type +{ + CoplanarFront = 0, + CoplanarBack = 1, + Front = 2, + Back = 3, + Spanning = 4, +} + + +interface SplitPolygonData +{ + type: Type; + front: Polygon; + back: Polygon; +} + +/** Class Polygon + * Represents a convex polygon. The vertices used to initialize a polygon must + * be coplanar and form a convex loop. They do not have to be `Vertex` + * instances but they must behave similarly (duck typing can be used for + * customization). + *
+ * Each convex polygon has a `shared` property, which is shared between all + * polygons that are clones of each other or were split from the same polygon. + * This can be used to define per-polygon properties (such as surface color). + *
+ * The plane of the polygon is calculated from the vertex coordinates if not provided. + * The plane can alternatively be passed as the third argument to avoid calculations. + * + *表示凸多边形。 用于初始化多边形的顶点必须共面并形成凸环。 + *多边形是彼此克隆或从同一多边形分割的多边形。 + *这可用于定义每个多边形属性(例如表面颜色)。 + */ +export class Polygon +{ + cachedBoundingSphere: [Vector3D, number]; + cachedBoundingBox: [Vector3D, Vector3D]; + constructor(public vertices: Vertex3D[], public plane?: Plane) + { + if (!plane) + this.plane = Plane.fromVector3Ds(vertices[0].pos, vertices[1].pos, vertices[2].pos); + + if (_CSGDEBUG) + if (!this.checkIfConvex()) throw new Error("Not convex!"); + } + + /** Check whether the polygon is convex. (it should be, otherwise we will get unexpected results)*/ + checkIfConvex(): boolean + { + return Polygon.verticesConvex(this.vertices, this.plane.normal); + } + + // returns an array with a Vector3D (center point) and a radius + + boundingSphere() + { + if (!this.cachedBoundingSphere) + { + let box = this.boundingBox(); + let middle = box[0].clone().add(box[1]).multiplyScalar(0.5); + let radius3 = box[1].clone().sub(middle); + let radius = radius3.length(); + this.cachedBoundingSphere = [middle, radius]; + } + return this.cachedBoundingSphere; + } + + // returns an array of two Vector3Ds (minimum coordinates and maximum coordinates) + + boundingBox(): Vector3D[] + { + if (!this.cachedBoundingBox) + { + let minpoint: Vector3D; + let maxpoint: Vector3D; + let vertices = this.vertices; + let numvertices = vertices.length; + if (numvertices === 0) + minpoint = new Vector3D(0, 0, 0); + else + minpoint = vertices[0].pos.clone(); + maxpoint = minpoint.clone(); + for (let i = 1; i < numvertices; i++) + { + let point = vertices[i].pos; + minpoint.min(point); + maxpoint.max(point); + } + this.cachedBoundingBox = [minpoint, maxpoint]; + } + return this.cachedBoundingBox; + } + + flipped() + { + let newvertices = this.vertices.map(v => v.flipped()); + newvertices.reverse(); + let newplane = this.plane.flipped(); + return new Polygon(newvertices, newplane); + } + + // Affine transformation of polygon. Returns a new Polygon + transform(matrix4x4: Matrix4) + { + let newvertices = this.vertices.map(v => v.transform(matrix4x4)); + let newplane = this.plane.transform(matrix4x4); + if (IsMirror(matrix4x4)) + { + // need to reverse the vertex order + // in order to preserve the inside/outside orientation: + newvertices.reverse(); + } + return new Polygon(newvertices, newplane); + } + + splitByPlane(plane: Plane): SplitPolygonData + { + let result: SplitPolygonData = { type: null, front: null, back: null }; + // cache in local lets (speedup): + let planeNormal = plane.normal; + let vertices = this.vertices; + let numVertices = vertices.length; + if (this.plane.equals(plane)) + { + result.type = Type.CoplanarFront; + } + else + { + let thisW = plane.w; + let hasFront = false; + let hasBack = false; + let vertexIsBack: boolean[] = []; + let MINEPS = -EPS; + for (let i = 0; i < numVertices; i++) + { + let t = planeNormal.dot(vertices[i].pos) - thisW; + let isBack = t < 0; + vertexIsBack.push(isBack); + if (t > EPS) hasFront = true; + if (t < MINEPS) hasBack = true; + } + if (!hasFront && !hasBack) + { + // all points coplanar + let t = planeNormal.dot(this.plane.normal); + result.type = t >= 0 ? Type.CoplanarFront : Type.CoplanarBack; + } + else if (!hasBack) + result.type = Type.Front; + else if (!hasFront) + result.type = Type.Back; + else + { + result.type = Type.Spanning; + let frontVertices: Vertex3D[] = []; + let backVertices: Vertex3D[] = []; + let isBack = vertexIsBack[0]; + for ( + let vertexIndex = 0; + vertexIndex < numVertices; + vertexIndex++ + ) + { + let vertex = vertices[vertexIndex]; + let nextVertexindex = vertexIndex + 1; + if (nextVertexindex >= numVertices) nextVertexindex = 0; + let nextIsBack = vertexIsBack[nextVertexindex]; + if (isBack === nextIsBack) + { + // line segment is on one side of the plane: + if (isBack) + backVertices.push(vertex); + else + frontVertices.push(vertex); + } + else + { + let intersectionVertex = plane.splitLineBetweenPoints(vertex, vertices[nextVertexindex]); + if (isBack) + { + backVertices.push(vertex); + backVertices.push(intersectionVertex); + frontVertices.push(intersectionVertex); + } + else + { + frontVertices.push(vertex); + frontVertices.push(intersectionVertex); + backVertices.push(intersectionVertex); + } + } + isBack = nextIsBack; + } // for vertexindex + // remove duplicate vertices: + let EPS_SQUARED = EPS * EPS; + arrayRemoveDuplicateBySort(backVertices, (v1, v2) => + { + return v1.pos.distanceToSquared(v2.pos) < EPS_SQUARED; + }); + arrayRemoveDuplicateBySort(frontVertices, (v1, v2) => + { + return v1.pos.distanceToSquared(v2.pos) < EPS_SQUARED; + }); + if (frontVertices.length >= 3) + result.front = new Polygon(frontVertices, this.plane); + if (backVertices.length >= 3) + result.back = new Polygon(backVertices, this.plane); + } + } + return result; + } + + static verticesConvex(vertices: Vertex3D[], planenormal: Vector3D) + { + let count = vertices.length; + if (count < 3) return false; + + let prevPrevPos = vertices[count - 2].pos; + let prevPos = vertices[count - 1].pos; + for (let i = 0; i < count; i++) + { + let pos = vertices[i].pos; + if (!Polygon.isConvexPoint(prevPrevPos, prevPos, pos, planenormal)) + return false; + + prevPrevPos = prevPos; + prevPos = pos; + } + return true; + } + + // 计算3点是否凸角 + static isConvexPoint(prevpoint: Vector3D, point: Vector3D, nextpoint: Vector3D, normal: Vector3D) + { + let crossproduct = point.clone().sub(prevpoint).cross(nextpoint.clone().sub(point)); + let crossdotnormal = crossproduct.dot(normal); + return crossdotnormal >= 0; + } +} diff --git a/src/csg/core/math/Side.ts b/src/csg/core/math/Side.ts new file mode 100644 index 000000000..6c8366c38 --- /dev/null +++ b/src/csg/core/math/Side.ts @@ -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; + } +} diff --git a/src/csg/core/math/Vector2.ts b/src/csg/core/math/Vector2.ts new file mode 100644 index 000000000..4b7db69e4 --- /dev/null +++ b/src/csg/core/math/Vector2.ts @@ -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; + } +} diff --git a/src/csg/core/math/Vector3.ts b/src/csg/core/math/Vector3.ts new file mode 100644 index 000000000..da9f7fff2 --- /dev/null +++ b/src/csg/core/math/Vector3.ts @@ -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) + + ")" + ); + } +} diff --git a/src/csg/core/math/Vertex2.ts b/src/csg/core/math/Vertex2.ts new file mode 100644 index 000000000..e5b9626f7 --- /dev/null +++ b/src/csg/core/math/Vertex2.ts @@ -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; + } +} diff --git a/src/csg/core/math/Vertex3.ts b/src/csg/core/math/Vertex3.ts new file mode 100644 index 000000000..65f31f574 --- /dev/null +++ b/src/csg/core/math/Vertex3.ts @@ -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); + } +} diff --git a/src/csg/core/math/lineUtils.ts b/src/csg/core/math/lineUtils.ts new file mode 100644 index 000000000..9d0372a37 --- /dev/null +++ b/src/csg/core/math/lineUtils.ts @@ -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; +}; diff --git a/src/csg/core/math/reTesselateCoplanarPolygons.ts b/src/csg/core/math/reTesselateCoplanarPolygons.ts new file mode 100644 index 000000000..6a4e446c0 --- /dev/null +++ b/src/csg/core/math/reTesselateCoplanarPolygons.ts @@ -0,0 +1,458 @@ +import { EPS } from "../constants"; +import { fnNumberSort, insertSorted, interpolateBetween2DPointsForY } from "../utils"; +import { Line2D } from "./Line2"; +import { OrthoNormalBasis } from "./OrthoNormalBasis"; +import { Polygon } from "./Polygon3"; +import { Vector2D } from "./Vector2"; +import { Vertex3D } from "./Vertex3"; + +//在这个文件中 Top 表示的是 y最小. +// Bottom 表示的是 y最大 + +interface ActivePolygon +{ + polygonindex: number; + leftvertexindex: number; + rightvertexindex: number; + + topleft: Vector2D; + bottomleft: Vector2D; + + topright: Vector2D; + bottomright: Vector2D; +} + +interface OutPolygon +{ + topleft: Vector2D; + topright: Vector2D; + bottomleft: Vector2D; + bottomright: Vector2D; + leftline: Line2D; + rightline: Line2D; + outpolygon?: { leftpoints: Vector2D[]; rightpoints: Vector2D[]; }; + leftlinecontinues?: boolean; + rightlinecontinues?: boolean; +} + +//一组共面多边形的Retesselation函数。 请参阅此文件顶部的介绍。 +export function reTesselateCoplanarPolygons( + sourcePolygons: Polygon[], + destpolygons: Polygon[] = [] +): Polygon[] +{ + let numPolygons = sourcePolygons.length; + if (numPolygons === 0) return; + + let plane = sourcePolygons[0].plane; + let orthobasis = new OrthoNormalBasis(plane); + + // let xcoordinatebins = {} + let yCoordinateBins: { [key: number]: number } = {}; //整数map + let yCoordinateBinningFactor = (1.0 / EPS) * 10; + + let polygonVertices2d: (Vector2D[])[] = []; // (Vector2[])[]; + let polygonTopVertexIndexes: number[] = []; // 每个多边形最顶层顶点的索引数组 minIndex + let topY2PolygonIndexes: { [key: number]: number[] } = {}; // Map + let yCoordinateToPolygonIndexes: { [key: string]: { [key: number]: boolean }; } = {}; // Map > Y坐标映射所有的多边形 + + //将多边形转换为2d点表 polygonVertices2d + //建立y对应的多边形Map yCoordinateToPolygonIndexes + for (let polygonIndex = 0; polygonIndex < numPolygons; polygonIndex++) + { + let poly3d = sourcePolygons[polygonIndex]; + let numVertices = poly3d.vertices.length; + + if (numVertices === 0) continue; + + let vertices2d: Vector2D[] = []; //Vector2d[]; + let minIndex = -1; + let miny: number, maxy: number; + for (let i = 0; i < numVertices; i++) + { + let pos2d = orthobasis.to2D(poly3d.vertices[i].pos); + // perform binning of y coordinates: If we have multiple vertices very + // close to each other, give them the same y coordinate: + let yCoordinatebin = Math.floor(pos2d.y * yCoordinateBinningFactor); + let newy: number; + if (yCoordinatebin in yCoordinateBins) + newy = yCoordinateBins[yCoordinatebin]; + else if (yCoordinatebin + 1 in yCoordinateBins) + newy = yCoordinateBins[yCoordinatebin + 1]; + else if (yCoordinatebin - 1 in yCoordinateBins) + newy = yCoordinateBins[yCoordinatebin - 1]; + else + { + newy = pos2d.y; + yCoordinateBins[yCoordinatebin] = pos2d.y; + } + pos2d = new Vector2D(pos2d.x, newy); + vertices2d.push(pos2d); + if (i === 0 || newy < miny) + { + miny = newy; + minIndex = i; + } + if (i === 0 || newy > maxy) maxy = newy; + + if (!(newy in yCoordinateToPolygonIndexes)) + yCoordinateToPolygonIndexes[newy] = {}; + + yCoordinateToPolygonIndexes[newy][polygonIndex] = true; + } + + //退化多边形,所有顶点都具有相同的y坐标。 从现在开始忽略它: + if (miny >= maxy) continue; + + if (!(miny in topY2PolygonIndexes)) topY2PolygonIndexes[miny] = []; + + topY2PolygonIndexes[miny].push(polygonIndex); + + // reverse the vertex order: + vertices2d.reverse(); + minIndex = numVertices - minIndex - 1; + polygonVertices2d.push(vertices2d); + polygonTopVertexIndexes.push(minIndex); + } + + //所有的y坐标,从小到大排序 + let yCoordinates: string[] = []; + for (let ycoordinate in yCoordinateToPolygonIndexes) + yCoordinates.push(ycoordinate); + yCoordinates.sort(fnNumberSort); + + //迭代y坐标 从低到高 + + // activepolygons :'active'的源多边形,即与y坐标相交 + // 多边形是从左往右排序的 + // activepolygons 中的每个元素都具有以下属性: + // polygonindex 源多边形的索引(即sourcepolygons的索引 和polygonvertices2d数组) + // leftvertexindex 左边 在当前y坐标处或刚好在当前y坐标之上 + // rightvertexindex 右边 + // topleft bottomleft 与当前y坐标交叉的多边形左侧的坐标 + // topright bottomright 与当前y坐标交叉的多边形右侧的坐标 + + let activePolygons: ActivePolygon[] = []; + let prevOutPolygonRow: OutPolygon[] = []; //上一个输出多边形行? + for (let yindex = 0; yindex < yCoordinates.length; yindex++) + { + let yCoordinateStr = yCoordinates[yindex]; + let yCoordinate = Number(yCoordinateStr); + + // 用当前的y 更新 activePolygons + // - 删除以y坐标结尾的所有多边形 删除polygon maxy = y 的多边形 + // - 更新 leftvertexindex 和 rightvertexindex (指向当前顶点索引) + // 在多边形的左侧和右侧 + + // 迭代在Y坐标处有一个角的所有多边形 + let polygonIndexeSwithCorner = + yCoordinateToPolygonIndexes[yCoordinateStr]; + for ( + let activePolygonIndex = 0; + activePolygonIndex < activePolygons.length; + activePolygonIndex++ + ) + { + let activepolygon = activePolygons[activePolygonIndex]; + let polygonindex = activepolygon.polygonindex; + + if (!polygonIndexeSwithCorner[polygonindex])//如果不在角内 + continue; + + //多边形在此y坐标处有一个角 + let vertices2d = polygonVertices2d[polygonindex]; + let numvertices = vertices2d.length; + let newleftvertexindex = activepolygon.leftvertexindex; + let newrightvertexindex = activepolygon.rightvertexindex; + + //看看我们是否需要增加 leftvertexindex 或减少 rightvertexindex : + while (true) + { + let nextleftvertexindex = newleftvertexindex + 1; + if (nextleftvertexindex >= numvertices) nextleftvertexindex = 0; + if (vertices2d[nextleftvertexindex].y !== yCoordinate) break; + newleftvertexindex = nextleftvertexindex; + } + //减少 rightvertexindex + let nextrightvertexindex = newrightvertexindex - 1; + if (nextrightvertexindex < 0) + nextrightvertexindex = numvertices - 1; + if (vertices2d[nextrightvertexindex].y === yCoordinate) + newrightvertexindex = nextrightvertexindex; + + if ( + newleftvertexindex !== activepolygon.leftvertexindex //有向上更新 + && newleftvertexindex === newrightvertexindex //指向同一个点 + ) + { + + // We have increased leftvertexindex or decreased rightvertexindex, and now they point to the same vertex + // This means that this is the bottom point of the polygon. We'll remove it: + //我们增加了leftvertexindex或减少了rightvertexindex,现在它们指向同一个顶点 + //这意味着这是多边形的底点。 我们将删除它: + activePolygons.splice(activePolygonIndex, 1); + --activePolygonIndex; + } else + { + activepolygon.leftvertexindex = newleftvertexindex; + activepolygon.rightvertexindex = newrightvertexindex; + activepolygon.topleft = vertices2d[newleftvertexindex]; + activepolygon.topright = vertices2d[newrightvertexindex]; + let nextleftvertexindex = newleftvertexindex + 1; + if (nextleftvertexindex >= numvertices) nextleftvertexindex = 0; + activepolygon.bottomleft = vertices2d[nextleftvertexindex]; + let nextrightvertexindex = newrightvertexindex - 1; + if (nextrightvertexindex < 0) nextrightvertexindex = numvertices - 1; + activepolygon.bottomright = vertices2d[nextrightvertexindex]; + } + } + + let nextYCoordinate: number; // number y + if (yindex >= yCoordinates.length - 1) + { + // last row, all polygons must be finished here: + // 最后一行,所有多边形必须在这里完成: + activePolygons = []; + } + else // yindex < ycoordinates.length-1 + { + nextYCoordinate = Number(yCoordinates[yindex + 1]); + let middleYCoordinate = 0.5 * (yCoordinate + nextYCoordinate); + // update activepolygons by adding any polygons that start here: + // 添加从这里开始的多边形 到 activePolygons + let startingPolygonIndexes = topY2PolygonIndexes[yCoordinateStr]; + for (let polygonindex_key in startingPolygonIndexes) + { + let polygonindex = startingPolygonIndexes[polygonindex_key]; + let vertices2d = polygonVertices2d[polygonindex]; + let numvertices = vertices2d.length; + let topVertexIndex = polygonTopVertexIndexes[polygonindex]; + // the top of the polygon may be a horizontal line. In that case topvertexindex can point to any point on this line. + // Find the left and right topmost vertices which have the current y coordinate: + // 顶部可以是一条直线,寻找最左边的点和最右边的点 + let topleftvertexindex = topVertexIndex; + while (true) + { + let i = topleftvertexindex + 1; + if (i >= numvertices) i = 0; + if (vertices2d[i].y !== yCoordinate) break; + if (i === topVertexIndex) break; // should not happen, but just to prevent endless loops + topleftvertexindex = i; + } + let toprightvertexindex = topVertexIndex; + while (true) + { + let i = toprightvertexindex - 1; + if (i < 0) i = numvertices - 1; + if (vertices2d[i].y !== yCoordinate) break; + if (i === topleftvertexindex) break; // should not happen, but just to prevent endless loops + toprightvertexindex = i; + } + + let nextleftvertexindex = topleftvertexindex + 1; + if (nextleftvertexindex >= numvertices) nextleftvertexindex = 0; + let nextrightvertexindex = toprightvertexindex - 1; + if (nextrightvertexindex < 0) nextrightvertexindex = numvertices - 1; + let newactivepolygon: ActivePolygon = { + polygonindex: polygonindex, + leftvertexindex: topleftvertexindex, + rightvertexindex: toprightvertexindex, + topleft: vertices2d[topleftvertexindex], + topright: vertices2d[toprightvertexindex], + bottomleft: vertices2d[nextleftvertexindex], + bottomright: vertices2d[nextrightvertexindex] + }; + + //二分插入 + insertSorted(activePolygons, newactivepolygon, function (el1: ActivePolygon, el2: ActivePolygon) + { + let x1 = interpolateBetween2DPointsForY( + el1.topleft, + el1.bottomleft, + middleYCoordinate + ); + let x2 = interpolateBetween2DPointsForY( + el2.topleft, + el2.bottomleft, + middleYCoordinate + ); + if (x1 > x2) return 1; + if (x1 < x2) return -1; + return 0; + }); + } + } + + //#region + // if( (yindex === ycoordinates.length-1) || (nextycoordinate - ycoordinate > EPS) ) + // if(true) + // { + + let newOutPolygonRow: OutPolygon[] = []; //输出多边形 + + // Build the output polygons for the next row in newOutPolygonRow: + //现在 activepolygons 是最新的 + //为 newOutPolygonRow 中的下一行构建输出多边形: + for (let activepolygonKey in activePolygons) + { + let activepolygon = activePolygons[activepolygonKey]; + + let x = interpolateBetween2DPointsForY( + activepolygon.topleft, + activepolygon.bottomleft, + yCoordinate + ); + let topleft = new Vector2D(x, yCoordinate); + x = interpolateBetween2DPointsForY( + activepolygon.topright, + activepolygon.bottomright, + yCoordinate + ); + let topright = new Vector2D(x, yCoordinate); + x = interpolateBetween2DPointsForY( + activepolygon.topleft, + activepolygon.bottomleft, + nextYCoordinate + ); + let bottomleft = new Vector2D(x, nextYCoordinate); + x = interpolateBetween2DPointsForY( + activepolygon.topright, + activepolygon.bottomright, + nextYCoordinate + ); + let bottomright = new Vector2D(x, nextYCoordinate); + let outPolygon = { + topleft: topleft, + topright: topright, + bottomleft: bottomleft, + bottomright: bottomright, + leftline: Line2D.fromPoints(topleft, bottomleft), + rightline: Line2D.fromPoints(bottomright, topright) + }; + + if (newOutPolygonRow.length > 0) + { + let prevoutpolygon = + newOutPolygonRow[newOutPolygonRow.length - 1]; + let d1 = outPolygon.topleft.distanceTo(prevoutpolygon.topright); + let d2 = outPolygon.bottomleft.distanceTo( + prevoutpolygon.bottomright + ); + if (d1 < EPS && d2 < EPS) + { + // we can join this polygon with the one to the left: + outPolygon.topleft = prevoutpolygon.topleft; + outPolygon.leftline = prevoutpolygon.leftline; + outPolygon.bottomleft = prevoutpolygon.bottomleft; + newOutPolygonRow.splice(newOutPolygonRow.length - 1, 1); + } + } + + newOutPolygonRow.push(outPolygon); + } + + if (yindex > 0) + { + // try to match the new polygons against the previous row: + //尝试将新多边形与上一行匹配: + let prevContinuedIndexes: { [key: number]: boolean } = {}; + let matchedIndexes: { [key: number]: boolean } = {}; + for (let i = 0; i < newOutPolygonRow.length; i++) + { + let thispolygon = newOutPolygonRow[i]; + for (let ii = 0; ii < prevOutPolygonRow.length; ii++) + { + if (!matchedIndexes[ii]) + { + // not already processed? + // We have a match if the sidelines are equal or if the top coordinates + // are on the sidelines of the previous polygon + let prevpolygon = prevOutPolygonRow[ii]; + if (prevpolygon.bottomleft.distanceTo(thispolygon.topleft) < EPS) + { + if (prevpolygon.bottomright.distanceTo(thispolygon.topright) < EPS) + { + // Yes, the top of this polygon matches the bottom of the previous: + matchedIndexes[ii] = true; + // Now check if the joined polygon would remain convex: + let d1 = thispolygon.leftline.direction().x - prevpolygon.leftline.direction().x; + let d2 = thispolygon.rightline.direction().x - prevpolygon.rightline.direction().x; + let leftlinecontinues = Math.abs(d1) < EPS; + let rightlinecontinues = Math.abs(d2) < EPS; + let leftlineisconvex = leftlinecontinues || d1 >= 0; + let rightlineisconvex = rightlinecontinues || d2 >= 0; + if (leftlineisconvex && rightlineisconvex) + { + // yes, both sides have convex corners: + // This polygon will continue the previous polygon + thispolygon.outpolygon = prevpolygon.outpolygon; + thispolygon.leftlinecontinues = leftlinecontinues; + thispolygon.rightlinecontinues = rightlinecontinues; + prevContinuedIndexes[ii] = true; + } + break; + } + } + } // if(!prevcontinuedindexes[ii]) + } // for ii + } // for i + for (let ii = 0; ii < prevOutPolygonRow.length; ii++) + { + if (!prevContinuedIndexes[ii]) + { + // polygon ends here + // Finish the polygon with the last point(s): + let prevpolygon = prevOutPolygonRow[ii]; + prevpolygon.outpolygon.rightpoints.push(prevpolygon.bottomright); + if (prevpolygon.bottomright.distanceTo(prevpolygon.bottomleft) > EPS) + { + // polygon ends with a horizontal line: + prevpolygon.outpolygon.leftpoints.push(prevpolygon.bottomleft); + } + // reverse the left half so we get a counterclockwise circle: + prevpolygon.outpolygon.leftpoints.reverse(); + let points2d = prevpolygon.outpolygon.rightpoints.concat(prevpolygon.outpolygon.leftpoints); + + let vertices = points2d.map(v => new Vertex3D(orthobasis.to3D(v))); + let polygon = new Polygon(vertices, plane); + destpolygons.push(polygon); + } + } + } + + for (let i = 0; i < newOutPolygonRow.length; i++) + { + let thispolygon = newOutPolygonRow[i]; + if (!thispolygon.outpolygon) + { + // polygon starts here: + thispolygon.outpolygon = { + leftpoints: [], + rightpoints: [] + }; + thispolygon.outpolygon.leftpoints.push(thispolygon.topleft); + if (thispolygon.topleft.distanceTo(thispolygon.topright) > EPS) + { + // we have a horizontal line at the top: + thispolygon.outpolygon.rightpoints.push(thispolygon.topright); + } + } + else + { + // continuation of a previous row + if (!thispolygon.leftlinecontinues) + { + thispolygon.outpolygon.leftpoints.push(thispolygon.topleft); + } + if (!thispolygon.rightlinecontinues) + { + thispolygon.outpolygon.rightpoints.push(thispolygon.topright); + } + } + } + + prevOutPolygonRow = newOutPolygonRow; + // } + //#endregion + } // for yindex +} diff --git a/src/csg/core/trees.ts b/src/csg/core/trees.ts new file mode 100644 index 000000000..582808569 --- /dev/null +++ b/src/csg/core/trees.ts @@ -0,0 +1,482 @@ +import { EPS, _CSGDEBUG } from "./constants"; +import { Plane } from "./math/Plane"; +import { Polygon, Type } from "./math/Polygon3"; +import { Vector3D } from "./math/Vector3"; + +// # class PolygonTreeNode +// This class manages hierarchical splits of polygons +// At the top is a root node which doesn hold a polygon, only child PolygonTreeNodes +// Below that are zero or more 'top' nodes; each holds a polygon. The polygons can be in different planes +// splitByPlane() splits a node by a plane. If the plane intersects the polygon, two new child nodes +// are created holding the splitted polygon. +// getPolygons() retrieves the polygon from the tree. If for PolygonTreeNode the polygon is split but +// the two split parts (child nodes) are still intact, then the unsplit polygon is returned. +// This ensures that we can safely split a polygon into many fragments. If the fragments are untouched, +// getPolygons() will return the original unsplit polygon instead of the fragments. +// remove() removes a polygon from the tree. Once a polygon is removed, the parent polygons are invalidated +// since they are no longer intact. +// constructor creates the root node: +//此类管理多边形的层次分割 +//顶部是一个根节点,它不包含多边形,只有子PolygonTreeNodes +//下面是零个或多个“顶部”节点; 每个都有一个多边形。 多边形可以位于不同的平面中 +// splitByPlane()按平面拆分节点。 如果平面与多边形相交,则会有两个新的子节点 +//创建持有分割多边形。 +// getPolygons()从树中检索多边形。 如果对于PolygonTreeNode,则多边形被拆分但是 +//两个分割部分(子节点)仍然完好无损,然后返回未分割的多边形。 +//这确保我们可以安全地将多边形拆分为多个片段。 如果碎片未受影响, +// getPolygons()将返回原始的未分割多边形而不是片段。 +// remove()从树中删除多边形。 删除多边形后,父多边形将失效 +//因为它们不再完好无损 +//构造函数创建根节点: +class PolygonTreeNode +{ + parent: PolygonTreeNode; + children: PolygonTreeNode[] = []; + polygon: Polygon; + removed: boolean = false; + constructor(polygon?: Polygon) + { + this.polygon = polygon; + } + + // fill the tree with polygons. Should be called on the root node only; child nodes must + // always be a derivate (split) of the parent node. + addPolygons(polygons: Polygon[]) + { + // new polygons can only be added to root node; children can only be splitted polygons + if (!this.isRootNode()) + throw new Error("Assertion failed"); + + for (let polygon of polygons) + this.addChild(polygon); + } + + // remove a node + // - the siblings become toplevel nodes + // - the parent is removed recursively + + remove() + { + if (this.removed) return; + + this.removed = true; + + if (_CSGDEBUG) + { + if (this.isRootNode()) throw new Error("Assertion failed"); // can't remove root node + if (this.children.length) throw new Error("Assertion failed"); // we shouldn't remove nodes with children + } + + // remove ourselves from the parent's children list: + let parentschildren = this.parent.children; + let i = parentschildren.indexOf(this); + if (i < 0) throw new Error("Assertion failed"); + parentschildren.splice(i, 1); + + // invalidate the parent's polygon, and of all parents above it: + this.parent.recursivelyInvalidatePolygon(); + } + + isRemoved() + { + return this.removed; + } + + isRootNode() + { + return !this.parent; + } + + // invert all polygons in the tree. Call on the root node + + invert() + { + if (!this.isRootNode()) throw new Error("Assertion failed"); // can only call this on the root node + this.invertSub(); + } + + getPolygon(): Polygon + { + if (!this.polygon) throw new Error("Assertion failed"); // doesn't have a polygon, which means that it has been broken down + return this.polygon; + } + + getPolygons(outPolygons: Polygon[] = []): Polygon[] + { + let children: PolygonTreeNode[] = [this]; + let queue = [children]; + for (let i = 0; i < queue.length; ++i) + { + // queue size can change in loop, don't cache length + children = queue[i]; + for (let node of children) + { + if (node.polygon) + // the polygon hasn't been broken yet. We can ignore the children and return our polygon: + outPolygons.push(node.polygon); + else + // our polygon has been split up and broken, so gather all subpolygons from the children + queue.push(node.children); + } + } + + return outPolygons; + } + + // split the node by a plane; add the resulting nodes to the frontnodes and backnodes array + // If the plane doesn't intersect the polygon, the 'this' object is added to one of the arrays + // If the plane does intersect the polygon, two new child nodes are created for the front and back fragments, + // and added to both arrays. + + splitByPlane( + plane: Plane, + coplanarFrontNodes: PolygonTreeNode[], + coplanarBackNodes: PolygonTreeNode[], + frontNodes: PolygonTreeNode[], + backNodes: PolygonTreeNode[] + ) + { + if (this.children.length) + { + let queue = [this.children]; + for (let i = 0; i < queue.length; i++) + { + // queue.length can increase, do not cache + let nodes = queue[i]; + for (let j = 0, l = nodes.length; j < l; j++) + { + // ok to cache length + let node = nodes[j]; + if (node.children.length) + queue.push(node.children); + else + { + // no children. Split the polygon: + node.splitByPlaneNotChildren(plane, coplanarFrontNodes, coplanarBackNodes, frontNodes, backNodes); + } + } + } + } + else + { + this.splitByPlaneNotChildren(plane, coplanarFrontNodes, coplanarBackNodes, frontNodes, backNodes); + } + } + + // only to be called for nodes with no children + // 仅用于没有子节点的节点 + private splitByPlaneNotChildren( + plane: Plane, + coplanarFrontNodes: PolygonTreeNode[], + coplanarBackNodes: PolygonTreeNode[], + frontNodes: PolygonTreeNode[], + backNodes: PolygonTreeNode[] + ) + { + if (!this.polygon) return; + + let polygon = this.polygon; + let bound = polygon.boundingSphere(); + let sphereradius = bound[1] + EPS; // FIXME Why add imprecision? + let planenormal = plane.normal; + let spherecenter = bound[0]; + let d = planenormal.dot(spherecenter) - plane.w; + if (d > sphereradius) + frontNodes.push(this); + else if (d < -sphereradius) + backNodes.push(this); + else + { + let splitresult = polygon.splitByPlane(plane); + switch (splitresult.type) + { + case Type.CoplanarFront: + coplanarFrontNodes.push(this); + break; + + case Type.CoplanarBack: + coplanarBackNodes.push(this); + break; + + case Type.Front: + frontNodes.push(this); + break; + + case Type.Back: + backNodes.push(this); + break; + + case Type.Spanning: + if (splitresult.front) + { + let frontNode = this.addChild(splitresult.front); + frontNodes.push(frontNode); + } + if (splitresult.back) + { + let backNode = this.addChild(splitresult.back); + backNodes.push(backNode); + } + break; + } + } + } + + // add child to a node + // this should be called whenever the polygon is split + // a child should be created for every fragment of the split polygon + // returns the newly created child + addChild(polygon: Polygon): PolygonTreeNode + { + let newchild = new PolygonTreeNode(polygon); + newchild.parent = this; + this.children.push(newchild); + return newchild; + } + + invertSub() + { + let queue: PolygonTreeNode[][] = [[this]]; + for (let i = 0; i < queue.length; i++) + { + let children = queue[i]; + for (let j = 0, l = children.length; j < l; j++) + { + let node = children[j]; + if (node.polygon) + node.polygon = node.polygon.flipped(); + queue.push(node.children); + } + } + } + + recursivelyInvalidatePolygon() + { + let node: PolygonTreeNode = this; + while (node.polygon) + { + node.polygon = null; + if (node.parent) + node = node.parent; + } + } +} + +// # class Tree +// This is the root of a BSP tree +// We are using this separate class for the root of the tree, to hold the PolygonTreeNode root +// The actual tree is kept in this.rootnode +export class Tree +{ + polygonTree = new PolygonTreeNode(); + rootNode = new Node(null); + constructor(polygons: Polygon[]) + { + this.addPolygons(polygons); + } + + invert() + { + this.polygonTree.invert(); + this.rootNode.invert(); + } + + // Remove all polygons in this BSP tree that are inside the other BSP tree + /** + * this 减去 tree 删除此BSP树中位于其他BSP树内的所有多边形 + * @param tree 不会被修改 + * @param [alsoRemovecoplanarFront=false] 同时删除共面 + */ + clipTo(tree: Tree, alsoRemovecoplanarFront = false) + { + this.rootNode.clipTo(tree, alsoRemovecoplanarFront); + } + + allPolygons() + { + return this.polygonTree.getPolygons(); + } + + addPolygons(polygons: Polygon[]) + { + let polygonTreeNodes = polygons.map((p) => this.polygonTree.addChild(p)); + this.rootNode.addPolygonTreeNodes(polygonTreeNodes); + } +} + +// # class Node +// Holds a node in a BSP tree. A BSP tree is built from a collection of polygons +// by picking a polygon to split along. +// Polygons are not stored directly in the tree, but in PolygonTreeNodes, stored in +// this.polygontreenodes. Those PolygonTreeNodes are children of the owning +// Tree.polygonTree +// This is not a leafy BSP tree since there is +// no distinction between internal and leaf nodes. +class Node +{ + plane: Plane; + front: Node; + back: Node; + polygonTreeNodes: PolygonTreeNode[] = []; + parent: Node; + constructor(parent: Node) + { + this.parent = parent; + } + + // Convert solid space to empty space and empty space to solid space. + invert() + { + let queue: Node[] = [this]; + for (let i = 0; i < queue.length; i++) + { + let node = queue[i]; + if (node.plane) node.plane = node.plane.flipped(); + if (node.front) queue.push(node.front); + if (node.back) queue.push(node.back); + let temp = node.front; + node.front = node.back; + node.back = temp; + } + } + + // clip polygontreenodes to our plane + // calls remove() for all clipped PolygonTreeNodes + //将polygontreenodes剪辑到我们的飞机上 + //为所有剪切的PolygonTreeNodes调用remove() + clipPolygons(polygonTreeNodes: PolygonTreeNode[], alsoRemoveCoplanarFront: boolean) + { + interface D + { + node: Node; + polygonTreeNodes: PolygonTreeNode[]; + } + + let args: D = { node: this, polygonTreeNodes }; + let stack: D[] = []; + + do + { + let node = args.node; + let polygonTreeNodes1 = args.polygonTreeNodes; + + // begin "function" + if (node.plane) + { + let backnodes: PolygonTreeNode[] = []; + let frontnodes: PolygonTreeNode[] = []; + let coplanarfrontnodes = alsoRemoveCoplanarFront ? backnodes : frontnodes; + let plane = node.plane; + for (let node1 of polygonTreeNodes1) + { + if (!node1.isRemoved()) + node1.splitByPlane(plane, coplanarfrontnodes, backnodes, frontnodes, backnodes); + } + + if (node.front && frontnodes.length > 0) + stack.push({ node: node.front, polygonTreeNodes: frontnodes }); + + let numbacknodes = backnodes.length; + if (node.back && numbacknodes > 0) + stack.push({ node: node.back, polygonTreeNodes: backnodes }); + else + { + // there's nothing behind this plane. Delete the nodes behind this plane: + // 这架飞机背后什么也没有。 删除此平面后面的节点: + for (let i = 0; i < numbacknodes; i++) + backnodes[i].remove(); + } + } + args = stack.pop(); + } + while (args); + } + + // Remove all polygons in this BSP tree that are inside the other BSP tree + // `tree`. + clipTo(tree: Tree, alsoRemovecoplanarFront: boolean) + { + let node: Node = this; + let stack: Node[] = []; + do + { + if (node.polygonTreeNodes.length > 0) + { + tree.rootNode.clipPolygons( + node.polygonTreeNodes, + alsoRemovecoplanarFront + ); + } + if (node.front) stack.push(node.front); + if (node.back) stack.push(node.back); + node = stack.pop(); + } + while (node); + } + + addPolygonTreeNodes(polygonTreeNodes: PolygonTreeNode[]) + { + interface D + { + node: Node; + polygontreenodes: PolygonTreeNode[]; + } + let args: D = { node: this, polygontreenodes: polygonTreeNodes }; + let stack: D[] = []; + do + { + let node = args.node; + polygonTreeNodes = args.polygontreenodes; + + if (polygonTreeNodes.length === 0) + { + args = stack.pop(); + continue; + } + if (!node.plane) + { + let bestplane = polygonTreeNodes[Math.floor(polygonTreeNodes.length / 2)].getPolygon().plane; + node.plane = bestplane; + } + let frontNodes: PolygonTreeNode[] = []; + let backNodes: PolygonTreeNode[] = []; + + for (let i = 0, n = polygonTreeNodes.length; i < n; ++i) + { + polygonTreeNodes[i].splitByPlane( + node.plane, + node.polygonTreeNodes, + backNodes, + frontNodes, + backNodes + ); + } + + if (frontNodes.length > 0) + { + if (!node.front) node.front = new Node(node); + stack.push({ node: node.front, polygontreenodes: frontNodes }); + } + if (backNodes.length > 0) + { + if (!node.back) node.back = new Node(node); + stack.push({ node: node.back, polygontreenodes: backNodes }); + } + + args = stack.pop(); + } + while (args); + } + + getParentPlaneNormals(normals: Vector3D[], maxdepth: number) + { + if (maxdepth > 0) + { + if (this.parent) + { + normals.push(this.parent.plane.normal); + this.parent.getParentPlaneNormals(normals, maxdepth - 1); + } + } + } +} diff --git a/src/csg/core/utils.ts b/src/csg/core/utils.ts new file mode 100644 index 000000000..79eb06eed --- /dev/null +++ b/src/csg/core/utils.ts @@ -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(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; +} diff --git a/src/csg/core/utils/cagValidation.ts b/src/csg/core/utils/cagValidation.ts new file mode 100644 index 000000000..e2b3c3615 --- /dev/null +++ b/src/csg/core/utils/cagValidation.ts @@ -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; +}; diff --git a/src/csg/core/utils/canonicalize.ts b/src/csg/core/utils/canonicalize.ts new file mode 100644 index 000000000..c20c5b190 --- /dev/null +++ b/src/csg/core/utils/canonicalize.ts @@ -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); +}; diff --git a/src/csg/core/utils/csgMeasurements.ts b/src/csg/core/utils/csgMeasurements.ts new file mode 100644 index 000000000..b129ded89 --- /dev/null +++ b/src/csg/core/utils/csgMeasurements.ts @@ -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; +}; diff --git a/src/csg/core/utils/retesellate.ts b/src/csg/core/utils/retesellate.ts new file mode 100644 index 000000000..fa2ba883d --- /dev/null +++ b/src/csg/core/utils/retesellate.ts @@ -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; +};