openjscad-aurora-webapp
/
csg.js
5969 строк · 193.0 Кб
1/*
2## License
3
4Copyright (c) 2013 Eduard Bespalov (edwbes@gmail.com): .solidFromSlices()
5Copyright (c) 2013 Rene K. Mueller (http://OpenJSCAD.org): AMF export added, CSG.center([flag,flag,flag]);
6Copyright (c) 2012 Joost Nieuwenhuijse (joost@newhouse.nl)
7Copyright (c) 2012 Alexandre Girard (https://github.com/alx)
8Copyright (c) 2011 Evan Wallace (http://evanw.github.com/csg.js/) -- original csg.js
9
10All code released under MIT license
11
12## Overview
13
14For an overview of the CSG process see the original csg.js code:
15http://evanw.github.com/csg.js/
16
17CSG operations through BSP trees suffer from one problem: heavy fragmentation
18of polygons. If two CSG solids of n polygons are unified, the resulting solid may have
19in the order of n*n polygons, because each polygon is split by the planes of all other
20polygons. After a few operations the number of polygons explodes.
21
22This version of CSG.js solves the problem in 3 ways:
23
241. Every polygon split is recorded in a tree (CSG.PolygonTreeNode). This is a separate
25tree, not to be confused with the CSG tree. If a polygon is split into two parts but in
26the end both fragments have not been discarded by the CSG operation, we can retrieve
27the original unsplit polygon from the tree, instead of the two fragments.
28
29This does not completely solve the issue though: if a polygon is split multiple times
30the number of fragments depends on the order of subsequent splits, and we might still
31end up with unncessary splits:
32Suppose a polygon is first split into A and B, and then into A1, B1, A2, B2. Suppose B2 is
33discarded. We will end up with 2 polygons: A and B1. Depending on the actual split boundaries
34we could still have joined A and B1 into one polygon. Therefore a second approach is used as well:
35
362. After CSG operations all coplanar polygon fragments are joined by a retesselating
37operation. See CSG.reTesselated(). Retesselation is done through a
38linear sweep over the polygon surface. The sweep line passes over the y coordinates
39of all vertices in the polygon. Polygons are split at each sweep line, and the fragments
40are joined horizontally and vertically into larger polygons (making sure that we
41will end up with convex polygons).
42This still doesn't solve the problem completely: due to floating point imprecisions
43we may end up with small gaps between polygons, and polygons may not be exactly coplanar
44anymore, and as a result the retesselation algorithm may fail to join those polygons.
45Therefore:
46
473. A canonicalization algorithm is implemented: it looks for vertices that have
48approximately the same coordinates (with a certain tolerance, say 1e-5) and replaces
49them with the same vertex. If polygons share a vertex they will actually point to the
50same CSG.Vertex instance. The same is done for polygon planes. See CSG.canonicalized().
51
52
53Performance improvements to the original CSG.js:
54
55Replaced the flip() and invert() methods by flipped() and inverted() which don't
56modify the source object. This allows to get rid of all clone() calls, so that
57multiple polygons can refer to the same CSG.Plane instance etc.
58
59The original union() used an extra invert(), clipTo(), invert() sequence just to remove the
60coplanar front faces from b; this is now combined in a single b.clipTo(a, true) call.
61
62Detection whether a polygon is in front or in back of a plane: for each polygon
63we are caching the coordinates of the bounding sphere. If the bounding sphere is
64in front or in back of the plane we don't have to check the individual vertices
65anymore.
66
67
68Other additions to the original CSG.js:
69
70CSG.Vector class has been renamed into CSG.Vector3D
71
72Classes for 3D lines, 2D vectors, 2D lines, and methods to find the intersection of
73a line and a plane etc.
74
75Transformations: CSG.transform(), CSG.translate(), CSG.rotate(), CSG.scale()
76
77Expanding or contracting a solid: CSG.expand() and CSG.contract(). Creates nice
78smooth corners.
79
80The vertex normal has been removed since it complicates retesselation. It's not needed
81for solid CAD anyway.
82
83*/
84
85(function(module){86
87var _CSGDEBUG = false;88
89function fnNumberSort(a, b) {90return a - b;91}
92
93// # class CSG
94// Holds a binary space partition tree representing a 3D solid. Two solids can
95// be combined using the `union()`, `subtract()`, and `intersect()` methods.
96var CSG = function() {97this.polygons = [];98this.properties = new CSG.Properties();99this.isCanonicalized = true;100this.isRetesselated = true;101};102
103CSG.defaultResolution2D = 32;104CSG.defaultResolution3D = 12;105
106// Construct a CSG solid from a list of `CSG.Polygon` instances.
107CSG.fromPolygons = function(polygons) {108var csg = new CSG();109csg.polygons = polygons;110csg.isCanonicalized = false;111csg.isRetesselated = false;112return csg;113};114
115// Construct a CSG solid from generated slices.
116// Look at CSG.Polygon.prototype.solidFromSlices for details
117CSG.fromSlices = function(options) {118return (new CSG.Polygon.createFromPoints([119[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]120])).solidFromSlices(options);121};122
123// create from an untyped object with identical property names:
124CSG.fromObject = function(obj) {125var polygons = obj.polygons.map(function(p) {126return CSG.Polygon.fromObject(p);127});128var csg = CSG.fromPolygons(polygons);129csg = csg.canonicalized();130return csg;131};132
133// Reconstruct a CSG from the output of toCompactBinary()
134CSG.fromCompactBinary = function(bin) {135if(bin['class'] != "CSG") throw new Error("Not a CSG");136var planes = [],137planeData = bin.planeData,138numplanes = planeData.length / 4,139arrayindex = 0,140x, y, z, w, normal, plane;141for(var planeindex = 0; planeindex < numplanes; planeindex++) {142x = planeData[arrayindex++];143y = planeData[arrayindex++];144z = planeData[arrayindex++];145w = planeData[arrayindex++];146normal = new CSG.Vector3D(x, y, z);147plane = new CSG.Plane(normal, w);148planes.push(plane);149}150
151var vertices = [],152vertexData = bin.vertexData,153numvertices = vertexData.length / 3,154pos, vertex;155arrayindex = 0;156for(var vertexindex = 0; vertexindex < numvertices; vertexindex++) {157x = vertexData[arrayindex++];158y = vertexData[arrayindex++];159z = vertexData[arrayindex++];160pos = new CSG.Vector3D(x, y, z);161vertex = new CSG.Vertex(pos);162vertices.push(vertex);163}164
165var shareds = bin.shared.map(function(shared) {166return CSG.Polygon.Shared.fromObject(shared);167});168
169var polygons = [],170numpolygons = bin.numPolygons,171numVerticesPerPolygon = bin.numVerticesPerPolygon,172polygonVertices = bin.polygonVertices,173polygonPlaneIndexes = bin.polygonPlaneIndexes,174polygonSharedIndexes = bin.polygonSharedIndexes,175numpolygonvertices, polygonvertices, shared, polygon;//already defined plane,176arrayindex = 0;177for(var polygonindex = 0; polygonindex < numpolygons; polygonindex++) {178numpolygonvertices = numVerticesPerPolygon[polygonindex];179polygonvertices = [];180for(var i = 0; i < numpolygonvertices; i++) {181polygonvertices.push(vertices[polygonVertices[arrayindex++]]);182}183plane = planes[polygonPlaneIndexes[polygonindex]];184shared = shareds[polygonSharedIndexes[polygonindex]];185polygon = new CSG.Polygon(polygonvertices, shared, plane);186polygons.push(polygon);187}188var csg = CSG.fromPolygons(polygons);189csg.isCanonicalized = true;190csg.isRetesselated = true;191return csg;192};193
194CSG.prototype = {195toPolygons: function() {196return this.polygons;197},198
199// Return a new CSG solid representing space in either this solid or in the200// solid `csg`. Neither this solid nor the solid `csg` are modified.201//202// A.union(B)203//204// +-------+ +-------+205// | | | |206// | A | | |207// | +--+----+ = | +----+208// +----+--+ | +----+ |209// | B | | |210// | | | |211// +-------+ +-------+212//213union: function(csg) {214var csgs;215if(csg instanceof Array) {216csgs = csg;217} else {218csgs = [csg];219}220var result = this;221for(var i = 0; i < csgs.length; i++) {222var islast = (i == (csgs.length - 1));223result = result.unionSub(csgs[i], islast, islast);224}225return result;226},227
228unionSub: function(csg, retesselate, canonicalize) {229if(!this.mayOverlap(csg)) {230return this.unionForNonIntersecting(csg);231} else {232var a = new CSG.Tree(this.polygons);233var b = new CSG.Tree(csg.polygons);234a.clipTo(b, false);235
236// b.clipTo(a, true); // ERROR: this doesn't work237b.clipTo(a);238b.invert();239b.clipTo(a);240b.invert();241
242var newpolygons = a.allPolygons().concat(b.allPolygons());243var result = CSG.fromPolygons(newpolygons);244result.properties = this.properties._merge(csg.properties);245if(retesselate) result = result.reTesselated();246if(canonicalize) result = result.canonicalized();247return result;248}249},250
251// Like union, but when we know that the two solids are not intersecting252// Do not use if you are not completely sure that the solids do not intersect!253unionForNonIntersecting: function(csg) {254var newpolygons = this.polygons.concat(csg.polygons);255var result = CSG.fromPolygons(newpolygons);256result.properties = this.properties._merge(csg.properties);257result.isCanonicalized = this.isCanonicalized && csg.isCanonicalized;258result.isRetesselated = this.isRetesselated && csg.isRetesselated;259return result;260},261
262// Return a new CSG solid representing space in this solid but not in the263// solid `csg`. Neither this solid nor the solid `csg` are modified.264//265// A.subtract(B)266//267// +-------+ +-------+268// | | | |269// | A | | |270// | +--+----+ = | +--+271// +----+--+ | +----+272// | B |273// | |274// +-------+275//276subtract: function(csg) {277var csgs;278if(csg instanceof Array) {279csgs = csg;280} else {281csgs = [csg];282}283var result = this;284for(var i = 0; i < csgs.length; i++) {285var islast = (i == (csgs.length - 1));286result = result.subtractSub(csgs[i], islast, islast);287}288return result;289},290
291subtractSub: function(csg, retesselate, canonicalize) {292var a = new CSG.Tree(this.polygons);293var b = new CSG.Tree(csg.polygons);294a.invert();295a.clipTo(b);296b.clipTo(a, true);297a.addPolygons(b.allPolygons());298a.invert();299var result = CSG.fromPolygons(a.allPolygons());300result.properties = this.properties._merge(csg.properties);301if(retesselate) result = result.reTesselated();302if(canonicalize) result = result.canonicalized();303return result;304},305
306// Return a new CSG solid representing space both this solid and in the307// solid `csg`. Neither this solid nor the solid `csg` are modified.308//309// A.intersect(B)310//311// +-------+312// | |313// | A |314// | +--+----+ = +--+315// +----+--+ | +--+316// | B |317// | |318// +-------+319//320intersect: function(csg) {321var csgs;322if(csg instanceof Array) {323csgs = csg;324} else {325csgs = [csg];326}327var result = this;328for(var i = 0; i < csgs.length; i++) {329var islast = (i == (csgs.length - 1));330result = result.intersectSub(csgs[i], islast, islast);331}332return result;333},334
335intersectSub: function(csg, retesselate, canonicalize) {336var a = new CSG.Tree(this.polygons);337var b = new CSG.Tree(csg.polygons);338a.invert();339b.clipTo(a);340b.invert();341a.clipTo(b);342b.clipTo(a);343a.addPolygons(b.allPolygons());344a.invert();345var result = CSG.fromPolygons(a.allPolygons());346result.properties = this.properties._merge(csg.properties);347if(retesselate) result = result.reTesselated();348if(canonicalize) result = result.canonicalized();349return result;350},351
352// Return a new CSG solid with solid and empty space switched. This solid is353// not modified.354inverse: function() {355var flippedpolygons = this.polygons.map(function(p) {356return p.flipped();357});358return CSG.fromPolygons(flippedpolygons);359// TODO: flip properties360},361
362// Affine transformation of CSG object. Returns a new CSG object363transform1: function(matrix4x4) {364var newpolygons = this.polygons.map(function(p) {365return p.transform(matrix4x4);366});367var result = CSG.fromPolygons(newpolygons);368result.properties = this.properties._transform(matrix4x4);369result.isRetesselated = this.isRetesselated;370return result;371},372
373transform: function(matrix4x4) {374var ismirror = matrix4x4.isMirroring();375var transformedvertices = {};376var transformedplanes = {};377var newpolygons = this.polygons.map(function(p) {378var newplane;379var plane = p.plane;380var planetag = plane.getTag();381if(planetag in transformedplanes) {382newplane = transformedplanes[planetag];383} else {384newplane = plane.transform(matrix4x4);385transformedplanes[planetag] = newplane;386}387var newvertices = p.vertices.map(function(v) {388var newvertex;389var vertextag = v.getTag();390if(vertextag in transformedvertices) {391newvertex = transformedvertices[vertextag];392} else {393newvertex = v.transform(matrix4x4);394transformedvertices[vertextag] = newvertex;395}396return newvertex;397});398if(ismirror) newvertices.reverse();399return new CSG.Polygon(newvertices, p.shared, newplane);400});401var result = CSG.fromPolygons(newpolygons);402result.properties = this.properties._transform(matrix4x4);403result.isRetesselated = this.isRetesselated;404result.isCanonicalized = this.isCanonicalized;405return result;406},407
408toStlString: function() {409var result = "solid csg.js\n";410this.polygons.map(function(p) {411result += p.toStlString();412});413result += "endsolid csg.js\n";414return result;415},416
417toAMFString: function(m) {418var result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<amf"+(m&&m.unit?" unit=\"+m.unit\"":"")+">\n";419for(var k in m) {420result += "<metadata type=\""+k+"\">"+m[k]+"</metadata>\n";421}422result += "<object id=\"0\">\n<mesh>\n<vertices>\n";423
424this.polygons.map(function(p) { // first we dump all vertices of all polygons425for(var i=0; i<p.vertices.length; i++) {426result += p.vertices[i].toAMFString();427}428});429result += "</vertices>\n";430
431var n = 0;432this.polygons.map(function(p) { // then we dump all polygons433result += "<volume>\n";434if(p.vertices.length<3)435return;436var r = 1, g = 0.4, b = 1, a = 1, colorSet = false;437if(p.shared && p.shared.color) {438r = p.shared.color[0];439g = p.shared.color[1];440b = p.shared.color[2];441a = p.shared.color[3];442colorSet = true;443} else if(p.color) {444r = p.color[0];445g = p.color[1];446b = p.color[2];447if(p.color.length()>3) a = p.color[3];448colorSet = true;449}450
451result += "<color><r>"+r+"</r><g>"+g+"</g><b>"+b+"</b>"+(a!==undefined?"<a>"+a+"</a>":"")+"</color>";452
453for(var i=0; i<p.vertices.length-2; i++) { // making sure they are all triangles (triangular polygons)454result += "<triangle>";455result += "<v1>" + (n) + "</v1>";456result += "<v2>" + (n+i+1) + "</v2>";457result += "<v3>" + (n+i+2) + "</v3>";458result += "</triangle>\n";459}460n += p.vertices.length;461result += "</volume>\n";462});463result += "</mesh>\n</object>\n";464result += "</amf>\n";465return result;466},467
468toX3D: function() {469// materialPolygonLists470// key: a color string (e.g. "0 1 1" for yellow)471// value: an array of strings specifying polygons of this color472// (as space-separated indices into vertexCoords)473var materialPolygonLists = {},474// list of coordinates (as "x y z" strings)475vertexCoords = [],476// map to look up the index in vertexCoords of a given vertex477vertexTagToCoordIndexMap = {};478
479this.polygons.map(function(p) {480var red = 0,481green = 0,482blue = 1; // default color is blue483if(p.shared && p.shared.color) {484red = p.shared.color[0];485green = p.shared.color[1];486blue = p.shared.color[2];487}488
489var polygonVertexIndices = [],490numvertices = p.vertices.length,491vertex;492for(var i = 0; i < numvertices; i++) {493vertex = p.vertices[i];494if(!(vertex.getTag() in vertexTagToCoordIndexMap)) {495vertexCoords.push(vertex.pos._x.toString() + " " +496vertex.pos._y.toString() + " " +497vertex.pos._z.toString()498);499vertexTagToCoordIndexMap[vertex.getTag()] = vertexCoords.length - 1;500}501polygonVertexIndices.push(vertexTagToCoordIndexMap[vertex.getTag()]);502}503
504var polygonString = polygonVertexIndices.join(" ");505
506var colorString = red.toString() + " " + green.toString() + " " + blue.toString();507if(!(colorString in materialPolygonLists)) {508materialPolygonLists[colorString] = [];509}510// add this polygonString to the list of colorString-colored polygons511materialPolygonLists[colorString].push(polygonString);512});513
514
515// create output document516var docType = document.implementation.createDocumentType("X3D",517'ISO//Web3D//DTD X3D 3.1//EN" "http://www.web3d.org/specifications/x3d-3.1.dtd', null);518var exportDoc = document.implementation.createDocument(null, "X3D", docType);519exportDoc.insertBefore(520exportDoc.createProcessingInstruction('xml', 'version="1.0" encoding="UTF-8"'),521exportDoc.doctype);522
523var exportRoot = exportDoc.getElementsByTagName("X3D")[0];524exportRoot.setAttribute("profile", "Interchange");525exportRoot.setAttribute("version", "3.1");526exportRoot.setAttribute("xsd:noNamespaceSchemaLocation","http://www.web3d.org/specifications/x3d-3.1.xsd");527exportRoot.setAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema-instance");528
529var exportScene = exportDoc.createElement("Scene");530exportRoot.appendChild(exportScene);531
532/*533For each color, create a shape made of an appropriately colored
534material which contains all polygons that are this color.
535
536The first shape will contain the definition of all vertices,
537(<Coordinate DEF="coords_mesh"/>), which will be referenced by
538subsequent shapes.
539*/
540var coordsMeshDefined = false;541for(var colorString in materialPolygonLists) {542var polygonList = materialPolygonLists[colorString];543var shape = exportDoc.createElement("Shape");544exportScene.appendChild(shape);545
546var appearance = exportDoc.createElement("Appearance");547shape.appendChild(appearance);548
549var material = exportDoc.createElement("Material");550appearance.appendChild(material);551material.setAttribute("diffuseColor", colorString);552material.setAttribute("ambientIntensity", "1.0");553
554var ifs = exportDoc.createElement("IndexedFaceSet");555shape.appendChild(ifs);556ifs.setAttribute("solid", "true");557ifs.setAttribute("coordIndex", polygonList.join(" -1 ") + " -1");558
559var coordinate = exportDoc.createElement("Coordinate");560ifs.appendChild(coordinate);561if(coordsMeshDefined) {562coordinate.setAttribute("USE", "coords_mesh");563} else {564coordinate.setAttribute("DEF", "coords_mesh");565coordinate.setAttribute("point", vertexCoords.join(" "));566coordsMeshDefined = true;567}568}569
570var x3dstring = (new XMLSerializer()).serializeToString(exportDoc);571return new Blob([x3dstring], {572type: "model/x3d+xml"573});574},575
576// see http://en.wikipedia.org/wiki/STL_%28file_format%29#Binary_STL577toStlBinary: function(p) {578// first check if the host is little-endian:579var buffer = new ArrayBuffer(4);580var int32buffer = new Int32Array(buffer, 0, 1);581var int8buffer = new Int8Array(buffer, 0, 4);582int32buffer[0] = 0x11223344;583if(int8buffer[0] != 0x44) {584throw new Error("Binary STL output is currently only supported on little-endian (Intel) processors");585}586
587var numtriangles = 0;588this.polygons.map(function(p) {589var numvertices = p.vertices.length;590var thisnumtriangles = (numvertices >= 3) ? numvertices - 2 : 0;591numtriangles += thisnumtriangles;592});593var headerarray = new Uint8Array(80);594for(var i = 0; i < 80; i++) {595headerarray[i] = 65;596}597var ar1 = new Uint32Array(1);598ar1[0] = numtriangles;599// write the triangles to allTrianglesBuffer:600var allTrianglesBuffer = new ArrayBuffer(50 * numtriangles);601var allTrianglesBufferAsInt8 = new Int8Array(allTrianglesBuffer);602// a tricky problem is that a Float32Array must be aligned at 4-byte boundaries (at least in certain browsers)603// while each triangle takes 50 bytes. Therefore we write each triangle to a temporary buffer, and copy that604// into allTrianglesBuffer:605var triangleBuffer = new ArrayBuffer(50);606var triangleBufferAsInt8 = new Int8Array(triangleBuffer);607// each triangle consists of 12 floats:608var triangleFloat32array = new Float32Array(triangleBuffer, 0, 12);609// and one uint16:610var triangleUint16array = new Uint16Array(triangleBuffer, 48, 1);611var byteoffset = 0;612this.polygons.map(function(p) {613var numvertices = p.vertices.length;614for(var i = 0; i < numvertices - 2; i++) {615var normal = p.plane.normal;616triangleFloat32array[0] = normal._x;617triangleFloat32array[1] = normal._y;618triangleFloat32array[2] = normal._z;619var arindex = 3;620for(var v = 0; v < 3; v++) {621var vv = v + ((v > 0) ? i : 0);622var vertexpos = p.vertices[vv].pos;623triangleFloat32array[arindex++] = vertexpos._x;624triangleFloat32array[arindex++] = vertexpos._y;625triangleFloat32array[arindex++] = vertexpos._z;626}627triangleUint16array[0] = 0;628// copy the triangle into allTrianglesBuffer:629allTrianglesBufferAsInt8.set(triangleBufferAsInt8, byteoffset);630byteoffset += 50;631}632});633if(p&&p.webBlob) { // -- want a blob direct634return new Blob([headerarray.buffer, ar1.buffer, allTrianglesBuffer], {635type: "application/sla"636});637} else {638// we differentiate, as binary string blobbing gives bad blob in web, we need binary string for CLI639// perhaps there is a way to make it working (see openjscad for stlb)\640//641// concat 3 buffers together -- don't make blob so early, we want data (non-blob) for nodejs too642// must be string data direct to write643var stl = new Uint8Array(headerarray.buffer.byteLength + ar1.buffer.byteLength + allTrianglesBuffer.byteLength);644var j = 0;645for(var i=0; i<headerarray.buffer.byteLength; i++) { stl.buffer[j++] = headerarray.buffer[i]; }646for(var i=0; i<ar1.buffer.byteLength; i++) { stl.buffer[j++] = ar1.buffer[i]; }647for(var i=0; i<allTrianglesBuffer.byteLength; i++) { stl.buffer[j++] = allTrianglesBuffer[i]; }648return String.fromCharCode.apply(null, new Uint8Array(stl.buffer));649
650}651},652
653toString: function() {654var result = "CSG solid:\n";655this.polygons.map(function(p) {656result += p.toString();657});658return result;659},660
661center: function(c) {662if(!c.length) c = [c,c,c];663var b = this.getBounds();664return this.translate([665c[0]?-(b[1].x-b[0].x)/2-b[0].x:0,666c[1]?-(b[1].y-b[0].y)/2-b[0].y:0,667c[2]?-(b[1].z-b[0].z)/2-b[0].z:0]);668},669
670// Expand the solid671// resolution: number of points per 360 degree for the rounded corners672expand: function(radius, resolution) {673var result = this.expandedShell(radius, resolution, true);674result = result.reTesselated();675result.properties = this.properties; // keep original properties676return result;677},678
679// Contract the solid680// resolution: number of points per 360 degree for the rounded corners681contract: function(radius, resolution) {682var expandedshell = this.expandedShell(radius, resolution, false);683var result = this.subtract(expandedshell);684result = result.reTesselated();685result.properties = this.properties; // keep original properties686return result;687},688
689// Create the expanded shell of the solid:690// All faces are extruded to get a thickness of 2*radius691// Cylinders are constructed around every side692// Spheres are placed on every vertex693// unionWithThis: if true, the resulting solid will be united with 'this' solid;694// the result is a true expansion of the solid695// If false, returns only the shell696expandedShell: function(radius, resolution, unionWithThis) {697var csg = this.reTesselated();698var result;699if(unionWithThis) {700result = csg;701} else {702result = new CSG();703}704
705// first extrude all polygons:706csg.polygons.map(function(polygon) {707var extrudevector = polygon.plane.normal.unit().times(2 * radius);708var translatedpolygon = polygon.translate(extrudevector.times(-0.5));709var extrudedface = translatedpolygon.extrude(extrudevector);710result = result.unionSub(extrudedface, false, false);711});712
713// Make a list of all unique vertex pairs (i.e. all sides of the solid)714// For each vertex pair we collect the following:715// v1: first coordinate716// v2: second coordinate717// planenormals: array of normal vectors of all planes touching this side718var vertexpairs = {}; // map of 'vertex pair tag' to {v1, v2, planenormals}719csg.polygons.map(function(polygon) {720var numvertices = polygon.vertices.length;721var prevvertex = polygon.vertices[numvertices - 1];722var prevvertextag = prevvertex.getTag();723for(var i = 0; i < numvertices; i++) {724var vertex = polygon.vertices[i];725var vertextag = vertex.getTag();726var vertextagpair;727if(vertextag < prevvertextag) {728vertextagpair = vertextag + "-" + prevvertextag;729} else {730vertextagpair = prevvertextag + "-" + vertextag;731}732var obj;733if(vertextagpair in vertexpairs) {734obj = vertexpairs[vertextagpair];735} else {736obj = {737v1: prevvertex,738v2: vertex,739planenormals: []740};741vertexpairs[vertextagpair] = obj;742}743obj.planenormals.push(polygon.plane.normal);744
745prevvertextag = vertextag;746prevvertex = vertex;747}748});749
750// now construct a cylinder on every side751// The cylinder is always an approximation of a true cylinder: it will have <resolution> polygons752// around the sides. We will make sure though that the cylinder will have an edge at every753// face that touches this side. This ensures that we will get a smooth fill even754// if two edges are at, say, 10 degrees and the resolution is low.755// Note: the result is not retesselated yet but it really should be!756for(var vertextagpair in vertexpairs) {757var vertexpair = vertexpairs[vertextagpair],758startpoint = vertexpair.v1.pos,759endpoint = vertexpair.v2.pos,760// our x,y and z vectors:761zbase = endpoint.minus(startpoint).unit(),762xbase = vertexpair.planenormals[0].unit(),763ybase = xbase.cross(zbase),764
765// make a list of angles that the cylinder should traverse:766angles = [];767
768// first of all equally spaced around the cylinder:769for(var i = 0; i < resolution; i++) {770angles.push(i * Math.PI * 2 / resolution);771}772
773// and also at every normal of all touching planes:774for(var i = 0, iMax = vertexpair.planenormals.length; i < iMax; i++) {775var planenormal = vertexpair.planenormals[i],776si = ybase.dot(planenormal),777co = xbase.dot(planenormal),778angle = Math.atan2(si, co);779
780if(angle < 0) angle += Math.PI * 2;781angles.push(angle);782angle = Math.atan2(-si, -co);783if(angle < 0) angle += Math.PI * 2;784angles.push(angle);785}786
787// this will result in some duplicate angles but we will get rid of those later.788// Sort:789angles = angles.sort(fnNumberSort);790
791// Now construct the cylinder by traversing all angles:792var numangles = angles.length,793prevp1, prevp2,794startfacevertices = [],795endfacevertices = [],796polygons = [];797for(var i = -1; i < numangles; i++) {798var angle = angles[(i < 0) ? (i + numangles) : i],799si = Math.sin(angle),800co = Math.cos(angle),801p = xbase.times(co * radius).plus(ybase.times(si * radius)),802p1 = startpoint.plus(p),803p2 = endpoint.plus(p),804skip = false;805if(i >= 0) {806if(p1.distanceTo(prevp1) < 1e-5) {807skip = true;808}809}810if(!skip) {811if(i >= 0) {812startfacevertices.push(new CSG.Vertex(p1));813endfacevertices.push(new CSG.Vertex(p2));814var polygonvertices = [815new CSG.Vertex(prevp2),816new CSG.Vertex(p2),817new CSG.Vertex(p1),818new CSG.Vertex(prevp1)];819var polygon = new CSG.Polygon(polygonvertices);820polygons.push(polygon);821}822prevp1 = p1;823prevp2 = p2;824}825}826endfacevertices.reverse();827polygons.push(new CSG.Polygon(startfacevertices));828polygons.push(new CSG.Polygon(endfacevertices));829var cylinder = CSG.fromPolygons(polygons);830result = result.unionSub(cylinder, false, false);831}832
833// make a list of all unique vertices834// For each vertex we also collect the list of normals of the planes touching the vertices835var vertexmap = {};836csg.polygons.map(function(polygon) {837polygon.vertices.map(function(vertex) {838var vertextag = vertex.getTag();839var obj;840if(vertextag in vertexmap) {841obj = vertexmap[vertextag];842} else {843obj = {844pos: vertex.pos,845normals: []846};847vertexmap[vertextag] = obj;848}849obj.normals.push(polygon.plane.normal);850});851});852
853// and build spheres at each vertex854// We will try to set the x and z axis to the normals of 2 planes855// This will ensure that our sphere tesselation somewhat matches 2 planes856for(var vertextag in vertexmap) {857var vertexobj = vertexmap[vertextag];858// use the first normal to be the x axis of our sphere:859var xaxis = vertexobj.normals[0].unit();860// and find a suitable z axis. We will use the normal which is most perpendicular to the x axis:861var bestzaxis = null;862var bestzaxisorthogonality = 0;863for(var i = 1; i < vertexobj.normals.length; i++) {864var normal = vertexobj.normals[i].unit();865var cross = xaxis.cross(normal);866var crosslength = cross.length();867if(crosslength > 0.05) {868if(crosslength > bestzaxisorthogonality) {869bestzaxisorthogonality = crosslength;870bestzaxis = normal;871}872}873}874if(!bestzaxis) {875bestzaxis = xaxis.randomNonParallelVector();876}877var yaxis = xaxis.cross(bestzaxis).unit();878var zaxis = yaxis.cross(xaxis);879var sphere = CSG.sphere({880center: vertexobj.pos,881radius: radius,882resolution: resolution,883axes: [xaxis, yaxis, zaxis]884});885result = result.unionSub(sphere, false, false);886}887
888return result;889},890
891canonicalized: function() {892if(this.isCanonicalized) {893return this;894} else {895var factory = new CSG.fuzzyCSGFactory();896var result = factory.getCSG(this);897result.isCanonicalized = true;898result.isRetesselated = this.isRetesselated;899result.properties = this.properties; // keep original properties900return result;901}902},903
904reTesselated: function() {905if(this.isRetesselated) {906return this;907} else {908var csg = this.canonicalized();909var polygonsPerPlane = {};910csg.polygons.map(function(polygon) {911var planetag = polygon.plane.getTag();912var sharedtag = polygon.shared.getTag();913planetag += "/" + sharedtag;914if(!(planetag in polygonsPerPlane)) {915polygonsPerPlane[planetag] = [];916}917polygonsPerPlane[planetag].push(polygon);918});919var destpolygons = [];920for(var planetag in polygonsPerPlane) {921var sourcepolygons = polygonsPerPlane[planetag];922if(sourcepolygons.length < 2) {923destpolygons = destpolygons.concat(sourcepolygons);924} else {925var retesselayedpolygons = [];926CSG.reTesselateCoplanarPolygons(sourcepolygons, retesselayedpolygons);927destpolygons = destpolygons.concat(retesselayedpolygons);928}929}930var result = CSG.fromPolygons(destpolygons);931result.isRetesselated = true;932result = result.canonicalized();933// result.isCanonicalized = true;934result.properties = this.properties; // keep original properties935return result;936}937},938
939// returns an array of two CSG.Vector3Ds (minimum coordinates and maximum coordinates)940getBounds: function() {941if(!this.cachedBoundingBox) {942var minpoint = new CSG.Vector3D(0, 0, 0);943var maxpoint = new CSG.Vector3D(0, 0, 0);944var polygons = this.polygons;945var numpolygons = polygons.length;946for(var i = 0; i < numpolygons; i++) {947var polygon = polygons[i];948var bounds = polygon.boundingBox();949if(i === 0) {950minpoint = bounds[0];951maxpoint = bounds[1];952} else {953minpoint = minpoint.min(bounds[0]);954maxpoint = maxpoint.max(bounds[1]);955}956}957this.cachedBoundingBox = [minpoint, maxpoint];958}959return this.cachedBoundingBox;960},961
962// returns true if there is a possibility that the two solids overlap963// returns false if we can be sure that they do not overlap964mayOverlap: function(csg) {965if((this.polygons.length === 0) || (csg.polygons.length === 0)) {966return false;967} else {968var mybounds = this.getBounds();969var otherbounds = csg.getBounds();970// [0].x/y971// +-----+972// | |973// | |974// +-----+975// [1].x/y976//return false;977//echo(mybounds,"=",otherbounds);978if(mybounds[1].x < otherbounds[0].x) return false;979if(mybounds[0].x > otherbounds[1].x) return false;980if(mybounds[1].y < otherbounds[0].y) return false;981if(mybounds[0].y > otherbounds[1].y) return false;982if(mybounds[1].z < otherbounds[0].z) return false;983if(mybounds[0].z > otherbounds[1].z) return false;984return true;985}986},987
988// Cut the solid by a plane. Returns the solid on the back side of the plane989cutByPlane: function(plane) {990if(this.polygons.length === 0) {991return new CSG();992}993// Ideally we would like to do an intersection with a polygon of inifinite size994// but this is not supported by our implementation. As a workaround, we will create995// a cube, with one face on the plane, and a size larger enough so that the entire996// solid fits in the cube.997// find the max distance of any vertex to the center of the plane:998var planecenter = plane.normal.times(plane.w);999var maxdistance = 0;1000this.polygons.map(function(polygon) {1001polygon.vertices.map(function(vertex) {1002var distance = vertex.pos.distanceToSquared(planecenter);1003if(distance > maxdistance) maxdistance = distance;1004});1005});1006maxdistance = Math.sqrt(maxdistance);1007maxdistance *= 1.01; // make sure it's really larger1008// Now build a polygon on the plane, at any point farther than maxdistance from the plane center:1009var vertices = [];1010var orthobasis = new CSG.OrthoNormalBasis(plane);1011vertices.push(new CSG.Vertex(orthobasis.to3D(new CSG.Vector2D(maxdistance, -maxdistance))));1012vertices.push(new CSG.Vertex(orthobasis.to3D(new CSG.Vector2D(-maxdistance, -maxdistance))));1013vertices.push(new CSG.Vertex(orthobasis.to3D(new CSG.Vector2D(-maxdistance, maxdistance))));1014vertices.push(new CSG.Vertex(orthobasis.to3D(new CSG.Vector2D(maxdistance, maxdistance))));1015var polygon = new CSG.Polygon(vertices, null, plane.flipped());1016
1017// and extrude the polygon into a cube, backwards of the plane:1018var cube = polygon.extrude(plane.normal.times(-maxdistance));1019
1020// Now we can do the intersection:1021var result = this.intersect(cube);1022result.properties = this.properties; // keep original properties1023return result;1024},1025
1026// Connect a solid to another solid, such that two CSG.Connectors become connected1027// myConnector: a CSG.Connector of this solid1028// otherConnector: a CSG.Connector to which myConnector should be connected1029// mirror: false: the 'axis' vectors of the connectors should point in the same direction1030// true: the 'axis' vectors of the connectors should point in opposite direction1031// normalrotation: degrees of rotation between the 'normal' vectors of the two1032// connectors1033connectTo: function(myConnector, otherConnector, mirror, normalrotation) {1034var matrix = myConnector.getTransformationTo(otherConnector, mirror, normalrotation);1035return this.transform(matrix);1036},1037
1038// set the .shared property of all polygons1039// Returns a new CSG solid, the original is unmodified!1040setShared: function(shared) {1041var polygons = this.polygons.map(function(p) {1042return new CSG.Polygon(p.vertices, shared, p.plane);1043});1044var result = CSG.fromPolygons(polygons);1045result.properties = this.properties; // keep original properties1046result.isRetesselated = this.isRetesselated;1047result.isCanonicalized = this.isCanonicalized;1048return result;1049},1050
1051/**1052* @param {Array} color [red, green, blue] color values are float numbers 0..1
1053* @return {CSG} new CSG instance
1054*/
1055setColor: function(red, green, blue, alpha) { //for backward compatibility1056var color = red instanceof Array ? red : [red||0, green||0, blue||0, isNaN(alpha) ? 1. : alpha];1057var newshared = new CSG.Polygon.Shared(color);1058return this.setShared(newshared);1059},1060
1061toCompactBinary: function() {1062var csg = this.canonicalized(),1063numpolygons = csg.polygons.length,1064numpolygonvertices = 0,1065numvertices = 0,1066vertexmap = {},1067vertices = [],1068numplanes = 0,1069planemap = {},1070polygonindex = 0,1071planes = [],1072shareds = [],1073sharedmap = {},1074numshared = 0;1075// for (var i = 0, iMax = csg.polygons.length; i < iMax; i++) {1076// var p = csg.polygons[i];1077// for (var j = 0, jMax = p.length; j < jMax; j++) {1078// ++numpolygonvertices;1079// var vertextag = p[j].getTag();1080// if(!(vertextag in vertexmap)) {1081// vertexmap[vertextag] = numvertices++;1082// vertices.push(p[j]);1083// }1084// }1085csg.polygons.map(function(p) {1086p.vertices.map(function(v) {1087++numpolygonvertices;1088var vertextag = v.getTag();1089if(!(vertextag in vertexmap)) {1090vertexmap[vertextag] = numvertices++;1091vertices.push(v);1092}1093});1094
1095var planetag = p.plane.getTag();1096if(!(planetag in planemap)) {1097planemap[planetag] = numplanes++;1098planes.push(p.plane);1099}1100var sharedtag = p.shared.getTag();1101if(!(sharedtag in sharedmap)) {1102sharedmap[sharedtag] = numshared++;1103shareds.push(p.shared);1104}1105});1106var numVerticesPerPolygon = new Uint32Array(numpolygons),1107polygonSharedIndexes = new Uint32Array(numpolygons),1108polygonVertices = new Uint32Array(numpolygonvertices),1109polygonPlaneIndexes = new Uint32Array(numpolygons),1110vertexData = new Float64Array(numvertices * 3),1111planeData = new Float64Array(numplanes * 4),1112polygonVerticesIndex = 0;1113for(var polygonindex = 0; polygonindex < numpolygons; ++polygonindex) {1114var p = csg.polygons[polygonindex];1115numVerticesPerPolygon[polygonindex] = p.vertices.length;1116p.vertices.map(function(v) {1117var vertextag = v.getTag();1118var vertexindex = vertexmap[vertextag];1119polygonVertices[polygonVerticesIndex++] = vertexindex;1120});1121var planetag = p.plane.getTag();1122var planeindex = planemap[planetag];1123polygonPlaneIndexes[polygonindex] = planeindex;1124var sharedtag = p.shared.getTag();1125var sharedindex = sharedmap[sharedtag];1126polygonSharedIndexes[polygonindex] = sharedindex;1127}1128var verticesArrayIndex = 0;1129vertices.map(function(v) {1130var pos = v.pos;1131vertexData[verticesArrayIndex++] = pos._x;1132vertexData[verticesArrayIndex++] = pos._y;1133vertexData[verticesArrayIndex++] = pos._z;1134});1135var planesArrayIndex = 0;1136planes.map(function(p) {1137var normal = p.normal;1138planeData[planesArrayIndex++] = normal._x;1139planeData[planesArrayIndex++] = normal._y;1140planeData[planesArrayIndex++] = normal._z;1141planeData[planesArrayIndex++] = p.w;1142});1143var result = {1144"class": "CSG",1145numPolygons: numpolygons,1146numVerticesPerPolygon: numVerticesPerPolygon,1147polygonPlaneIndexes: polygonPlaneIndexes,1148polygonSharedIndexes: polygonSharedIndexes,1149polygonVertices: polygonVertices,1150vertexData: vertexData,1151planeData: planeData,1152shared: shareds1153};1154return result;1155},1156
1157// For debugging1158// Creates a new solid with a tiny cube at every vertex of the source solid1159toPointCloud: function(cuberadius) {1160var csg = this.reTesselated();1161
1162var result = new CSG();1163
1164// make a list of all unique vertices1165// For each vertex we also collect the list of normals of the planes touching the vertices1166var vertexmap = {};1167csg.polygons.map(function(polygon) {1168polygon.vertices.map(function(vertex) {1169vertexmap[vertex.getTag()] = vertex.pos;1170});1171});1172
1173for(var vertextag in vertexmap) {1174var pos = vertexmap[vertextag];1175var cube = CSG.cube({1176center: pos,1177radius: cuberadius1178});1179result = result.unionSub(cube, false, false);1180}1181result = result.reTesselated();1182return result;1183},1184
1185// Get the transformation that transforms this CSG such that it is lying on the z=0 plane,1186// as flat as possible (i.e. the least z-height).1187// So that it is in an orientation suitable for CNC milling1188getTransformationToFlatLying: function() {1189if(this.polygons.length === 0) {1190return new CSG.Matrix4x4(); // unity1191} else {1192// get a list of unique planes in the CSG:1193var csg = this.canonicalized();1194var planemap = {};1195csg.polygons.map(function(polygon) {1196planemap[polygon.plane.getTag()] = polygon.plane;1197});1198// try each plane in the CSG and find the plane that, when we align it flat onto z=0,1199// gives the least height in z-direction.1200// If two planes give the same height, pick the plane that originally had a normal closest1201// to [0,0,-1].1202var xvector = new CSG.Vector3D(1, 0, 0);1203var yvector = new CSG.Vector3D(0, 1, 0);1204var zvector = new CSG.Vector3D(0, 0, 1);1205var z0connectorx = new CSG.Connector([0, 0, 0], [0, 0, -1], xvector);1206var z0connectory = new CSG.Connector([0, 0, 0], [0, 0, -1], yvector);1207var isfirst = true;1208var minheight = 0;1209var maxdotz = 0;1210var besttransformation;1211for(var planetag in planemap) {1212var plane = planemap[planetag];1213var pointonplane = plane.normal.times(plane.w);1214var transformation;1215// We need a normal vecrtor for the transformation1216// determine which is more perpendicular to the plane normal: x or y?1217// we will align this as much as possible to the x or y axis vector1218var xorthogonality = plane.normal.cross(xvector).length();1219var yorthogonality = plane.normal.cross(yvector).length();1220if(xorthogonality > yorthogonality) {1221// x is better:1222var planeconnector = new CSG.Connector(pointonplane, plane.normal, xvector);1223transformation = planeconnector.getTransformationTo(z0connectorx, false, 0);1224} else {1225// y is better:1226var planeconnector = new CSG.Connector(pointonplane, plane.normal, yvector);1227transformation = planeconnector.getTransformationTo(z0connectory, false, 0);1228}1229var transformedcsg = csg.transform(transformation);1230var dotz = -plane.normal.dot(zvector);1231var bounds = transformedcsg.getBounds();1232var zheight = bounds[1].z - bounds[0].z;1233var isbetter = isfirst;1234if(!isbetter) {1235if(zheight < minheight) {1236isbetter = true;1237} else if(zheight == minheight) {1238if(dotz > maxdotz) isbetter = true;1239}1240}1241if(isbetter) {1242// translate the transformation around the z-axis and onto the z plane:1243var translation = [1244-0.5 * (bounds[1].x + bounds[0].x),1245-0.5 * (bounds[1].y + bounds[0].y),1246-bounds[0].z];1247transformation = transformation.multiply(CSG.Matrix4x4.translation(translation));1248minheight = zheight;1249maxdotz = dotz;1250besttransformation = transformation;1251}1252isfirst = false;1253}1254return besttransformation;1255}1256},1257
1258lieFlat: function() {1259var transformation = this.getTransformationToFlatLying();1260return this.transform(transformation);1261},1262
1263// project the 3D CSG onto a plane1264// This returns a 2D CAG with the 'shadow' shape of the 3D solid when projected onto the1265// plane represented by the orthonormal basis1266projectToOrthoNormalBasis: function(orthobasis) {1267var cags = [];1268this.polygons.map(function(polygon) {1269var cag = polygon.projectToOrthoNormalBasis(orthobasis);1270if(cag.sides.length > 0) {1271cags.push(cag);1272}1273});1274var result = new CAG().union(cags);1275return result;1276},1277
1278sectionCut: function(orthobasis) {1279var plane1 = orthobasis.plane;1280var plane2 = orthobasis.plane.flipped();1281plane1 = new CSG.Plane(plane1.normal, plane1.w + 1e-4);1282plane2 = new CSG.Plane(plane2.normal, plane2.w + 1e-4);1283var cut3d = this.cutByPlane(plane1);1284cut3d = cut3d.cutByPlane(plane2);1285return cut3d.projectToOrthoNormalBasis(orthobasis);1286},1287
1288/*1289fixTJunctions:
1290
1291Suppose we have two polygons ACDB and EDGF:
1292
1293A-----B
1294| |
1295| E--F
1296| | |
1297C-----D--G
1298
1299Note that vertex E forms a T-junction on the side BD. In this case some STL slicers will complain
1300that the solid is not watertight. This is because the watertightness check is done by checking if
1301each side DE is matched by another side ED.
1302
1303This function will return a new solid with ACDB replaced by ACDEB
1304
1305Note that this can create polygons that are slightly non-convex (due to rounding errors). Therefore the result should
1306not be used for further CSG operations!
1307*/
1308fixTJunctions: function() {1309var csg = this.canonicalized();1310var sidemap = {};1311for(var polygonindex = 0; polygonindex < csg.polygons.length; polygonindex++) {1312var polygon = csg.polygons[polygonindex];1313var numvertices = polygon.vertices.length;1314if(numvertices >= 3) // should be true1315{1316var vertex = polygon.vertices[0];1317var vertextag = vertex.getTag();1318for(var vertexindex = 0; vertexindex < numvertices; vertexindex++) {1319var nextvertexindex = vertexindex + 1;1320if(nextvertexindex == numvertices) nextvertexindex = 0;1321var nextvertex = polygon.vertices[nextvertexindex];1322var nextvertextag = nextvertex.getTag();1323var sidetag = vertextag + "/" + nextvertextag;1324var reversesidetag = nextvertextag + "/" + vertextag;1325if(reversesidetag in sidemap) {1326// this side matches the same side in another polygon. Remove from sidemap:1327var ar = sidemap[reversesidetag];1328ar.splice(-1, 1);1329if(ar.length === 0) {1330delete sidemap[reversesidetag];1331}1332} else {1333var sideobj = {1334vertex0: vertex,1335vertex1: nextvertex,1336polygonindex: polygonindex1337};1338if(!(sidetag in sidemap)) {1339sidemap[sidetag] = [sideobj];1340} else {1341sidemap[sidetag].push(sideobj);1342}1343}1344vertex = nextvertex;1345vertextag = nextvertextag;1346}1347}1348}1349// now sidemap contains 'unmatched' sides1350// i.e. side AB in one polygon does not have a matching side BA in another polygon1351var vertextag2sidestart = {};1352var vertextag2sideend = {};1353var sidestocheck = {};1354var sidemapisempty = true;1355for(var sidetag in sidemap) {1356sidemapisempty = false;1357sidestocheck[sidetag] = true;1358sidemap[sidetag].map(function(sideobj) {1359var starttag = sideobj.vertex0.getTag();1360var endtag = sideobj.vertex1.getTag();1361if(starttag in vertextag2sidestart) {1362vertextag2sidestart[starttag].push(sidetag);1363} else {1364vertextag2sidestart[starttag] = [sidetag];1365}1366if(endtag in vertextag2sideend) {1367vertextag2sideend[endtag].push(sidetag);1368} else {1369vertextag2sideend[endtag] = [sidetag];1370}1371});1372}1373
1374if(!sidemapisempty) {1375// make a copy of the polygons array, since we are going to modify it:1376var polygons = csg.polygons.slice(0);1377
1378function addSide (vertex0, vertex1, polygonindex) {1379var starttag = vertex0.getTag();1380var endtag = vertex1.getTag();1381if(starttag == endtag) throw new Error("Assertion failed");1382var newsidetag = starttag + "/" + endtag;1383var reversesidetag = endtag + "/" + starttag;1384if(reversesidetag in sidemap) {1385// we have a matching reverse oriented side.1386// Instead of adding the new side, cancel out the reverse side:1387// console.log("addSide("+newsidetag+") has reverse side:");1388deleteSide(vertex1, vertex0, null);1389return null;1390}1391// console.log("addSide("+newsidetag+")");1392var newsideobj = {1393vertex0: vertex0,1394vertex1: vertex1,1395polygonindex: polygonindex1396};1397if(!(newsidetag in sidemap)) {1398sidemap[newsidetag] = [newsideobj];1399} else {1400sidemap[newsidetag].push(newsideobj);1401}1402if(starttag in vertextag2sidestart) {1403vertextag2sidestart[starttag].push(newsidetag);1404} else {1405vertextag2sidestart[starttag] = [newsidetag];1406}1407if(endtag in vertextag2sideend) {1408vertextag2sideend[endtag].push(newsidetag);1409} else {1410vertextag2sideend[endtag] = [newsidetag];1411}1412return newsidetag;1413}1414
1415function deleteSide (vertex0, vertex1, polygonindex) {1416var starttag = vertex0.getTag();1417var endtag = vertex1.getTag();1418var sidetag = starttag + "/" + endtag;1419// console.log("deleteSide("+sidetag+")");1420if(!(sidetag in sidemap)) throw new Error("Assertion failed");1421var idx = -1;1422var sideobjs = sidemap[sidetag];1423for(var i = 0; i < sideobjs.length; i++) {1424var sideobj = sideobjs[i];1425if(sideobj.vertex0 != vertex0) continue;1426if(sideobj.vertex1 != vertex1) continue;1427if(polygonindex !== null) {1428if(sideobj.polygonindex != polygonindex) continue;1429}1430idx = i;1431break;1432}1433if(idx < 0) throw new Error("Assertion failed");1434sideobjs.splice(idx, 1);1435if(sideobjs.length === 0) {1436delete sidemap[sidetag];1437}1438idx = vertextag2sidestart[starttag].indexOf(sidetag);1439if(idx < 0) throw new Error("Assertion failed");1440vertextag2sidestart[starttag].splice(idx, 1);1441if(vertextag2sidestart[starttag].length === 0) {1442delete vertextag2sidestart[starttag];1443}1444
1445idx = vertextag2sideend[endtag].indexOf(sidetag);1446if(idx < 0) throw new Error("Assertion failed");1447vertextag2sideend[endtag].splice(idx, 1);1448if(vertextag2sideend[endtag].length === 0) {1449delete vertextag2sideend[endtag];1450}1451}1452
1453
1454while(true) {1455var sidemapisempty = true;1456for(var sidetag in sidemap) {1457sidemapisempty = false;1458sidestocheck[sidetag] = true;1459}1460if(sidemapisempty) break;1461var donesomething = false;1462while(true) {1463var sidetagtocheck = null;1464for(var sidetag in sidestocheck) {1465sidetagtocheck = sidetag;1466break;1467}1468if(sidetagtocheck === null) break; // sidestocheck is empty, we're done!1469var donewithside = true;1470if(sidetagtocheck in sidemap) {1471var sideobjs = sidemap[sidetagtocheck];1472if(sideobjs.length === 0) throw new Error("Assertion failed");1473var sideobj = sideobjs[0];1474for(var directionindex = 0; directionindex < 2; directionindex++) {1475var startvertex = (directionindex === 0) ? sideobj.vertex0 : sideobj.vertex1;1476var endvertex = (directionindex === 0) ? sideobj.vertex1 : sideobj.vertex0;1477var startvertextag = startvertex.getTag();1478var endvertextag = endvertex.getTag();1479var matchingsides = [];1480if(directionindex === 0) {1481if(startvertextag in vertextag2sideend) {1482matchingsides = vertextag2sideend[startvertextag];1483}1484} else {1485if(startvertextag in vertextag2sidestart) {1486matchingsides = vertextag2sidestart[startvertextag];1487}1488}1489for(var matchingsideindex = 0; matchingsideindex < matchingsides.length; matchingsideindex++) {1490var matchingsidetag = matchingsides[matchingsideindex];1491var matchingside = sidemap[matchingsidetag][0];1492var matchingsidestartvertex = (directionindex === 0) ? matchingside.vertex0 : matchingside.vertex1;1493var matchingsideendvertex = (directionindex === 0) ? matchingside.vertex1 : matchingside.vertex0;1494var matchingsidestartvertextag = matchingsidestartvertex.getTag();1495var matchingsideendvertextag = matchingsideendvertex.getTag();1496if(matchingsideendvertextag != startvertextag) throw new Error("Assertion failed");1497if(matchingsidestartvertextag == endvertextag) {1498// matchingside cancels sidetagtocheck1499deleteSide(startvertex, endvertex, null);1500deleteSide(endvertex, startvertex, null);1501donewithside = false;1502directionindex = 2; // skip reverse direction check1503donesomething = true;1504break;1505} else {1506var startpos = startvertex.pos;1507var endpos = endvertex.pos;1508var checkpos = matchingsidestartvertex.pos;1509var direction = checkpos.minus(startpos);1510// Now we need to check if endpos is on the line startpos-checkpos:1511var t = endpos.minus(startpos).dot(direction) / direction.dot(direction);1512if((t > 0) && (t < 1)) {1513var closestpoint = startpos.plus(direction.times(t));1514var distancesquared = closestpoint.distanceToSquared(endpos);1515if(distancesquared < 1e-10) {1516// Yes it's a t-junction! We need to split matchingside in two:1517var polygonindex = matchingside.polygonindex;1518var polygon = polygons[polygonindex];1519// find the index of startvertextag in polygon:1520var insertionvertextag = matchingside.vertex1.getTag();1521var insertionvertextagindex = -1;1522for(var i = 0; i < polygon.vertices.length; i++) {1523if(polygon.vertices[i].getTag() == insertionvertextag) {1524insertionvertextagindex = i;1525break;1526}1527}1528if(insertionvertextagindex < 0) throw new Error("Assertion failed");1529// split the side by inserting the vertex:1530var newvertices = polygon.vertices.slice(0);1531newvertices.splice(insertionvertextagindex, 0, endvertex);1532var newpolygon = new CSG.Polygon(newvertices, polygon.shared /*polygon.plane*/ );1533polygons[polygonindex] = newpolygon;1534
1535// remove the original sides from our maps:1536// deleteSide(sideobj.vertex0, sideobj.vertex1, null);1537deleteSide(matchingside.vertex0, matchingside.vertex1, polygonindex);1538var newsidetag1 = addSide(matchingside.vertex0, endvertex, polygonindex);1539var newsidetag2 = addSide(endvertex, matchingside.vertex1, polygonindex);1540if(newsidetag1 !== null) sidestocheck[newsidetag1] = true;1541if(newsidetag2 !== null) sidestocheck[newsidetag2] = true;1542donewithside = false;1543directionindex = 2; // skip reverse direction check1544donesomething = true;1545break;1546} // if(distancesquared < 1e-10)1547} // if( (t > 0) && (t < 1) )1548} // if(endingstidestartvertextag == endvertextag)1549} // for matchingsideindex1550} // for directionindex1551} // if(sidetagtocheck in sidemap)1552if(donewithside) {1553delete sidestocheck[sidetag];1554}1555}1556if(!donesomething) break;1557}1558var newcsg = CSG.fromPolygons(polygons);1559newcsg.properties = csg.properties;1560newcsg.isCanonicalized = true;1561newcsg.isRetesselated = true;1562csg = newcsg;1563} // if(!sidemapisempty)1564var sidemapisempty = true;1565for(var sidetag in sidemap) {1566sidemapisempty = false;1567break;1568}1569if(!sidemapisempty) {1570throw new Error("!sidemapisempty");1571}1572return csg;1573}1574};1575
1576// Parse an option from the options object
1577// If the option is not present, return the default value
1578CSG.parseOption = function(options, optionname, defaultvalue) {1579var result = defaultvalue;1580if(options) {1581if(optionname in options) {1582result = options[optionname];1583}1584}1585return result;1586};1587
1588// Parse an option and force into a CSG.Vector3D. If a scalar is passed it is converted
1589// into a vector with equal x,y,z
1590CSG.parseOptionAs3DVector = function(options, optionname, defaultvalue) {1591var result = CSG.parseOption(options, optionname, defaultvalue);1592result = new CSG.Vector3D(result);1593return result;1594};1595
1596// Parse an option and force into a CSG.Vector2D. If a scalar is passed it is converted
1597// into a vector with equal x,y
1598CSG.parseOptionAs2DVector = function(options, optionname, defaultvalue) {1599var result = CSG.parseOption(options, optionname, defaultvalue);1600result = new CSG.Vector2D(result);1601return result;1602};1603
1604CSG.parseOptionAsFloat = function(options, optionname, defaultvalue) {1605var result = CSG.parseOption(options, optionname, defaultvalue);1606if(typeof(result) == "string") {1607result = Number(result);1608}1609if(isNaN(result) || typeof(result) != "number") {1610throw new Error("Parameter " + optionname + " should be a number");1611}1612return result;1613};1614
1615CSG.parseOptionAsInt = function(options, optionname, defaultvalue) {1616var result = CSG.parseOption(options, optionname, defaultvalue);1617result = Number(Math.floor(result));1618if (isNaN(result)) {1619throw new Error("Parameter " + optionname + " should be a number");1620}1621return result;1622};1623
1624CSG.parseOptionAsBool = function(options, optionname, defaultvalue) {1625var result = CSG.parseOption(options, optionname, defaultvalue);1626if(typeof(result) == "string") {1627if(result == "true") result = true;1628else if(result == "false") result = false;1629else if(result == 0) result = false;1630}1631result = !! result;1632return result;1633};1634
1635// Construct an axis-aligned solid cuboid.
1636// Parameters:
1637// center: center of cube (default [0,0,0])
1638// radius: radius of cube (default [1,1,1]), can be specified as scalar or as 3D vector
1639//
1640// Example code:
1641//
1642// var cube = CSG.cube({
1643// center: [0, 0, 0],
1644// radius: 1
1645// });
1646CSG.cube = function(options) {1647var c = CSG.parseOptionAs3DVector(options, "center", [0, 0, 0]);1648var r = CSG.parseOptionAs3DVector(options, "radius", [1, 1, 1]);1649var result = CSG.fromPolygons([1650[1651[0, 4, 6, 2],1652[-1, 0, 0]1653],1654[1655[1, 3, 7, 5],1656[+1, 0, 0]1657],1658[1659[0, 1, 5, 4],1660[0, -1, 0]1661],1662[1663[2, 6, 7, 3],1664[0, +1, 0]1665],1666[1667[0, 2, 3, 1],1668[0, 0, -1]1669],1670[1671[4, 5, 7, 6],1672[0, 0, +1]1673]1674].map(function(info) {1675//var normal = new CSG.Vector3D(info[1]);1676//var plane = new CSG.Plane(normal, 1);1677var vertices = info[0].map(function(i) {1678var pos = new CSG.Vector3D(1679c.x + r.x * (2 * !! (i & 1) - 1), c.y + r.y * (2 * !! (i & 2) - 1), c.z + r.z * (2 * !! (i & 4) - 1));1680return new CSG.Vertex(pos);1681});1682return new CSG.Polygon(vertices, null /* , plane */ );1683}));1684result.properties.cube = new CSG.Properties();1685result.properties.cube.center = new CSG.Vector3D(c);1686// add 6 connectors, at the centers of each face:1687result.properties.cube.facecenters = [1688new CSG.Connector(new CSG.Vector3D([r.x, 0, 0]).plus(c), [1, 0, 0], [0, 0, 1]),1689new CSG.Connector(new CSG.Vector3D([-r.x, 0, 0]).plus(c), [-1, 0, 0], [0, 0, 1]),1690new CSG.Connector(new CSG.Vector3D([0, r.y, 0]).plus(c), [0, 1, 0], [0, 0, 1]),1691new CSG.Connector(new CSG.Vector3D([0, -r.y, 0]).plus(c), [0, -1, 0], [0, 0, 1]),1692new CSG.Connector(new CSG.Vector3D([0, 0, r.z]).plus(c), [0, 0, 1], [1, 0, 0]),1693new CSG.Connector(new CSG.Vector3D([0, 0, -r.z]).plus(c), [0, 0, -1], [1, 0, 0])1694];1695return result;1696};1697
1698// Construct a solid sphere
1699//
1700// Parameters:
1701// center: center of sphere (default [0,0,0])
1702// radius: radius of sphere (default 1), must be a scalar
1703// resolution: determines the number of polygons per 360 degree revolution (default 12)
1704// axes: (optional) an array with 3 vectors for the x, y and z base vectors
1705//
1706// Example usage:
1707//
1708// var sphere = CSG.sphere({
1709// center: [0, 0, 0],
1710// radius: 2,
1711// resolution: 32,
1712// });
1713CSG.sphere = function(options) {1714options = options || {};1715var center = CSG.parseOptionAs3DVector(options, "center", [0, 0, 0]);1716var radius = CSG.parseOptionAsFloat(options, "radius", 1);1717var resolution = CSG.parseOptionAsInt(options, "resolution", CSG.defaultResolution3D);1718var xvector, yvector, zvector;1719if('axes' in options) {1720xvector = options.axes[0].unit().times(radius);1721yvector = options.axes[1].unit().times(radius);1722zvector = options.axes[2].unit().times(radius);1723} else {1724xvector = new CSG.Vector3D([1, 0, 0]).times(radius);1725yvector = new CSG.Vector3D([0, -1, 0]).times(radius);1726zvector = new CSG.Vector3D([0, 0, 1]).times(radius);1727}1728if(resolution < 4) resolution = 4;1729var qresolution = Math.round(resolution / 4);1730var prevcylinderpoint;1731var polygons = [];1732for(var slice1 = 0; slice1 <= resolution; slice1++) {1733var angle = Math.PI * 2.0 * slice1 / resolution;1734var cylinderpoint = xvector.times(Math.cos(angle)).plus(yvector.times(Math.sin(angle)));1735if(slice1 > 0) {1736// cylinder vertices:1737var vertices = [];1738var prevcospitch, prevsinpitch;1739for(var slice2 = 0; slice2 <= qresolution; slice2++) {1740var pitch = 0.5 * Math.PI * slice2 / qresolution;1741var cospitch = Math.cos(pitch);1742var sinpitch = Math.sin(pitch);1743if(slice2 > 0) {1744vertices = [];1745vertices.push(new CSG.Vertex(center.plus(prevcylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))));1746vertices.push(new CSG.Vertex(center.plus(cylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))));1747if(slice2 < qresolution) {1748vertices.push(new CSG.Vertex(center.plus(cylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))));1749}1750vertices.push(new CSG.Vertex(center.plus(prevcylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))));1751polygons.push(new CSG.Polygon(vertices));1752vertices = [];1753vertices.push(new CSG.Vertex(center.plus(prevcylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))));1754vertices.push(new CSG.Vertex(center.plus(cylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))));1755if(slice2 < qresolution) {1756vertices.push(new CSG.Vertex(center.plus(cylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))));1757}1758vertices.push(new CSG.Vertex(center.plus(prevcylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))));1759vertices.reverse();1760polygons.push(new CSG.Polygon(vertices));1761}1762prevcospitch = cospitch;1763prevsinpitch = sinpitch;1764}1765}1766prevcylinderpoint = cylinderpoint;1767}1768var result = CSG.fromPolygons(polygons);1769result.properties.sphere = new CSG.Properties();1770result.properties.sphere.center = new CSG.Vector3D(center);1771result.properties.sphere.facepoint = center.plus(xvector);1772return result;1773};1774
1775// Construct a solid cylinder.
1776//
1777// Parameters:
1778// start: start point of cylinder (default [0, -1, 0])
1779// end: end point of cylinder (default [0, 1, 0])
1780// radius: radius of cylinder (default 1), must be a scalar
1781// resolution: determines the number of polygons per 360 degree revolution (default 12)
1782//
1783// Example usage:
1784//
1785// var cylinder = CSG.cylinder({
1786// start: [0, -1, 0],
1787// end: [0, 1, 0],
1788// radius: 1,
1789// resolution: 16
1790// });
1791CSG.cylinder = function(options) {1792var s = CSG.parseOptionAs3DVector(options, "start", [0, -1, 0]);1793var e = CSG.parseOptionAs3DVector(options, "end", [0, 1, 0]);1794var r = CSG.parseOptionAsFloat(options, "radius", 1);1795var rEnd = CSG.parseOptionAsFloat(options, "radiusEnd", r);1796var rStart = CSG.parseOptionAsFloat(options, "radiusStart", r);1797
1798if((rEnd < 0) || (rStart < 0)) {1799throw new Error("Radius should be non-negative");1800}1801if((rEnd === 0) && (rStart === 0)) {1802throw new Error("Either radiusStart or radiusEnd should be positive");1803}1804
1805var slices = CSG.parseOptionAsFloat(options, "resolution", CSG.defaultResolution2D);1806var ray = e.minus(s);1807var axisZ = ray.unit(); //, isY = (Math.abs(axisZ.y) > 0.5);1808var axisX = axisZ.randomNonParallelVector().unit();1809
1810// var axisX = new CSG.Vector3D(isY, !isY, 0).cross(axisZ).unit();1811var axisY = axisX.cross(axisZ).unit();1812var start = new CSG.Vertex(s);1813var end = new CSG.Vertex(e);1814var polygons = [];1815
1816function point(stack, slice, radius) {1817var angle = slice * Math.PI * 2;1818var out = axisX.times(Math.cos(angle)).plus(axisY.times(Math.sin(angle)));1819var pos = s.plus(ray.times(stack)).plus(out.times(radius));1820return new CSG.Vertex(pos);1821}1822for(var i = 0; i < slices; i++) {1823var t0 = i / slices,1824t1 = (i + 1) / slices;1825if(rEnd == rStart) {1826polygons.push(new CSG.Polygon([start, point(0, t0, rEnd), point(0, t1, rEnd)]));1827polygons.push(new CSG.Polygon([point(0, t1, rEnd), point(0, t0, rEnd), point(1, t0, rEnd), point(1, t1, rEnd)]));1828polygons.push(new CSG.Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]));1829} else {1830if(rStart > 0) {1831polygons.push(new CSG.Polygon([start, point(0, t0, rStart), point(0, t1, rStart)]));1832polygons.push(new CSG.Polygon([point(0, t0, rStart), point(1, t0, rEnd), point(0, t1, rStart)]));1833}1834if(rEnd > 0) {1835polygons.push(new CSG.Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)]));1836polygons.push(new CSG.Polygon([point(1, t0, rEnd), point(1, t1, rEnd), point(0, t1, rStart)]));1837}1838}1839}1840var result = CSG.fromPolygons(polygons);1841result.properties.cylinder = new CSG.Properties();1842result.properties.cylinder.start = new CSG.Connector(s, axisZ.negated(), axisX);1843result.properties.cylinder.end = new CSG.Connector(e, axisZ, axisX);1844result.properties.cylinder.facepoint = s.plus(axisX.times(rStart));1845return result;1846};1847
1848// Like a cylinder, but with rounded ends instead of flat
1849//
1850// Parameters:
1851// start: start point of cylinder (default [0, -1, 0])
1852// end: end point of cylinder (default [0, 1, 0])
1853// radius: radius of cylinder (default 1), must be a scalar
1854// resolution: determines the number of polygons per 360 degree revolution (default 12)
1855// normal: a vector determining the starting angle for tesselation. Should be non-parallel to start.minus(end)
1856//
1857// Example usage:
1858//
1859// var cylinder = CSG.roundedCylinder({
1860// start: [0, -1, 0],
1861// end: [0, 1, 0],
1862// radius: 1,
1863// resolution: 16
1864// });
1865CSG.roundedCylinder = function(options) {1866var p1 = CSG.parseOptionAs3DVector(options, "start", [0, -1, 0]);1867var p2 = CSG.parseOptionAs3DVector(options, "end", [0, 1, 0]);1868var radius = CSG.parseOptionAsFloat(options, "radius", 1);1869var direction = p2.minus(p1);1870var defaultnormal;1871if(Math.abs(direction.x) > Math.abs(direction.y)) {1872defaultnormal = new CSG.Vector3D(0, 1, 0);1873} else {1874defaultnormal = new CSG.Vector3D(1, 0, 0);1875}1876var normal = CSG.parseOptionAs3DVector(options, "normal", defaultnormal);1877var resolution = CSG.parseOptionAsFloat(options, "resolution", CSG.defaultResolution3D);1878if(resolution < 4) resolution = 4;1879var polygons = [];1880var qresolution = Math.floor(0.25 * resolution);1881var length = direction.length();1882if(length < 1e-10) {1883return CSG.sphere({1884center: p1,1885radius: radius,1886resolution: resolution1887});1888}1889var zvector = direction.unit().times(radius);1890var xvector = zvector.cross(normal).unit().times(radius);1891var yvector = xvector.cross(zvector).unit().times(radius);1892var prevcylinderpoint;1893for(var slice1 = 0; slice1 <= resolution; slice1++) {1894var angle = Math.PI * 2.0 * slice1 / resolution;1895var cylinderpoint = xvector.times(Math.cos(angle)).plus(yvector.times(Math.sin(angle)));1896if(slice1 > 0) {1897// cylinder vertices:1898var vertices = [];1899vertices.push(new CSG.Vertex(p1.plus(cylinderpoint)));1900vertices.push(new CSG.Vertex(p1.plus(prevcylinderpoint)));1901vertices.push(new CSG.Vertex(p2.plus(prevcylinderpoint)));1902vertices.push(new CSG.Vertex(p2.plus(cylinderpoint)));1903polygons.push(new CSG.Polygon(vertices));1904var prevcospitch, prevsinpitch;1905for(var slice2 = 0; slice2 <= qresolution; slice2++) {1906var pitch = 0.5 * Math.PI * slice2 / qresolution;1907//var pitch = Math.asin(slice2/qresolution);1908var cospitch = Math.cos(pitch);1909var sinpitch = Math.sin(pitch);1910if(slice2 > 0) {1911vertices = [];1912vertices.push(new CSG.Vertex(p1.plus(prevcylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))));1913vertices.push(new CSG.Vertex(p1.plus(cylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch)))));1914if(slice2 < qresolution) {1915vertices.push(new CSG.Vertex(p1.plus(cylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))));1916}1917vertices.push(new CSG.Vertex(p1.plus(prevcylinderpoint.times(cospitch).minus(zvector.times(sinpitch)))));1918polygons.push(new CSG.Polygon(vertices));1919vertices = [];1920vertices.push(new CSG.Vertex(p2.plus(prevcylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))));1921vertices.push(new CSG.Vertex(p2.plus(cylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch)))));1922if(slice2 < qresolution) {1923vertices.push(new CSG.Vertex(p2.plus(cylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))));1924}1925vertices.push(new CSG.Vertex(p2.plus(prevcylinderpoint.times(cospitch).plus(zvector.times(sinpitch)))));1926vertices.reverse();1927polygons.push(new CSG.Polygon(vertices));1928}1929prevcospitch = cospitch;1930prevsinpitch = sinpitch;1931}1932}1933prevcylinderpoint = cylinderpoint;1934}1935var result = CSG.fromPolygons(polygons);1936var ray = zvector.unit();1937var axisX = xvector.unit();1938result.properties.roundedCylinder = new CSG.Properties();1939result.properties.roundedCylinder.start = new CSG.Connector(p1, ray.negated(), axisX);1940result.properties.roundedCylinder.end = new CSG.Connector(p2, ray, axisX);1941result.properties.roundedCylinder.facepoint = p1.plus(xvector);1942return result;1943};1944
1945// Construct an axis-aligned solid rounded cuboid.
1946// Parameters:
1947// center: center of cube (default [0,0,0])
1948// radius: radius of cube (default [1,1,1]), can be specified as scalar or as 3D vector
1949// roundradius: radius of rounded corners (default 0.2), must be a scalar
1950// resolution: determines the number of polygons per 360 degree revolution (default 8)
1951//
1952// Example code:
1953//
1954// var cube = CSG.roundedCube({
1955// center: [0, 0, 0],
1956// radius: 1,
1957// roundradius: 0.2,
1958// resolution: 8,
1959// });
1960CSG.roundedCube = function(options) {1961var center = CSG.parseOptionAs3DVector(options, "center", [0, 0, 0]);1962var cuberadius = CSG.parseOptionAs3DVector(options, "radius", [1, 1, 1]);1963var resolution = CSG.parseOptionAsFloat(options, "resolution", CSG.defaultResolution3D);1964if(resolution < 4) resolution = 4;1965var roundradius = CSG.parseOptionAsFloat(options, "roundradius", 0.2);1966var innercuberadius = cuberadius;1967innercuberadius = innercuberadius.minus(new CSG.Vector3D(roundradius));1968var result = CSG.cube({1969center: center,1970radius: [cuberadius.x, innercuberadius.y, innercuberadius.z]1971});1972result = result.unionSub(CSG.cube({1973center: center,1974radius: [innercuberadius.x, cuberadius.y, innercuberadius.z]1975}), false, false);1976result = result.unionSub(CSG.cube({1977center: center,1978radius: [innercuberadius.x, innercuberadius.y, cuberadius.z]1979}), false, false);1980for(var level = 0; level < 2; level++) {1981var z = innercuberadius.z;1982if(level == 1) z = -z;1983var p1 = new CSG.Vector3D(innercuberadius.x, innercuberadius.y, z).plus(center);1984var p2 = new CSG.Vector3D(innercuberadius.x, -innercuberadius.y, z).plus(center);1985var p3 = new CSG.Vector3D(-innercuberadius.x, -innercuberadius.y, z).plus(center);1986var p4 = new CSG.Vector3D(-innercuberadius.x, innercuberadius.y, z).plus(center);1987var sphere = CSG.sphere({1988center: p1,1989radius: roundradius,1990resolution: resolution1991});1992result = result.unionSub(sphere, false, false);1993sphere = CSG.sphere({1994center: p2,1995radius: roundradius,1996resolution: resolution1997});1998result = result.unionSub(sphere, false, false);1999sphere = CSG.sphere({2000center: p3,2001radius: roundradius,2002resolution: resolution2003});2004result = result.unionSub(sphere, false, false);2005sphere = CSG.sphere({2006center: p4,2007radius: roundradius,2008resolution: resolution2009});2010result = result.unionSub(sphere, false, true);2011var cylinder = CSG.cylinder({2012start: p1,2013end: p2,2014radius: roundradius,2015resolution: resolution2016});2017result = result.unionSub(cylinder, false, false);2018cylinder = CSG.cylinder({2019start: p2,2020end: p3,2021radius: roundradius,2022resolution: resolution2023});2024result = result.unionSub(cylinder, false, false);2025cylinder = CSG.cylinder({2026start: p3,2027end: p4,2028radius: roundradius,2029resolution: resolution2030});2031result = result.unionSub(cylinder, false, false);2032cylinder = CSG.cylinder({2033start: p4,2034end: p1,2035radius: roundradius,2036resolution: resolution2037});2038result = result.unionSub(cylinder, false, false);2039if(level === 0) {2040var d = new CSG.Vector3D(0, 0, -2 * z);2041cylinder = CSG.cylinder({2042start: p1,2043end: p1.plus(d),2044radius: roundradius,2045resolution: resolution2046});2047result = result.unionSub(cylinder);2048cylinder = CSG.cylinder({2049start: p2,2050end: p2.plus(d),2051radius: roundradius,2052resolution: resolution2053});2054result = result.unionSub(cylinder);2055cylinder = CSG.cylinder({2056start: p3,2057end: p3.plus(d),2058radius: roundradius,2059resolution: resolution2060});2061result = result.unionSub(cylinder);2062cylinder = CSG.cylinder({2063start: p4,2064end: p4.plus(d),2065radius: roundradius,2066resolution: resolution2067});2068result = result.unionSub(cylinder, false, true);2069}2070}2071result = result.reTesselated();2072result.properties.roundedCube = new CSG.Properties();2073result.properties.roundedCube.center = new CSG.Vertex(center);2074result.properties.roundedCube.facecenters = [2075new CSG.Connector(new CSG.Vector3D([cuberadius.x, 0, 0]).plus(center), [1, 0, 0], [0, 0, 1]),2076new CSG.Connector(new CSG.Vector3D([-cuberadius.x, 0, 0]).plus(center), [-1, 0, 0], [0, 0, 1]),2077new CSG.Connector(new CSG.Vector3D([0, cuberadius.y, 0]).plus(center), [0, 1, 0], [0, 0, 1]),2078new CSG.Connector(new CSG.Vector3D([0, -cuberadius.y, 0]).plus(center), [0, -1, 0], [0, 0, 1]),2079new CSG.Connector(new CSG.Vector3D([0, 0, cuberadius.z]).plus(center), [0, 0, 1], [1, 0, 0]),2080new CSG.Connector(new CSG.Vector3D([0, 0, -cuberadius.z]).plus(center), [0, 0, -1], [1, 0, 0])];2081return result;2082};2083
2084CSG.IsFloat = function(n) {2085return(!isNaN(n)) || (n === Infinity) || (n === -Infinity);2086};2087
2088// solve 2x2 linear equation:
2089// [ab][x] = [u]
2090// [cd][y] [v]
2091CSG.solve2Linear = function(a, b, c, d, u, v) {2092var det = a * d - b * c;2093var invdet = 1.0 / det;2094var x = u * d - b * v;2095var y = -u * c + a * v;2096x *= invdet;2097y *= invdet;2098return [x, y];2099};2100
2101// # class Vector3D
2102// Represents a 3D vector.
2103//
2104// Example usage:
2105//
2106// new CSG.Vector3D(1, 2, 3);
2107// new CSG.Vector3D([1, 2, 3]);
2108// new CSG.Vector3D({ x: 1, y: 2, z: 3 });
2109// new CSG.Vector3D(1, 2); // assumes z=0
2110// new CSG.Vector3D([1, 2]); // assumes z=0
2111CSG.Vector3D = function(x, y, z) {2112if(arguments.length == 3) {2113this._x = parseFloat(x);2114this._y = parseFloat(y);2115this._z = parseFloat(z);2116} else if(arguments.length == 2) {2117this._x = parseFloat(x);2118this._y = parseFloat(y);2119this._z = 0;2120} else {2121var ok = true;2122if(arguments.length == 1) {2123if(typeof(x) == "object") {2124if(x instanceof CSG.Vector3D) {2125this._x = x._x;2126this._y = x._y;2127this._z = x._z;2128} else if(x instanceof CSG.Vector2D) {2129this._x = x._x;2130this._y = x._y;2131this._z = 0;2132} else if(x instanceof Array) {2133if((x.length < 2) || (x.length > 3)) {2134ok = false;2135} else {2136this._x = parseFloat(x[0]);2137this._y = parseFloat(x[1]);2138if(x.length == 3) {2139this._z = parseFloat(x[2]);2140} else {2141this._z = 0;2142}2143}2144} else if(('x' in x) && ('y' in x)) {2145this._x = parseFloat(x.x);2146this._y = parseFloat(x.y);2147if('z' in x) {2148this._z = parseFloat(x.z);2149} else {2150this._z = 0;2151}2152} else ok = false;2153} else {2154var v = parseFloat(x);2155this._x = v;2156this._y = v;2157this._z = v;2158}2159} else ok = false;2160if(ok) {2161if((!CSG.IsFloat(this._x)) || (!CSG.IsFloat(this._y)) || (!CSG.IsFloat(this._z))) ok = false;2162}2163if(!ok) {2164throw new Error("wrong arguments");2165}2166}2167};2168
2169CSG.Vector3D.prototype = {2170get x() {2171return this._x;2172}, get y() {2173return this._y;2174}, get z() {2175return this._z;2176},2177
2178set x(v) {2179throw new Error("Vector3D is immutable");2180}, set y(v) {2181throw new Error("Vector3D is immutable");2182}, set z(v) {2183throw new Error("Vector3D is immutable");2184},2185
2186clone: function() {2187return new CSG.Vector3D(this);2188},2189
2190negated: function() {2191return new CSG.Vector3D(-this._x, -this._y, -this._z);2192},2193
2194abs: function() {2195return new CSG.Vector3D(Math.abs(this._x), Math.abs(this._y), Math.abs(this._z));2196},2197
2198plus: function(a) {2199return new CSG.Vector3D(this._x + a._x, this._y + a._y, this._z + a._z);2200},2201
2202minus: function(a) {2203return new CSG.Vector3D(this._x - a._x, this._y - a._y, this._z - a._z);2204},2205
2206times: function(a) {2207return new CSG.Vector3D(this._x * a, this._y * a, this._z * a);2208},2209
2210dividedBy: function(a) {2211return new CSG.Vector3D(this._x / a, this._y / a, this._z / a);2212},2213
2214dot: function(a) {2215return this._x * a._x + this._y * a._y + this._z * a._z;2216},2217
2218lerp: function(a, t) {2219return this.plus(a.minus(this).times(t));2220},2221
2222lengthSquared: function() {2223return this.dot(this);2224},2225
2226length: function() {2227return Math.sqrt(this.lengthSquared());2228},2229
2230unit: function() {2231return this.dividedBy(this.length());2232},2233
2234cross: function(a) {2235return new CSG.Vector3D(2236this._y * a._z - this._z * a._y, this._z * a._x - this._x * a._z, this._x * a._y - this._y * a._x);2237},2238
2239distanceTo: function(a) {2240return this.minus(a).length();2241},2242
2243distanceToSquared: function(a) {2244return this.minus(a).lengthSquared();2245},2246
2247equals: function(a) {2248return(this._x == a._x) && (this._y == a._y) && (this._z == a._z);2249},2250
2251// Right multiply by a 4x4 matrix (the vector is interpreted as a row vector)2252// Returns a new CSG.Vector3D2253multiply4x4: function(matrix4x4) {2254return matrix4x4.leftMultiply1x3Vector(this);2255},2256
2257transform: function(matrix4x4) {2258return matrix4x4.leftMultiply1x3Vector(this);2259},2260
2261toStlString: function() {2262return this._x + " " + this._y + " " + this._z;2263},2264
2265toAMFString: function() {2266return "<x>" + this._x + "</x><y>" + this._y + "</y><z>" + this._z + "</z>";2267},2268
2269toString: function() {2270return "(" + this._x.toFixed(2) + ", " + this._y.toFixed(2) + ", " + this._z.toFixed(2) + ")";2271},2272
2273// find a vector that is somewhat perpendicular to this one2274randomNonParallelVector: function() {2275var abs = this.abs();2276if((abs._x <= abs._y) && (abs._x <= abs._z)) {2277return new CSG.Vector3D(1, 0, 0);2278} else if((abs._y <= abs._x) && (abs._y <= abs._z)) {2279return new CSG.Vector3D(0, 1, 0);2280} else {2281return new CSG.Vector3D(0, 0, 1);2282}2283},2284
2285min: function(p) {2286return new CSG.Vector3D(2287Math.min(this._x, p._x), Math.min(this._y, p._y), Math.min(this._z, p._z));2288},2289
2290max: function(p) {2291return new CSG.Vector3D(2292Math.max(this._x, p._x), Math.max(this._y, p._y), Math.max(this._z, p._z));2293}2294};2295
2296// # class Vertex
2297// Represents a vertex of a polygon. Use your own vertex class instead of this
2298// one to provide additional features like texture coordinates and vertex
2299// colors. Custom vertex classes need to provide a `pos` property
2300// `flipped()`, and `interpolate()` methods that behave analogous to the ones
2301// defined by `CSG.Vertex`.
2302CSG.Vertex = function(pos) {2303this.pos = pos;2304};2305
2306// create from an untyped object with identical property names:
2307CSG.Vertex.fromObject = function(obj) {2308var pos = new CSG.Vector3D(obj.pos);2309return new CSG.Vertex(pos);2310};2311
2312CSG.Vertex.prototype = {2313// Return a vertex with all orientation-specific data (e.g. vertex normal) flipped. Called when the2314// orientation of a polygon is flipped.2315flipped: function() {2316return this;2317},2318
2319getTag: function() {2320var result = this.tag;2321if(!result) {2322result = CSG.getTag();2323this.tag = result;2324}2325return result;2326},2327
2328// Create a new vertex between this vertex and `other` by linearly2329// interpolating all properties using a parameter of `t`. Subclasses should2330// override this to interpolate additional properties.2331interpolate: function(other, t) {2332var newpos = this.pos.lerp(other.pos, t);2333return new CSG.Vertex(newpos);2334},2335
2336// Affine transformation of vertex. Returns a new CSG.Vertex2337transform: function(matrix4x4) {2338var newpos = this.pos.multiply4x4(matrix4x4);2339return new CSG.Vertex(newpos);2340},2341
2342toStlString: function() {2343return "vertex " + this.pos.toStlString() + "\n";2344},2345
2346toAMFString: function() {2347return "<vertex><coordinates>" + this.pos.toAMFString() + "</coordinates></vertex>\n";2348},2349
2350toString: function() {2351return this.pos.toString();2352}2353};2354
2355// # class Plane
2356// Represents a plane in 3D space.
2357CSG.Plane = function(normal, w) {2358this.normal = normal;2359this.w = w;2360};2361
2362// create from an untyped object with identical property names:
2363CSG.Plane.fromObject = function(obj) {2364var normal = new CSG.Vector3D(obj.normal);2365var w = parseFloat(obj.w);2366return new CSG.Plane(normal, w);2367};2368
2369// `CSG.Plane.EPSILON` is the tolerance used by `splitPolygon()` to decide if a
2370// point is on the plane.
2371CSG.Plane.EPSILON = 1e-5;2372
2373CSG.Plane.fromVector3Ds = function(a, b, c) {2374var n = b.minus(a).cross(c.minus(a)).unit();2375return new CSG.Plane(n, n.dot(a));2376};2377
2378// like fromVector3Ds, but allow the vectors to be on one point or one line
2379// in such a case a random plane through the given points is constructed
2380CSG.Plane.anyPlaneFromVector3Ds = function(a, b, c) {2381var v1 = b.minus(a);2382var v2 = c.minus(a);2383if(v1.length() < 1e-5) {2384v1 = v2.randomNonParallelVector();2385}2386if(v2.length() < 1e-5) {2387v2 = v1.randomNonParallelVector();2388}2389var normal = v1.cross(v2);2390if(normal.length() < 1e-5) {2391// this would mean that v1 == v2.negated()2392v2 = v1.randomNonParallelVector();2393normal = v1.cross(v2);2394}2395normal = normal.unit();2396return new CSG.Plane(normal, normal.dot(a));2397};2398
2399CSG.Plane.fromPoints = function(a, b, c) {2400a = new CSG.Vector3D(a);2401b = new CSG.Vector3D(b);2402c = new CSG.Vector3D(c);2403return CSG.Plane.fromVector3Ds(a, b, c);2404};2405
2406CSG.Plane.fromNormalAndPoint = function(normal, point) {2407normal = new CSG.Vector3D(normal);2408point = new CSG.Vector3D(point);2409normal = normal.unit();2410var w = point.dot(normal);2411return new CSG.Plane(normal, w);2412};2413
2414CSG.Plane.prototype = {2415flipped: function() {2416return new CSG.Plane(this.normal.negated(), -this.w);2417},2418
2419getTag: function() {2420var result = this.tag;2421if(!result) {2422result = CSG.getTag();2423this.tag = result;2424}2425return result;2426},2427
2428equals: function(n) {2429return this.normal.equals(n.normal) && this.w == n.w;2430},2431
2432transform: function(matrix4x4) {2433var ismirror = matrix4x4.isMirroring();2434// get two vectors in the plane:2435var r = this.normal.randomNonParallelVector();2436var u = this.normal.cross(r);2437var v = this.normal.cross(u);2438// get 3 points in the plane:2439var point1 = this.normal.times(this.w);2440var point2 = point1.plus(u);2441var point3 = point1.plus(v);2442// transform the points:2443point1 = point1.multiply4x4(matrix4x4);2444point2 = point2.multiply4x4(matrix4x4);2445point3 = point3.multiply4x4(matrix4x4);2446// and create a new plane from the transformed points:2447var newplane = CSG.Plane.fromVector3Ds(point1, point2, point3);2448if(ismirror) {2449// the transform is mirroring2450// We should mirror the plane:2451newplane = newplane.flipped();2452}2453return newplane;2454},2455
2456// Returns object:2457// .type:2458// 0: coplanar-front2459// 1: coplanar-back2460// 2: front2461// 3: back2462// 4: spanning2463// In case the polygon is spanning, returns:2464// .front: a CSG.Polygon of the front part2465// .back: a CSG.Polygon of the back part2466splitPolygon: function(polygon) {2467var result = {2468type: null,2469front: null,2470back: null2471};2472// cache in local vars (speedup):2473var planenormal = this.normal;2474var vertices = polygon.vertices;2475var numvertices = vertices.length;2476if(polygon.plane.equals(this)) {2477result.type = 0;2478} else {2479var EPS = CSG.Plane.EPSILON;2480var thisw = this.w;2481var hasfront = false;2482var hasback = false;2483var vertexIsBack = [];2484var MINEPS = -EPS;2485for(var i = 0; i < numvertices; i++) {2486var t = planenormal.dot(vertices[i].pos) - thisw;2487var isback = (t < 0);2488vertexIsBack.push(isback);2489if(t > EPS) hasfront = true;2490if(t < MINEPS) hasback = true;2491}2492if((!hasfront) && (!hasback)) {2493// all points coplanar2494var t = planenormal.dot(polygon.plane.normal);2495result.type = (t >= 0) ? 0 : 1;2496} else if(!hasback) {2497result.type = 2;2498} else if(!hasfront) {2499result.type = 3;2500} else {2501// spanning2502result.type = 4;2503var frontvertices = [],2504backvertices = [];2505var isback = vertexIsBack[0];2506for(var vertexindex = 0; vertexindex < numvertices; vertexindex++) {2507var vertex = vertices[vertexindex];2508var nextvertexindex = vertexindex + 1;2509if(nextvertexindex >= numvertices) nextvertexindex = 0;2510var nextisback = vertexIsBack[nextvertexindex];2511if(isback == nextisback) {2512// line segment is on one side of the plane:2513if(isback) {2514backvertices.push(vertex);2515} else {2516frontvertices.push(vertex);2517}2518} else {2519// line segment intersects plane:2520var point = vertex.pos;2521var nextpoint = vertices[nextvertexindex].pos;2522var intersectionpoint = this.splitLineBetweenPoints(point, nextpoint);2523var intersectionvertex = new CSG.Vertex(intersectionpoint);2524if(isback) {2525backvertices.push(vertex);2526backvertices.push(intersectionvertex);2527frontvertices.push(intersectionvertex);2528} else {2529frontvertices.push(vertex);2530frontvertices.push(intersectionvertex);2531backvertices.push(intersectionvertex);2532}2533}2534isback = nextisback;2535} // for vertexindex2536// remove duplicate vertices:2537var EPS_SQUARED = CSG.Plane.EPSILON * CSG.Plane.EPSILON;2538if(backvertices.length >= 3) {2539var prevvertex = backvertices[backvertices.length - 1];2540for(var vertexindex = 0; vertexindex < backvertices.length; vertexindex++) {2541var vertex = backvertices[vertexindex];2542if(vertex.pos.distanceToSquared(prevvertex.pos) < EPS_SQUARED) {2543backvertices.splice(vertexindex, 1);2544vertexindex--;2545}2546prevvertex = vertex;2547}2548}2549if(frontvertices.length >= 3) {2550var prevvertex = frontvertices[frontvertices.length - 1];2551for(var vertexindex = 0; vertexindex < frontvertices.length; vertexindex++) {2552var vertex = frontvertices[vertexindex];2553if(vertex.pos.distanceToSquared(prevvertex.pos) < EPS_SQUARED) {2554frontvertices.splice(vertexindex, 1);2555vertexindex--;2556}2557prevvertex = vertex;2558}2559}2560if(frontvertices.length >= 3) {2561result.front = new CSG.Polygon(frontvertices, polygon.shared, polygon.plane);2562}2563if(backvertices.length >= 3) {2564result.back = new CSG.Polygon(backvertices, polygon.shared, polygon.plane);2565}2566}2567}2568return result;2569},2570
2571// robust splitting of a line by a plane2572// will work even if the line is parallel to the plane2573splitLineBetweenPoints: function(p1, p2) {2574var direction = p2.minus(p1);2575var labda = (this.w - this.normal.dot(p1)) / this.normal.dot(direction);2576if(isNaN(labda)) labda = 0;2577if(labda > 1) labda = 1;2578if(labda < 0) labda = 0;2579var result = p1.plus(direction.times(labda));2580return result;2581},2582
2583// returns CSG.Vector3D2584intersectWithLine: function(line3d) {2585return line3d.intersectWithPlane(this);2586},2587
2588// intersection of two planes2589intersectWithPlane: function(plane) {2590return CSG.Line3D.fromPlanes(this, plane);2591},2592
2593signedDistanceToPoint: function(point) {2594var t = this.normal.dot(point) - this.w;2595return t;2596},2597
2598toString: function() {2599return "[normal: " + this.normal.toString() + ", w: " + this.w + "]";2600},2601
2602mirrorPoint: function(point3d) {2603var distance = this.signedDistanceToPoint(point3d);2604var mirrored = point3d.minus(this.normal.times(distance * 2.0));2605return mirrored;2606}2607};2608
2609
2610// # class Polygon
2611// Represents a convex polygon. The vertices used to initialize a polygon must
2612// be coplanar and form a convex loop. They do not have to be `CSG.Vertex`
2613// instances but they must behave similarly (duck typing can be used for
2614// customization).
2615//
2616// Each convex polygon has a `shared` property, which is shared between all
2617// polygons that are clones of each other or were split from the same polygon.
2618// This can be used to define per-polygon properties (such as surface color).
2619//
2620// The plane of the polygon is calculated from the vertex coordinates
2621// To avoid unnecessary recalculation, the plane can alternatively be
2622// passed as the third argument
2623CSG.Polygon = function(vertices, shared, plane) {2624this.vertices = vertices;2625if(!shared) shared = CSG.Polygon.defaultShared;2626this.shared = shared;2627//var numvertices = vertices.length;2628
2629if(arguments.length >= 3) {2630this.plane = plane;2631} else {2632this.plane = CSG.Plane.fromVector3Ds(vertices[0].pos, vertices[1].pos, vertices[2].pos);2633}2634
2635if(_CSGDEBUG) {2636this.checkIfConvex();2637}2638};2639
2640// create from an untyped object with identical property names:
2641CSG.Polygon.fromObject = function(obj) {2642var vertices = obj.vertices.map(function(v) {2643return CSG.Vertex.fromObject(v);2644});2645var shared = CSG.Polygon.Shared.fromObject(obj.shared);2646var plane = CSG.Plane.fromObject(obj.plane);2647return new CSG.Polygon(vertices, shared, plane);2648};2649
2650CSG.Polygon.prototype = {2651// check whether the polygon is convex (it should be, otherwise we will get unexpected results)2652checkIfConvex: function() {2653if(!CSG.Polygon.verticesConvex(this.vertices, this.plane.normal)) {2654CSG.Polygon.verticesConvex(this.vertices, this.plane.normal);2655throw new Error("Not convex!");2656}2657},2658
2659/**2660* @param {Array} color [red, green, blue, alpha] color values are float numbers 0..1
2661* @return {CSG.Polygon} The current polygon
2662*/
2663setColor: function(red, green, blue, alpha) {2664var color = red instanceof Array ? red : [red||0, green||0, blue||0, isNaN(alpha) ? 1. : alpha];2665this.shared = new CSG.Polygon.Shared(color);2666return this;2667},2668
2669// Extrude a polygon into the direction offsetvector2670// Returns a CSG object2671extrude: function(offsetvector) {2672var newpolygons = [];2673
2674var polygon1 = this;2675var direction = polygon1.plane.normal.dot(offsetvector);2676if(direction > 0) {2677polygon1 = polygon1.flipped();2678}2679newpolygons.push(polygon1);2680var polygon2 = polygon1.translate(offsetvector);2681var numvertices = this.vertices.length;2682for(var i = 0; i < numvertices; i++) {2683var sidefacepoints = [];2684var nexti = (i < (numvertices - 1)) ? i + 1 : 0;2685sidefacepoints.push(polygon1.vertices[i].pos);2686sidefacepoints.push(polygon2.vertices[i].pos);2687sidefacepoints.push(polygon2.vertices[nexti].pos);2688sidefacepoints.push(polygon1.vertices[nexti].pos);2689var sidefacepolygon = CSG.Polygon.createFromPoints(sidefacepoints, this.shared);2690newpolygons.push(sidefacepolygon);2691}2692polygon2 = polygon2.flipped();2693newpolygons.push(polygon2);2694return CSG.fromPolygons(newpolygons);2695},2696
2697translate: function(offset) {2698return this.transform(CSG.Matrix4x4.translation(offset));2699},2700
2701// returns an array with a CSG.Vector3D (center point) and a radius2702boundingSphere: function() {2703if(!this.cachedBoundingSphere) {2704var box = this.boundingBox();2705var middle = box[0].plus(box[1]).times(0.5);2706var radius3 = box[1].minus(middle);2707var radius = radius3.length();2708this.cachedBoundingSphere = [middle, radius];2709}2710return this.cachedBoundingSphere;2711},2712
2713// returns an array of two CSG.Vector3Ds (minimum coordinates and maximum coordinates)2714boundingBox: function() {2715if(!this.cachedBoundingBox) {2716var minpoint, maxpoint;2717var vertices = this.vertices;2718var numvertices = vertices.length;2719if(numvertices === 0) {2720minpoint = new CSG.Vector3D(0, 0, 0);2721} else {2722minpoint = vertices[0].pos;2723}2724maxpoint = minpoint;2725for(var i = 1; i < numvertices; i++) {2726var point = vertices[i].pos;2727minpoint = minpoint.min(point);2728maxpoint = maxpoint.max(point);2729}2730this.cachedBoundingBox = [minpoint, maxpoint];2731}2732return this.cachedBoundingBox;2733},2734
2735flipped: function() {2736var newvertices = this.vertices.map(function(v) {2737return v.flipped();2738});2739newvertices.reverse();2740var newplane = this.plane.flipped();2741return new CSG.Polygon(newvertices, this.shared, newplane);2742},2743
2744// Affine transformation of polygon. Returns a new CSG.Polygon2745transform: function(matrix4x4) {2746var newvertices = this.vertices.map(function(v) {2747return v.transform(matrix4x4);2748});2749var newplane = this.plane.transform(matrix4x4);2750var scalefactor = matrix4x4.elements[0] * matrix4x4.elements[5] * matrix4x4.elements[10];2751if(scalefactor < 0) {2752// the transformation includes mirroring. We need to reverse the vertex order2753// in order to preserve the inside/outside orientation:2754newvertices.reverse();2755}2756return new CSG.Polygon(newvertices, this.shared, newplane);2757},2758
2759toStlString: function() {2760var result = "";2761if(this.vertices.length >= 3) // should be!2762{2763// STL requires triangular polygons. If our polygon has more vertices, create2764// multiple triangles:2765var firstVertexStl = this.vertices[0].toStlString();2766for(var i = 0; i < this.vertices.length - 2; i++) {2767result += "facet normal " + this.plane.normal.toStlString() + "\nouter loop\n";2768result += firstVertexStl;2769result += this.vertices[i + 1].toStlString();2770result += this.vertices[i + 2].toStlString();2771result += "endloop\nendfacet\n";2772}2773}2774return result;2775},2776
2777toString: function() {2778var result = "Polygon plane: " + this.plane.toString() + "\n";2779this.vertices.map(function(vertex) {2780result += " " + vertex.toString() + "\n";2781});2782return result;2783},2784
2785// project the 3D polygon onto a plane2786projectToOrthoNormalBasis: function(orthobasis) {2787var points2d = this.vertices.map(function(vertex) {2788return orthobasis.to2D(vertex.pos);2789});2790var result = CAG.fromPointsNoCheck(points2d);2791var area = result.area();2792if(Math.abs(area) < 1e-5) {2793// the polygon was perpendicular to the orthnormal plane. The resulting 2D polygon would be degenerate2794// return an empty area instead:2795result = new CAG();2796} else if(area < 0) {2797result = result.flipped();2798}2799return result;2800},2801
2802/**2803* Creates solid from slices (CSG.Polygon) by generating walls
2804* @param {Object} options Solid generating options
2805* - numslices {Number} Number of slices to be generated
2806* - callback(t, slice) {Function} Callback function generating slices.
2807* arguments: t = [0..1], slice = [0..numslices - 1]
2808* return: CSG.Polygon or null to skip
2809* - loop {Boolean} no flats, only walls, it's used to generate solids like a tor
2810*
2811* by Eduard Bespalov AKA tedbeer (2013)
2812*/
2813solidFromSlices: function(options) {2814var polygons = [],2815csg = null,2816prev = null,2817bottom = null,2818top = null,2819numSlices = 2,2820bLoop = false,2821fnCallback,2822flipped = null;2823
2824if (options) {2825bLoop = Boolean(options['loop']);2826
2827if (options.numslices)2828numSlices = options.numslices;2829
2830if (options.callback)2831fnCallback = options.callback;2832}2833if (!fnCallback) {2834var square = new CSG.Polygon.createFromPoints([2835[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]2836]);2837fnCallback = function(t, slice) {2838return t == 0 || t == 1 ? square.translate([0,0,t]) : null;2839}2840}2841for(var i = 0, iMax = numSlices - 1; i <= iMax; i++) {2842csg = fnCallback.call(this, i / iMax, i);2843if (csg) {2844if (!(csg instanceof CSG.Polygon)) {2845throw new Error("CSG.Polygon.solidFromSlices callback error: CSG.Polygon expected");2846}2847csg.checkIfConvex();2848
2849if (prev) {//generate walls2850if (flipped === null) {//not generated yet2851flipped = prev.plane.signedDistanceToPoint(csg.vertices[0].pos) < 0;2852}2853this._addWalls(polygons, prev, csg, flipped);2854
2855} else {//the first - will be a bottom2856bottom = csg;2857}2858prev = csg;2859} //callback can return null to skip that slice2860}2861top = csg;2862
2863if (bLoop) {2864var bSameTopBottom = bottom.vertices.length == top.vertices.length &&2865bottom.vertices.every(function(v, index){2866return v.pos.equals(top.vertices[index].pos)2867});2868//if top and bottom are not the same -2869//generate walls between them2870if (!bSameTopBottom) {2871this._addWalls(polygons, top, bottom, flipped);2872} //else - already generated2873} else {2874//save top and bottom2875//TODO: flip if necessary2876polygons.unshift(flipped ? bottom : bottom.flipped());2877polygons.push(flipped ? top.flipped() : top);2878}2879return CSG.fromPolygons(polygons);2880},2881/**2882*
2883* @param walls Array of wall polygons
2884* @param bottom Bottom polygon
2885* @param top Top polygon
2886*/
2887_addWalls: function(walls, bottom, top, bFlipped) {2888var bottomPoints = bottom.vertices.slice(0),//make a copy2889topPoints = top.vertices.slice(0),//make a copy2890color = top.shared || null;2891
2892//check if bottom perimeter is closed2893if (!bottomPoints[0].pos.equals(bottomPoints[bottomPoints.length - 1].pos)) {2894bottomPoints.push(bottomPoints[0]);2895}2896
2897//check if top perimeter is closed2898if (!topPoints[0].pos.equals(topPoints[topPoints.length - 1].pos)) {2899topPoints.push(topPoints[0]);2900}2901if (bFlipped) {2902bottomPoints = bottomPoints.reverse();2903topPoints = topPoints.reverse();2904}2905
2906var iTopLen = topPoints.length - 1,2907iBotLen = bottomPoints.length - 1,2908iExtra = iTopLen - iBotLen, //how many extra triangles we need2909bMoreTops = iExtra > 0,2910bMoreBottoms = iExtra < 0;2911
2912var aMin = []; //indexes to start extra triangles (polygon with minimal square)2913//init - we need exactly /iExtra/ small triangles2914for (var i = Math.abs(iExtra); i > 0; i--) {2915aMin.push({len: Infinity, index: -1});2916}2917
2918var len;2919if (bMoreBottoms) {2920for (var i = 0; i < iBotLen; i++) {2921len = bottomPoints[i].pos.distanceToSquared(bottomPoints[i+1].pos);2922//find the element to replace2923for (var j = aMin.length - 1; j >= 0; j--) {2924if (aMin[j].len > len) {2925aMin[j].len = len;2926aMin.index = j;2927break;2928}2929}//for2930}2931} else if (bMoreTops) {2932for (var i = 0; i < iTopLen; i++) {2933len = topPoints[i].pos.distanceToSquared(topPoints[i+1].pos);2934//find the element to replace2935for (var j = aMin.length - 1; j >= 0; j--) {2936if (aMin[j].len > len) {2937aMin[j].len = len;2938aMin.index = j;2939break;2940}2941}//for2942}2943}//if2944//sort by index2945aMin.sort(fnSortByIndex);2946var getTriangle = function addWallsPutTriangle (pointA, pointB, pointC, color) {2947return new CSG.Polygon([pointA, pointB, pointC], color);2948//return bFlipped ? triangle.flipped() : triangle;2949};2950
2951var bpoint = bottomPoints[0],2952tpoint = topPoints[0],2953secondPoint,2954nBotFacet, nTopFacet; //length of triangle facet side2955for (var iB = 0, iT = 0, iMax = iTopLen + iBotLen; iB + iT < iMax;) {2956if (aMin.length) {2957if (bMoreTops && iT == aMin[0].index) {//one vertex is on the bottom, 2 - on the top2958secondPoint = topPoints[++iT];2959//console.log('<<< extra top: ' + secondPoint + ', ' + tpoint + ', bottom: ' + bpoint);2960walls.push(getTriangle(2961secondPoint, tpoint, bpoint, color2962));2963tpoint = secondPoint;2964aMin.shift();2965continue;2966} else if (bMoreBottoms && iB == aMin[0].index) {2967secondPoint = bottomPoints[++iB];2968walls.push(getTriangle(2969tpoint, bpoint, secondPoint, color2970));2971bpoint = secondPoint;2972aMin.shift();2973continue;2974}2975}2976//choose the shortest path2977if (iB < iBotLen) { //one vertex is on the top, 2 - on the bottom2978nBotFacet = tpoint.pos.distanceToSquared(bottomPoints[iB+1].pos);2979} else {2980nBotFacet = Infinity;2981}2982if (iT < iTopLen) { //one vertex is on the bottom, 2 - on the top2983nTopFacet = bpoint.pos.distanceToSquared(topPoints[iT+1].pos);2984} else {2985nTopFacet = Infinity;2986}2987if (nBotFacet <= nTopFacet) {2988secondPoint = bottomPoints[++iB];2989walls.push(getTriangle(2990tpoint, bpoint, secondPoint, color2991));2992bpoint = secondPoint;2993} else if (iT < iTopLen) { //nTopFacet < Infinity2994secondPoint = topPoints[++iT];2995//console.log('<<< top: ' + secondPoint + ', ' + tpoint + ', bottom: ' + bpoint);2996walls.push(getTriangle(2997secondPoint, tpoint, bpoint, color2998));2999tpoint = secondPoint;3000};3001}3002return walls;3003}3004};3005
3006CSG.Polygon.verticesConvex = function(vertices, planenormal) {3007var numvertices = vertices.length;3008if(numvertices > 2) {3009var prevprevpos = vertices[numvertices - 2].pos;3010var prevpos = vertices[numvertices - 1].pos;3011for(var i = 0; i < numvertices; i++) {3012var pos = vertices[i].pos;3013if(!CSG.Polygon.isConvexPoint(prevprevpos, prevpos, pos, planenormal)) {3014return false;3015}3016prevprevpos = prevpos;3017prevpos = pos;3018}3019}3020return true;3021};3022
3023// Create a polygon from the given points
3024CSG.Polygon.createFromPoints = function(points, shared, plane) {3025var normal;3026if(arguments.length < 3) {3027// initially set a dummy vertex normal:3028normal = new CSG.Vector3D(0, 0, 0);3029} else {3030normal = plane.normal;3031}3032var vertices = [];3033points.map(function(p) {3034var vec = new CSG.Vector3D(p);3035var vertex = new CSG.Vertex(vec);3036vertices.push(vertex);3037});3038var polygon;3039if(arguments.length < 3) {3040polygon = new CSG.Polygon(vertices, shared);3041} else {3042polygon = new CSG.Polygon(vertices, shared, plane);3043}3044return polygon;3045};3046
3047// calculate whether three points form a convex corner
3048// prevpoint, point, nextpoint: the 3 coordinates (CSG.Vector3D instances)
3049// normal: the normal vector of the plane
3050CSG.Polygon.isConvexPoint = function(prevpoint, point, nextpoint, normal) {3051var crossproduct = point.minus(prevpoint).cross(nextpoint.minus(point));3052var crossdotnormal = crossproduct.dot(normal);3053return(crossdotnormal >= 0);3054};3055
3056CSG.Polygon.isStrictlyConvexPoint = function(prevpoint, point, nextpoint, normal) {3057var crossproduct = point.minus(prevpoint).cross(nextpoint.minus(point));3058var crossdotnormal = crossproduct.dot(normal);3059return(crossdotnormal >= 1e-5);3060};3061
3062// # class CSG.Polygon.Shared
3063// Holds the shared properties for each polygon (currently only color)
3064CSG.Polygon.Shared = function(color) {3065this.color = color;3066};3067
3068CSG.Polygon.Shared.fromObject = function(obj) {3069return new CSG.Polygon.Shared(obj.color);3070};3071
3072CSG.Polygon.Shared.prototype = {3073getTag: function() {3074var result = this.tag;3075if(!result) {3076result = CSG.getTag();3077this.tag = result;3078}3079return result;3080},3081// get a string uniquely identifying this object3082getHash: function() {3083if(!this.color) return "null";3084return "" + this.color[0] + "/" + this.color[1] + "/" + this.color[2] + "/" + this.color[3];3085}3086};3087
3088CSG.Polygon.defaultShared = new CSG.Polygon.Shared(null);3089
3090// # class PolygonTreeNode
3091// This class manages hierarchical splits of polygons
3092// At the top is a root node which doesn hold a polygon, only child PolygonTreeNodes
3093// Below that are zero or more 'top' nodes; each holds a polygon. The polygons can be in different planes
3094// splitByPlane() splits a node by a plane. If the plane intersects the polygon, two new child nodes
3095// are created holding the splitted polygon.
3096// getPolygons() retrieves the polygon from the tree. If for PolygonTreeNode the polygon is split but
3097// the two split parts (child nodes) are still intact, then the unsplit polygon is returned.
3098// This ensures that we can safely split a polygon into many fragments. If the fragments are untouched,
3099// getPolygons() will return the original unsplit polygon instead of the fragments.
3100// remove() removes a polygon from the tree. Once a polygon is removed, the parent polygons are invalidated
3101// since they are no longer intact.
3102// constructor creates the root node:
3103CSG.PolygonTreeNode = function() {3104this.parent = null;3105this.children = [];3106this.polygon = null;3107this.removed = false;3108};3109
3110CSG.PolygonTreeNode.prototype = {3111// fill the tree with polygons. Should be called on the root node only; child nodes must3112// always be a derivate (split) of the parent node.3113addPolygons: function(polygons) {3114if(!this.isRootNode())3115// new polygons can only be added to root node; children can only be splitted polygons3116throw new Error("Assertion failed");3117var _this = this;3118polygons.map(function(polygon) {3119_this.addChild(polygon);3120});3121},3122
3123// remove a node3124// - the siblings become toplevel nodes3125// - the parent is removed recursively3126remove: function() {3127if(!this.removed) {3128this.removed = true;3129
3130if(_CSGDEBUG) {3131if(this.isRootNode()) throw new Error("Assertion failed"); // can't remove root node3132if(this.children.length) throw new Error("Assertion failed"); // we shouldn't remove nodes with children3133}3134
3135// remove ourselves from the parent's children list:3136var parentschildren = this.parent.children;3137var i = parentschildren.indexOf(this);3138if(i < 0) throw new Error("Assertion failed");3139parentschildren.splice(i, 1);3140
3141// invalidate the parent's polygon, and of all parents above it:3142this.parent.recursivelyInvalidatePolygon();3143}3144},3145
3146isRemoved: function() {3147return this.removed;3148},3149
3150isRootNode: function() {3151return !this.parent;3152},3153
3154// invert all polygons in the tree. Call on the root node3155invert: function() {3156if(!this.isRootNode()) throw new Error("Assertion failed"); // can only call this on the root node3157this.invertSub();3158},3159
3160getPolygon: function() {3161if(!this.polygon) throw new Error("Assertion failed"); // doesn't have a polygon, which means that it has been broken down3162return this.polygon;3163},3164
3165getPolygons: function(result) {3166if(this.polygon) {3167// the polygon hasn't been broken yet. We can ignore the children and return our polygon:3168result.push(this.polygon);3169} else {3170// our polygon has been split up and broken, so gather all subpolygons from the children:3171var childpolygons = [];3172this.children.map(function(child) {3173child.getPolygons(childpolygons);3174});3175childpolygons.map(function(p) {3176result.push(p);3177});3178}3179},3180
3181// split the node by a plane; add the resulting nodes to the frontnodes and backnodes array3182// If the plane doesn't intersect the polygon, the 'this' object is added to one of the arrays3183// If the plane does intersect the polygon, two new child nodes are created for the front and back fragments,3184// and added to both arrays.3185splitByPlane: function(plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes) {3186var children = this.children;3187var numchildren = children.length;3188if(numchildren > 0) {3189// if we have children, split the children3190for(var i = 0; i < numchildren; i++) {3191children[i].splitByPlane(plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes);3192}3193} else {3194// no children. Split the polygon:3195var polygon = this.polygon;3196if(polygon) {3197var bound = polygon.boundingSphere();3198var sphereradius = bound[1] + 1e-4;3199var planenormal = plane.normal;3200var spherecenter = bound[0];3201var d = planenormal.dot(spherecenter) - plane.w;3202if(d > sphereradius) {3203frontnodes.push(this);3204} else if(d < -sphereradius) {3205backnodes.push(this);3206} else {3207var splitresult = plane.splitPolygon(polygon);3208switch(splitresult.type) {3209case 0:3210// coplanar front:3211coplanarfrontnodes.push(this);3212break;3213
3214case 1:3215// coplanar back:3216coplanarbacknodes.push(this);3217break;3218
3219case 2:3220// front:3221frontnodes.push(this);3222break;3223
3224case 3:3225// back:3226backnodes.push(this);3227break;3228
3229case 4:3230// spanning:3231if(splitresult.front) {3232var frontnode = this.addChild(splitresult.front);3233frontnodes.push(frontnode);3234}3235if(splitresult.back) {3236var backnode = this.addChild(splitresult.back);3237backnodes.push(backnode);3238}3239break;3240}3241}3242}3243}3244},3245
3246
3247// PRIVATE methods from here:3248// add child to a node3249// this should be called whenever the polygon is split3250// a child should be created for every fragment of the split polygon3251// returns the newly created child3252addChild: function(polygon) {3253var newchild = new CSG.PolygonTreeNode();3254newchild.parent = this;3255newchild.polygon = polygon;3256this.children.push(newchild);3257return newchild;3258},3259
3260invertSub: function() {3261if(this.polygon) {3262this.polygon = this.polygon.flipped();3263}3264this.children.map(function(child) {3265child.invertSub();3266});3267},3268
3269recursivelyInvalidatePolygon: function() {3270if(this.polygon) {3271this.polygon = null;3272if(this.parent) {3273this.parent.recursivelyInvalidatePolygon();3274}3275}3276}3277};3278
3279
3280
3281// # class Tree
3282// This is the root of a BSP tree
3283// We are using this separate class for the root of the tree, to hold the PolygonTreeNode root
3284// The actual tree is kept in this.rootnode
3285CSG.Tree = function(polygons) {3286this.polygonTree = new CSG.PolygonTreeNode();3287this.rootnode = new CSG.Node(null);3288if(polygons) this.addPolygons(polygons);3289};3290
3291CSG.Tree.prototype = {3292invert: function() {3293this.polygonTree.invert();3294this.rootnode.invert();3295},3296
3297// Remove all polygons in this BSP tree that are inside the other BSP tree3298// `tree`.3299clipTo: function(tree, alsoRemovecoplanarFront) {3300alsoRemovecoplanarFront = alsoRemovecoplanarFront ? true : false;3301this.rootnode.clipTo(tree, alsoRemovecoplanarFront);3302},3303
3304allPolygons: function() {3305var result = [];3306this.polygonTree.getPolygons(result);3307return result;3308},3309
3310addPolygons: function(polygons) {3311var _this = this;3312var polygontreenodes = polygons.map(function(p) {3313return _this.polygonTree.addChild(p);3314});3315this.rootnode.addPolygonTreeNodes(polygontreenodes);3316}3317};3318
3319// # class Node
3320// Holds a node in a BSP tree. A BSP tree is built from a collection of polygons
3321// by picking a polygon to split along.
3322// Polygons are not stored directly in the tree, but in PolygonTreeNodes, stored in
3323// this.polygontreenodes. Those PolygonTreeNodes are children of the owning
3324// CSG.Tree.polygonTree
3325// This is not a leafy BSP tree since there is
3326// no distinction between internal and leaf nodes.
3327CSG.Node = function(parent) {3328this.plane = null;3329this.front = null;3330this.back = null;3331this.polygontreenodes = [];3332this.parent = parent;3333};3334
3335CSG.Node.prototype = {3336// Convert solid space to empty space and empty space to solid space.3337invert: function() {3338if(this.plane) this.plane = this.plane.flipped();3339if(this.front) this.front.invert();3340if(this.back) this.back.invert();3341var temp = this.front;3342this.front = this.back;3343this.back = temp;3344},3345
3346// clip polygontreenodes to our plane3347// calls remove() for all clipped PolygonTreeNodes3348clipPolygons: function(polygontreenodes, alsoRemovecoplanarFront) {3349if(this.plane) {3350var backnodes = [];3351var frontnodes = [];3352var coplanarfrontnodes = alsoRemovecoplanarFront ? backnodes : frontnodes;3353var plane = this.plane;3354var numpolygontreenodes = polygontreenodes.length;3355for(var i = 0; i < numpolygontreenodes; i++) {3356var node = polygontreenodes[i];3357if(!node.isRemoved()) {3358node.splitByPlane(plane, coplanarfrontnodes, backnodes, frontnodes, backnodes);3359}3360}3361if(this.front && (frontnodes.length > 0)) {3362this.front.clipPolygons(frontnodes, alsoRemovecoplanarFront);3363}3364var numbacknodes = backnodes.length;3365if(this.back && (numbacknodes > 0)) {3366this.back.clipPolygons(backnodes, alsoRemovecoplanarFront);3367} else {3368// there's nothing behind this plane. Delete the nodes behind this plane:3369for(var i = 0; i < numbacknodes; i++) {3370backnodes[i].remove();3371}3372}3373}3374},3375
3376// Remove all polygons in this BSP tree that are inside the other BSP tree3377// `tree`.3378clipTo: function(tree, alsoRemovecoplanarFront) {3379if(this.polygontreenodes.length > 0) {3380tree.rootnode.clipPolygons(this.polygontreenodes, alsoRemovecoplanarFront);3381}3382if(this.front) this.front.clipTo(tree, alsoRemovecoplanarFront);3383if(this.back) this.back.clipTo(tree, alsoRemovecoplanarFront);3384},3385
3386addPolygonTreeNodes: function(polygontreenodes) {3387if(polygontreenodes.length === 0) return;3388var _this = this;3389if(!this.plane) {3390var bestplane = polygontreenodes[0].getPolygon().plane;3391/*3392var parentnormals = [];
3393this.getParentPlaneNormals(parentnormals, 6);
3394//parentnormals = [];
3395var numparentnormals = parentnormals.length;
3396var minmaxnormal = 1.0;
3397polygontreenodes.map(function(polygontreenode){
3398var plane = polygontreenodes[0].getPolygon().plane;
3399var planenormal = plane.normal;
3400var maxnormaldot = -1.0;
3401parentnormals.map(function(parentnormal){
3402var dot = parentnormal.dot(planenormal);
3403if(dot > maxnormaldot) maxnormaldot = dot;
3404});
3405if(maxnormaldot < minmaxnormal)
3406{
3407minmaxnormal = maxnormaldot;
3408bestplane = plane;
3409}
3410});
3411*/
3412this.plane = bestplane;3413}3414var frontnodes = [];3415var backnodes = [];3416polygontreenodes.map(function(polygontreenode) {3417polygontreenode.splitByPlane(_this.plane, _this.polygontreenodes, backnodes, frontnodes, backnodes);3418});3419if(frontnodes.length > 0) {3420if(!this.front) this.front = new CSG.Node(this);3421this.front.addPolygonTreeNodes(frontnodes);3422}3423if(backnodes.length > 0) {3424if(!this.back) this.back = new CSG.Node(this);3425this.back.addPolygonTreeNodes(backnodes);3426}3427},3428
3429getParentPlaneNormals: function(normals, maxdepth) {3430if(maxdepth > 0) {3431if(this.parent) {3432normals.push(this.parent.plane.normal);3433this.parent.getParentPlaneNormals(normals, maxdepth - 1);3434}3435}3436}3437};3438
3439//////////
3440// # class Matrix4x4:
3441// Represents a 4x4 matrix. Elements are specified in row order
3442CSG.Matrix4x4 = function(elements) {3443if(arguments.length >= 1) {3444this.elements = elements;3445} else {3446// if no arguments passed: create unity matrix3447this.elements = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];3448}3449};3450
3451CSG.Matrix4x4.prototype = {3452plus: function(m) {3453var r = [];3454for(var i = 0; i < 16; i++) {3455r[i] = this.elements[i] + m.elements[i];3456}3457return new CSG.Matrix4x4(r);3458},3459
3460minus: function(m) {3461var r = [];3462for(var i = 0; i < 16; i++) {3463r[i] = this.elements[i] - m.elements[i];3464}3465return new CSG.Matrix4x4(r);3466},3467
3468// right multiply by another 4x4 matrix:3469multiply: function(m) {3470// cache elements in local variables, for speedup:3471var this0 = this.elements[0];3472var this1 = this.elements[1];3473var this2 = this.elements[2];3474var this3 = this.elements[3];3475var this4 = this.elements[4];3476var this5 = this.elements[5];3477var this6 = this.elements[6];3478var this7 = this.elements[7];3479var this8 = this.elements[8];3480var this9 = this.elements[9];3481var this10 = this.elements[10];3482var this11 = this.elements[11];3483var this12 = this.elements[12];3484var this13 = this.elements[13];3485var this14 = this.elements[14];3486var this15 = this.elements[15];3487var m0 = m.elements[0];3488var m1 = m.elements[1];3489var m2 = m.elements[2];3490var m3 = m.elements[3];3491var m4 = m.elements[4];3492var m5 = m.elements[5];3493var m6 = m.elements[6];3494var m7 = m.elements[7];3495var m8 = m.elements[8];3496var m9 = m.elements[9];3497var m10 = m.elements[10];3498var m11 = m.elements[11];3499var m12 = m.elements[12];3500var m13 = m.elements[13];3501var m14 = m.elements[14];3502var m15 = m.elements[15];3503
3504var result = [];3505result[0] = this0 * m0 + this1 * m4 + this2 * m8 + this3 * m12;3506result[1] = this0 * m1 + this1 * m5 + this2 * m9 + this3 * m13;3507result[2] = this0 * m2 + this1 * m6 + this2 * m10 + this3 * m14;3508result[3] = this0 * m3 + this1 * m7 + this2 * m11 + this3 * m15;3509result[4] = this4 * m0 + this5 * m4 + this6 * m8 + this7 * m12;3510result[5] = this4 * m1 + this5 * m5 + this6 * m9 + this7 * m13;3511result[6] = this4 * m2 + this5 * m6 + this6 * m10 + this7 * m14;3512result[7] = this4 * m3 + this5 * m7 + this6 * m11 + this7 * m15;3513result[8] = this8 * m0 + this9 * m4 + this10 * m8 + this11 * m12;3514result[9] = this8 * m1 + this9 * m5 + this10 * m9 + this11 * m13;3515result[10] = this8 * m2 + this9 * m6 + this10 * m10 + this11 * m14;3516result[11] = this8 * m3 + this9 * m7 + this10 * m11 + this11 * m15;3517result[12] = this12 * m0 + this13 * m4 + this14 * m8 + this15 * m12;3518result[13] = this12 * m1 + this13 * m5 + this14 * m9 + this15 * m13;3519result[14] = this12 * m2 + this13 * m6 + this14 * m10 + this15 * m14;3520result[15] = this12 * m3 + this13 * m7 + this14 * m11 + this15 * m15;3521return new CSG.Matrix4x4(result);3522},3523
3524clone: function() {3525var elements = this.elements.map(function(p) {3526return p;3527});3528return new CSG.Matrix4x4(elements);3529},3530
3531// Right multiply the matrix by a CSG.Vector3D (interpreted as 3 row, 1 column)3532// (result = M*v)3533// Fourth element is taken as 13534rightMultiply1x3Vector: function(v) {3535var v0 = v._x;3536var v1 = v._y;3537var v2 = v._z;3538var v3 = 1;3539var x = v0 * this.elements[0] + v1 * this.elements[1] + v2 * this.elements[2] + v3 * this.elements[3];3540var y = v0 * this.elements[4] + v1 * this.elements[5] + v2 * this.elements[6] + v3 * this.elements[7];3541var z = v0 * this.elements[8] + v1 * this.elements[9] + v2 * this.elements[10] + v3 * this.elements[11];3542var w = v0 * this.elements[12] + v1 * this.elements[13] + v2 * this.elements[14] + v3 * this.elements[15];3543// scale such that fourth element becomes 1:3544if(w != 1) {3545var invw = 1.0 / w;3546x *= invw;3547y *= invw;3548z *= invw;3549}3550return new CSG.Vector3D(x, y, z);3551},3552
3553// Multiply a CSG.Vector3D (interpreted as 3 column, 1 row) by this matrix3554// (result = v*M)3555// Fourth element is taken as 13556leftMultiply1x3Vector: function(v) {3557var v0 = v._x;3558var v1 = v._y;3559var v2 = v._z;3560var v3 = 1;3561var x = v0 * this.elements[0] + v1 * this.elements[4] + v2 * this.elements[8] + v3 * this.elements[12];3562var y = v0 * this.elements[1] + v1 * this.elements[5] + v2 * this.elements[9] + v3 * this.elements[13];3563var z = v0 * this.elements[2] + v1 * this.elements[6] + v2 * this.elements[10] + v3 * this.elements[14];3564var w = v0 * this.elements[3] + v1 * this.elements[7] + v2 * this.elements[11] + v3 * this.elements[15];3565// scale such that fourth element becomes 1:3566if(w != 1) {3567var invw = 1.0 / w;3568x *= invw;3569y *= invw;3570z *= invw;3571}3572return new CSG.Vector3D(x, y, z);3573},3574
3575// Right multiply the matrix by a CSG.Vector2D (interpreted as 2 row, 1 column)3576// (result = M*v)3577// Fourth element is taken as 13578rightMultiply1x2Vector: function(v) {3579var v0 = v.x;3580var v1 = v.y;3581var v2 = 0;3582var v3 = 1;3583var x = v0 * this.elements[0] + v1 * this.elements[1] + v2 * this.elements[2] + v3 * this.elements[3];3584var y = v0 * this.elements[4] + v1 * this.elements[5] + v2 * this.elements[6] + v3 * this.elements[7];3585var z = v0 * this.elements[8] + v1 * this.elements[9] + v2 * this.elements[10] + v3 * this.elements[11];3586var w = v0 * this.elements[12] + v1 * this.elements[13] + v2 * this.elements[14] + v3 * this.elements[15];3587// scale such that fourth element becomes 1:3588if(w != 1) {3589var invw = 1.0 / w;3590x *= invw;3591y *= invw;3592z *= invw;3593}3594return new CSG.Vector2D(x, y);3595},3596
3597// Multiply a CSG.Vector2D (interpreted as 2 column, 1 row) by this matrix3598// (result = v*M)3599// Fourth element is taken as 13600leftMultiply1x2Vector: function(v) {3601var v0 = v.x;3602var v1 = v.y;3603var v2 = 0;3604var v3 = 1;3605var x = v0 * this.elements[0] + v1 * this.elements[4] + v2 * this.elements[8] + v3 * this.elements[12];3606var y = v0 * this.elements[1] + v1 * this.elements[5] + v2 * this.elements[9] + v3 * this.elements[13];3607var z = v0 * this.elements[2] + v1 * this.elements[6] + v2 * this.elements[10] + v3 * this.elements[14];3608var w = v0 * this.elements[3] + v1 * this.elements[7] + v2 * this.elements[11] + v3 * this.elements[15];3609// scale such that fourth element becomes 1:3610if(w != 1) {3611var invw = 1.0 / w;3612x *= invw;3613y *= invw;3614z *= invw;3615}3616return new CSG.Vector2D(x, y);3617},3618
3619// determine whether this matrix is a mirroring transformation3620isMirroring: function() {3621var u = new CSG.Vector3D(this.elements[0], this.elements[4], this.elements[8]);3622var v = new CSG.Vector3D(this.elements[1], this.elements[5], this.elements[9]);3623var w = new CSG.Vector3D(this.elements[2], this.elements[6], this.elements[10]);3624
3625// for a true orthogonal, non-mirrored base, u.cross(v) == w3626// If they have an opposite direction then we are mirroring3627var mirrorvalue = u.cross(v).dot(w);3628var ismirror = (mirrorvalue < 0);3629return ismirror;3630}3631};3632
3633// return the unity matrix
3634CSG.Matrix4x4.unity = function() {3635return new CSG.Matrix4x4();3636};3637
3638// Create a rotation matrix for rotating around the x axis
3639CSG.Matrix4x4.rotationX = function(degrees) {3640var radians = degrees * Math.PI * (1.0 / 180.0);3641var cos = Math.cos(radians);3642var sin = Math.sin(radians);3643var els = [36441, 0, 0, 0, 0, cos, sin, 0, 0, -sin, cos, 0, 0, 0, 0, 1];3645return new CSG.Matrix4x4(els);3646};3647
3648// Create a rotation matrix for rotating around the y axis
3649CSG.Matrix4x4.rotationY = function(degrees) {3650var radians = degrees * Math.PI * (1.0 / 180.0);3651var cos = Math.cos(radians);3652var sin = Math.sin(radians);3653var els = [3654cos, 0, -sin, 0, 0, 1, 0, 0, sin, 0, cos, 0, 0, 0, 0, 1];3655return new CSG.Matrix4x4(els);3656};3657
3658// Create a rotation matrix for rotating around the z axis
3659CSG.Matrix4x4.rotationZ = function(degrees) {3660var radians = degrees * Math.PI * (1.0 / 180.0);3661var cos = Math.cos(radians);3662var sin = Math.sin(radians);3663var els = [3664cos, sin, 0, 0, -sin, cos, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];3665return new CSG.Matrix4x4(els);3666};3667
3668// Matrix for rotation about arbitrary point and axis
3669CSG.Matrix4x4.rotation = function(rotationCenter, rotationAxis, degrees) {3670rotationCenter = new CSG.Vector3D(rotationCenter);3671rotationAxis = new CSG.Vector3D(rotationAxis);3672var rotationPlane = CSG.Plane.fromNormalAndPoint(rotationAxis, rotationCenter);3673var orthobasis = new CSG.OrthoNormalBasis(rotationPlane);3674var transformation = CSG.Matrix4x4.translation(rotationCenter.negated());3675transformation = transformation.multiply(orthobasis.getProjectionMatrix());3676transformation = transformation.multiply(CSG.Matrix4x4.rotationZ(degrees));3677transformation = transformation.multiply(orthobasis.getInverseProjectionMatrix());3678transformation = transformation.multiply(CSG.Matrix4x4.translation(rotationCenter));3679return transformation;3680};3681
3682// Create an affine matrix for translation:
3683CSG.Matrix4x4.translation = function(v) {3684// parse as CSG.Vector3D, so we can pass an array or a CSG.Vector3D3685var vec = new CSG.Vector3D(v);3686var els = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, vec.x, vec.y, vec.z, 1];3687return new CSG.Matrix4x4(els);3688};3689
3690// Create an affine matrix for mirroring into an arbitrary plane:
3691CSG.Matrix4x4.mirroring = function(plane) {3692var nx = plane.normal.x;3693var ny = plane.normal.y;3694var nz = plane.normal.z;3695var w = plane.w;3696var els = [3697(1.0 - 2.0 * nx * nx), (-2.0 * ny * nx), (-2.0 * nz * nx), 0,3698(-2.0 * nx * ny), (1.0 - 2.0 * ny * ny), (-2.0 * nz * ny), 0,3699(-2.0 * nx * nz), (-2.0 * ny * nz), (1.0 - 2.0 * nz * nz), 0,3700(-2.0 * nx * w), (-2.0 * ny * w), (-2.0 * nz * w), 13701];3702return new CSG.Matrix4x4(els);3703};3704
3705// Create an affine matrix for scaling:
3706CSG.Matrix4x4.scaling = function(v) {3707// parse as CSG.Vector3D, so we can pass an array or a CSG.Vector3D3708var vec = new CSG.Vector3D(v);3709var els = [3710vec.x, 0, 0, 0, 0, vec.y, 0, 0, 0, 0, vec.z, 0, 0, 0, 0, 1];3711return new CSG.Matrix4x4(els);3712};3713
3714///////////////////////////////////////////////////
3715// # class Vector2D:
3716// Represents a 2 element vector
3717CSG.Vector2D = function(x, y) {3718if(arguments.length == 2) {3719this._x = parseFloat(x);3720this._y = parseFloat(y);3721} else {3722var ok = true;3723if(arguments.length == 1) {3724if(typeof(x) == "object") {3725if(x instanceof CSG.Vector2D) {3726this._x = x._x;3727this._y = x._y;3728} else if(x instanceof Array) {3729this._x = parseFloat(x[0]);3730this._y = parseFloat(x[1]);3731} else if(('x' in x) && ('y' in x)) {3732this._x = parseFloat(x.x);3733this._y = parseFloat(x.y);3734} else ok = false;3735} else {3736var v = parseFloat(x);3737this._x = v;3738this._y = v;3739}3740} else ok = false;3741if(ok) {3742if((!CSG.IsFloat(this._x)) || (!CSG.IsFloat(this._y))) ok = false;3743}3744if(!ok) {3745throw new Error("wrong arguments");3746}3747}3748};3749
3750CSG.Vector2D.fromAngle = function(radians) {3751return CSG.Vector2D.fromAngleRadians(radians);3752};3753
3754CSG.Vector2D.fromAngleDegrees = function(degrees) {3755var radians = Math.PI * degrees / 180;3756return CSG.Vector2D.fromAngleRadians(radians);3757};3758
3759CSG.Vector2D.fromAngleRadians = function(radians) {3760return new CSG.Vector2D(Math.cos(radians), Math.sin(radians));3761};3762
3763CSG.Vector2D.prototype = {3764get x() {3765return this._x;3766}, get y() {3767return this._y;3768},3769
3770set x(v) {3771throw new Error("Vector2D is immutable");3772}, set y(v) {3773throw new Error("Vector2D is immutable");3774},3775
3776// extend to a 3D vector by adding a z coordinate:3777toVector3D: function(z) {3778return new CSG.Vector3D(this._x, this._y, z);3779},3780
3781equals: function(a) {3782return(this._x == a._x) && (this._y == a._y);3783},3784
3785clone: function() {3786return new CSG.Vector2D(this._x, this._y);3787},3788
3789negated: function() {3790return new CSG.Vector2D(-this._x, -this._y);3791},3792
3793plus: function(a) {3794return new CSG.Vector2D(this._x + a._x, this._y + a._y);3795},3796
3797minus: function(a) {3798return new CSG.Vector2D(this._x - a._x, this._y - a._y);3799},3800
3801times: function(a) {3802return new CSG.Vector2D(this._x * a, this._y * a);3803},3804
3805dividedBy: function(a) {3806return new CSG.Vector2D(this._x / a, this._y / a);3807},3808
3809dot: function(a) {3810return this._x * a._x + this._y * a._y;3811},3812
3813lerp: function(a, t) {3814return this.plus(a.minus(this).times(t));3815},3816
3817length: function() {3818return Math.sqrt(this.dot(this));3819},3820
3821distanceTo: function(a) {3822return this.minus(a).length();3823},3824
3825distanceToSquared: function(a) {3826return this.minus(a).lengthSquared();3827},3828
3829lengthSquared: function() {3830return this.dot(this);3831},3832
3833unit: function() {3834return this.dividedBy(this.length());3835},3836
3837cross: function(a) {3838return this._x * a._y - this._y * a._x;3839},3840
3841// returns the vector rotated by 90 degrees clockwise3842normal: function() {3843return new CSG.Vector2D(this._y, -this._x);3844},3845
3846// Right multiply by a 4x4 matrix (the vector is interpreted as a row vector)3847// Returns a new CSG.Vector2D3848multiply4x4: function(matrix4x4) {3849return matrix4x4.leftMultiply1x2Vector(this);3850},3851
3852transform: function(matrix4x4) {3853return matrix4x4.leftMultiply1x2Vector(this);3854},3855
3856angle: function() {3857return this.angleRadians();3858},3859
3860angleDegrees: function() {3861var radians = this.angleRadians();3862return 180 * radians / Math.PI;3863},3864
3865angleRadians: function() {3866// y=sin, x=cos3867return Math.atan2(this._y, this._x);3868},3869
3870min: function(p) {3871return new CSG.Vector2D(3872Math.min(this._x, p._x), Math.min(this._y, p._y));3873},3874
3875max: function(p) {3876return new CSG.Vector2D(3877Math.max(this._x, p._x), Math.max(this._y, p._y));3878},3879
3880toString: function() {3881return "("+this._x.toFixed(2)+", "+this._y.toFixed(2)+")";3882}3883};3884
3885
3886// # class Line2D
3887// Represents a directional line in 2D space
3888// A line is parametrized by its normal vector (perpendicular to the line, rotated 90 degrees counter clockwise)
3889// and w. The line passes through the point <normal>.times(w).
3890// normal must be a unit vector!
3891// Equation: p is on line if normal.dot(p)==w
3892CSG.Line2D = function(normal, w) {3893normal = new CSG.Vector2D(normal);3894w = parseFloat(w);3895var l = normal.length();3896// normalize:3897w *= l;3898normal = normal.times(1.0 / l);3899this.normal = normal;3900this.w = w;3901};3902
3903CSG.Line2D.fromPoints = function(p1, p2) {3904p1 = new CSG.Vector2D(p1);3905p2 = new CSG.Vector2D(p2);3906var direction = p2.minus(p1);3907var normal = direction.normal().negated().unit();3908var w = p1.dot(normal);3909return new CSG.Line2D(normal, w);3910};3911
3912CSG.Line2D.prototype = {3913// same line but opposite direction:3914reverse: function() {3915return new CSG.Line2D(this.normal.negated(), -this.w);3916},3917
3918equals: function(l) {3919return(l.normal.equals(this.normal) && (l.w == this.w));3920},3921
3922origin: function() {3923return this.normal.times(this.w);3924},3925
3926direction: function() {3927return this.normal.normal();3928},3929
3930xAtY: function(y) {3931// (py == y) && (normal * p == w)3932// -> px = (w - normal._y * y) / normal.x3933var x = (this.w - this.normal._y * y) / this.normal.x;3934return x;3935},3936
3937absDistanceToPoint: function(point) {3938point = new CSG.Vector2D(point);3939var point_projected = point.dot(this.normal);3940var distance = Math.abs(point_projected - this.w);3941return distance;3942},3943/*FIXME: has error - origin is not defined, the method is never used3944closestPoint: function(point) {
3945point = new CSG.Vector2D(point);
3946var vector = point.dot(this.direction());
3947return origin.plus(vector);
3948},
3949*/
3950
3951// intersection between two lines, returns point as Vector2D3952intersectWithLine: function(line2d) {3953var point = CSG.solve2Linear(this.normal.x, this.normal.y, line2d.normal.x, line2d.normal.y, this.w, line2d.w);3954point = new CSG.Vector2D(point); // make vector2d3955return point;3956},3957
3958transform: function(matrix4x4) {3959var origin = new CSG.Vector2D(0, 0);3960var pointOnPlane = this.normal.times(this.w);3961var neworigin = origin.multiply4x4(matrix4x4);3962var neworiginPlusNormal = this.normal.multiply4x4(matrix4x4);3963var newnormal = neworiginPlusNormal.minus(neworigin);3964var newpointOnPlane = pointOnPlane.multiply4x4(matrix4x4);3965var neww = newnormal.dot(newpointOnPlane);3966return new CSG.Line2D(newnormal, neww);3967}3968};3969
3970// # class Line3D
3971// Represents a line in 3D space
3972// direction must be a unit vector
3973// point is a random point on the line
3974CSG.Line3D = function(point, direction) {3975point = new CSG.Vector3D(point);3976direction = new CSG.Vector3D(direction);3977this.point = point;3978this.direction = direction.unit();3979};3980
3981CSG.Line3D.fromPoints = function(p1, p2) {3982p1 = new CSG.Vector3D(p1);3983p2 = new CSG.Vector3D(p2);3984var direction = p2.minus(p1).unit();3985return new CSG.Line3D(p1, direction);3986};3987
3988CSG.Line3D.fromPlanes = function(p1, p2) {3989var direction = p1.normal.cross(p2.normal);3990var l = direction.length();3991if(l < 1e-10) {3992throw new Error("Parallel planes");3993}3994direction = direction.times(1.0 / l);3995
3996var mabsx = Math.abs(direction.x);3997var mabsy = Math.abs(direction.y);3998var mabsz = Math.abs(direction.z);3999var origin;4000if((mabsx >= mabsy) && (mabsx >= mabsz)) {4001// direction vector is mostly pointing towards x4002// find a point p for which x is zero:4003var r = CSG.solve2Linear(p1.normal.y, p1.normal.z, p2.normal.y, p2.normal.z, p1.w, p2.w);4004origin = new CSG.Vector3D(0, r[0], r[1]);4005} else if((mabsy >= mabsx) && (mabsy >= mabsz)) {4006// find a point p for which y is zero:4007var r = CSG.solve2Linear(p1.normal.x, p1.normal.z, p2.normal.x, p2.normal.z, p1.w, p2.w);4008origin = new CSG.Vector3D(r[0], 0, r[1]);4009} else {4010// find a point p for which z is zero:4011var r = CSG.solve2Linear(p1.normal.x, p1.normal.y, p2.normal.x, p2.normal.y, p1.w, p2.w);4012origin = new CSG.Vector3D(r[0], r[1], 0);4013}4014return new CSG.Line3D(origin, direction);4015};4016
4017
4018CSG.Line3D.prototype = {4019intersectWithPlane: function(plane) {4020// plane: plane.normal * p = plane.w4021// line: p=line.point + labda * line.direction4022var labda = (plane.w - plane.normal.dot(this.point)) / plane.normal.dot(this.direction);4023var point = this.point.plus(this.direction.times(labda));4024return point;4025},4026
4027clone: function(line) {4028return new CSG.Line3D(this.point.clone(), this.direction.clone());4029},4030
4031reverse: function() {4032return new CSG.Line3D(this.point.clone(), this.direction.negated());4033},4034
4035transform: function(matrix4x4) {4036var newpoint = this.point.multiply4x4(matrix4x4);4037var pointPlusDirection = this.point.plus(this.direction);4038var newPointPlusDirection = pointPlusDirection.multiply4x4(matrix4x4);4039var newdirection = newPointPlusDirection.minus(newpoint);4040return new CSG.Line3D(newpoint, newdirection);4041},4042
4043closestPointOnLine: function(point) {4044point = new CSG.Vector3D(point);4045var t = point.minus(this.point).dot(this.direction) / this.direction.dot(this.direction);4046var closestpoint = this.point.plus(this.direction.times(t));4047return closestpoint;4048},4049
4050distanceToPoint: function(point) {4051point = new CSG.Vector3D(point);4052var closestpoint = this.closestPointOnLine(point);4053var distancevector = point.minus(closestpoint);4054var distance = distancevector.length();4055return distance;4056},4057
4058equals: function(line3d) {4059if(!this.direction.equals(line3d.direction)) return false;4060var distance = this.distanceToPoint(line3d.point);4061if(distance > 1e-8) return false;4062return true;4063}4064};4065
4066
4067// # class OrthoNormalBasis
4068// Reprojects points on a 3D plane onto a 2D plane
4069// or from a 2D plane back onto the 3D plane
4070CSG.OrthoNormalBasis = function(plane, rightvector) {4071if(arguments.length < 2) {4072// choose an arbitrary right hand vector, making sure it is somewhat orthogonal to the plane normal:4073rightvector = plane.normal.randomNonParallelVector();4074} else {4075rightvector = new CSG.Vector3D(rightvector);4076}4077this.v = plane.normal.cross(rightvector).unit();4078this.u = this.v.cross(plane.normal);4079this.plane = plane;4080this.planeorigin = plane.normal.times(plane.w);4081};4082
4083// The z=0 plane, with the 3D x and y vectors mapped to the 2D x and y vector
4084CSG.OrthoNormalBasis.Z0Plane = function() {4085var plane = new CSG.Plane(new CSG.Vector3D([0, 0, 1]), 0);4086return new CSG.OrthoNormalBasis(plane, new CSG.Vector3D([1, 0, 0]));4087};4088
4089CSG.OrthoNormalBasis.prototype = {4090getProjectionMatrix: function() {4091return new CSG.Matrix4x4([4092this.u.x, this.v.x, this.plane.normal.x, 0,4093this.u.y, this.v.y, this.plane.normal.y, 0,4094this.u.z, this.v.z, this.plane.normal.z, 0,40950, 0, -this.plane.w, 1]);4096},4097
4098getInverseProjectionMatrix: function() {4099var p = this.plane.normal.times(this.plane.w);4100return new CSG.Matrix4x4([4101this.u.x, this.u.y, this.u.z, 0,4102this.v.x, this.v.y, this.v.z, 0,4103this.plane.normal.x, this.plane.normal.y, this.plane.normal.z, 0,4104p.x, p.y, p.z, 1]);4105},4106
4107to2D: function(vec3) {4108return new CSG.Vector2D(vec3.dot(this.u), vec3.dot(this.v));4109},4110
4111to3D: function(vec2) {4112return this.planeorigin.plus(this.u.times(vec2.x)).plus(this.v.times(vec2.y));4113},4114
4115line3Dto2D: function(line3d) {4116var a = line3d.point;4117var b = line3d.direction.plus(a);4118var a2d = this.to2D(a);4119var b2d = this.to2D(b);4120return CSG.Line2D.fromPoints(a2d, b2d);4121},4122
4123line2Dto3D: function(line2d) {4124var a = line2d.origin();4125var b = line2d.direction().plus(a);4126var a3d = this.to3D(a);4127var b3d = this.to3D(b);4128return CSG.Line3D.fromPoints(a3d, b3d);4129},4130
4131transform: function(matrix4x4) {4132// todo: this may not work properly in case of mirroring4133var newplane = this.plane.transform(matrix4x4);4134var rightpoint_transformed = this.u.transform(matrix4x4);4135var origin_transformed = new CSG.Vector3D(0, 0, 0).transform(matrix4x4);4136var newrighthandvector = rightpoint_transformed.minus(origin_transformed);4137var newbasis = new CSG.OrthoNormalBasis(newplane, newrighthandvector);4138return newbasis;4139}4140};4141
4142function insertSorted(array, element, comparefunc) {4143var leftbound = 0;4144var rightbound = array.length;4145while(rightbound > leftbound) {4146var testindex = Math.floor((leftbound + rightbound) / 2);4147var testelement = array[testindex];4148var compareresult = comparefunc(element, testelement);4149if(compareresult > 0) // element > testelement4150{4151leftbound = testindex + 1;4152} else {4153rightbound = testindex;4154}4155}4156array.splice(leftbound, 0, element);4157}
4158
4159// Get the x coordinate of a point with a certain y coordinate, interpolated between two
4160// points (CSG.Vector2D).
4161// Interpolation is robust even if the points have the same y coordinate
4162CSG.interpolateBetween2DPointsForY = function(point1, point2, y) {4163var f1 = y - point1.y;4164var f2 = point2.y - point1.y;4165if(f2 < 0) {4166f1 = -f1;4167f2 = -f2;4168}4169var t;4170if(f1 <= 0) {4171t = 0.0;4172} else if(f1 >= f2) {4173t = 1.0;4174} else if(f2 < 1e-10) {4175t = 0.5;4176} else {4177t = f1 / f2;4178}4179var result = point1.x + t * (point2.x - point1.x);4180return result;4181};4182
4183// Retesselation function for a set of coplanar polygons. See the introduction at the top of
4184// this file.
4185CSG.reTesselateCoplanarPolygons = function(sourcepolygons, destpolygons) {4186var EPS = 1e-5;4187
4188var numpolygons = sourcepolygons.length;4189if(numpolygons > 0) {4190var plane = sourcepolygons[0].plane;4191var shared = sourcepolygons[0].shared;4192var orthobasis = new CSG.OrthoNormalBasis(plane);4193var polygonvertices2d = []; // array of array of CSG.Vector2D4194var polygontopvertexindexes = []; // array of indexes of topmost vertex per polygon4195var topy2polygonindexes = {};4196var ycoordinatetopolygonindexes = {};4197
4198var xcoordinatebins = {};4199var ycoordinatebins = {};4200
4201// convert all polygon vertices to 2D4202// Make a list of all encountered y coordinates4203// And build a map of all polygons that have a vertex at a certain y coordinate:4204var ycoordinateBinningFactor = 1.0 / EPS * 10;4205for(var polygonindex = 0; polygonindex < numpolygons; polygonindex++) {4206var poly3d = sourcepolygons[polygonindex];4207var vertices2d = [];4208var numvertices = poly3d.vertices.length;4209var minindex = -1;4210if(numvertices > 0) {4211var miny, maxy, maxindex;4212for(var i = 0; i < numvertices; i++) {4213var pos2d = orthobasis.to2D(poly3d.vertices[i].pos);4214// perform binning of y coordinates: If we have multiple vertices very4215// close to each other, give them the same y coordinate:4216var ycoordinatebin = Math.floor(pos2d.y * ycoordinateBinningFactor);4217var newy;4218if(ycoordinatebin in ycoordinatebins) {4219newy = ycoordinatebins[ycoordinatebin];4220} else if(ycoordinatebin + 1 in ycoordinatebins) {4221newy = ycoordinatebins[ycoordinatebin + 1];4222} else if(ycoordinatebin - 1 in ycoordinatebins) {4223newy = ycoordinatebins[ycoordinatebin - 1];4224} else {4225newy = pos2d.y;4226ycoordinatebins[ycoordinatebin] = pos2d.y;4227}4228pos2d = new CSG.Vector2D(pos2d.x, newy);4229vertices2d.push(pos2d);4230var y = pos2d.y;4231if((i === 0) || (y < miny)) {4232miny = y;4233minindex = i;4234}4235if((i === 0) || (y > maxy)) {4236maxy = y;4237maxindex = i;4238}4239if(!(y in ycoordinatetopolygonindexes)) {4240ycoordinatetopolygonindexes[y] = {};4241}4242ycoordinatetopolygonindexes[y][polygonindex] = true;4243}4244if(miny >= maxy) {4245// degenerate polygon, all vertices have same y coordinate. Just ignore it from now:4246vertices2d = [];4247} else {4248if(!(miny in topy2polygonindexes)) {4249topy2polygonindexes[miny] = [];4250}4251topy2polygonindexes[miny].push(polygonindex);4252}4253} // if(numvertices > 0)4254// reverse the vertex order:4255vertices2d.reverse();4256minindex = numvertices - minindex - 1;4257polygonvertices2d.push(vertices2d);4258polygontopvertexindexes.push(minindex);4259}4260var ycoordinates = [];4261for(var ycoordinate in ycoordinatetopolygonindexes) ycoordinates.push(ycoordinate);4262ycoordinates.sort(fnNumberSort);4263
4264// Now we will iterate over all y coordinates, from lowest to highest y coordinate4265// activepolygons: source polygons that are 'active', i.e. intersect with our y coordinate4266// Is sorted so the polygons are in left to right order4267// Each element in activepolygons has these properties:4268// polygonindex: the index of the source polygon (i.e. an index into the sourcepolygons4269// and polygonvertices2d arrays)4270// leftvertexindex: the index of the vertex at the left side of the polygon (lowest x)4271// that is at or just above the current y coordinate4272// rightvertexindex: dito at right hand side of polygon4273// topleft, bottomleft: coordinates of the left side of the polygon crossing the current y coordinate4274// topright, bottomright: coordinates of the right hand side of the polygon crossing the current y coordinate4275var activepolygons = [];4276var prevoutpolygonrow = [];4277for(var yindex = 0; yindex < ycoordinates.length; yindex++) {4278var newoutpolygonrow = [];4279var ycoordinate_as_string = ycoordinates[yindex];4280var ycoordinate = Number(ycoordinate_as_string);4281
4282// update activepolygons for this y coordinate:4283// - Remove any polygons that end at this y coordinate4284// - update leftvertexindex and rightvertexindex (which point to the current vertex index4285// at the the left and right side of the polygon4286// Iterate over all polygons that have a corner at this y coordinate:4287var polygonindexeswithcorner = ycoordinatetopolygonindexes[ycoordinate_as_string];4288for(var activepolygonindex = 0; activepolygonindex < activepolygons.length; ++activepolygonindex) {4289var activepolygon = activepolygons[activepolygonindex];4290var polygonindex = activepolygon.polygonindex;4291if(polygonindexeswithcorner[polygonindex]) {4292// this active polygon has a corner at this y coordinate:4293var vertices2d = polygonvertices2d[polygonindex];4294var numvertices = vertices2d.length;4295var newleftvertexindex = activepolygon.leftvertexindex;4296var newrightvertexindex = activepolygon.rightvertexindex;4297// See if we need to increase leftvertexindex or decrease rightvertexindex:4298while(true) {4299var nextleftvertexindex = newleftvertexindex + 1;4300if(nextleftvertexindex >= numvertices) nextleftvertexindex = 0;4301if(vertices2d[nextleftvertexindex].y != ycoordinate) break;4302newleftvertexindex = nextleftvertexindex;4303}4304var nextrightvertexindex = newrightvertexindex - 1;4305if(nextrightvertexindex < 0) nextrightvertexindex = numvertices - 1;4306if(vertices2d[nextrightvertexindex].y == ycoordinate) {4307newrightvertexindex = nextrightvertexindex;4308}4309if((newleftvertexindex != activepolygon.leftvertexindex) && (newleftvertexindex == newrightvertexindex)) {4310// We have increased leftvertexindex or decreased rightvertexindex, and now they point to the same vertex4311// This means that this is the bottom point of the polygon. We'll remove it:4312activepolygons.splice(activepolygonindex, 1);4313--activepolygonindex;4314} else {4315activepolygon.leftvertexindex = newleftvertexindex;4316activepolygon.rightvertexindex = newrightvertexindex;4317activepolygon.topleft = vertices2d[newleftvertexindex];4318activepolygon.topright = vertices2d[newrightvertexindex];4319var nextleftvertexindex = newleftvertexindex + 1;4320if(nextleftvertexindex >= numvertices) nextleftvertexindex = 0;4321activepolygon.bottomleft = vertices2d[nextleftvertexindex];4322var nextrightvertexindex = newrightvertexindex - 1;4323if(nextrightvertexindex < 0) nextrightvertexindex = numvertices - 1;4324activepolygon.bottomright = vertices2d[nextrightvertexindex];4325}4326} // if polygon has corner here4327} // for activepolygonindex4328var nextycoordinate;4329if(yindex >= ycoordinates.length - 1) {4330// last row, all polygons must be finished here:4331activepolygons = [];4332nextycoordinate = null;4333} else // yindex < ycoordinates.length-14334{4335nextycoordinate = Number(ycoordinates[yindex + 1]);4336var middleycoordinate = 0.5 * (ycoordinate + nextycoordinate);4337// update activepolygons by adding any polygons that start here:4338var startingpolygonindexes = topy2polygonindexes[ycoordinate_as_string];4339for(var polygonindex_key in startingpolygonindexes) {4340var polygonindex = startingpolygonindexes[polygonindex_key];4341var vertices2d = polygonvertices2d[polygonindex];4342var numvertices = vertices2d.length;4343var topvertexindex = polygontopvertexindexes[polygonindex];4344// the top of the polygon may be a horizontal line. In that case topvertexindex can point to any point on this line.4345// Find the left and right topmost vertices which have the current y coordinate:4346var topleftvertexindex = topvertexindex;4347while(true) {4348var i = topleftvertexindex + 1;4349if(i >= numvertices) i = 0;4350if(vertices2d[i].y != ycoordinate) break;4351if(i == topvertexindex) break; // should not happen, but just to prevent endless loops4352topleftvertexindex = i;4353}4354var toprightvertexindex = topvertexindex;4355while(true) {4356var i = toprightvertexindex - 1;4357if(i < 0) i = numvertices - 1;4358if(vertices2d[i].y != ycoordinate) break;4359if(i == topleftvertexindex) break; // should not happen, but just to prevent endless loops4360toprightvertexindex = i;4361}4362var nextleftvertexindex = topleftvertexindex + 1;4363if(nextleftvertexindex >= numvertices) nextleftvertexindex = 0;4364var nextrightvertexindex = toprightvertexindex - 1;4365if(nextrightvertexindex < 0) nextrightvertexindex = numvertices - 1;4366var newactivepolygon = {4367polygonindex: polygonindex,4368leftvertexindex: topleftvertexindex,4369rightvertexindex: toprightvertexindex,4370topleft: vertices2d[topleftvertexindex],4371topright: vertices2d[toprightvertexindex],4372bottomleft: vertices2d[nextleftvertexindex],4373bottomright: vertices2d[nextrightvertexindex],4374};4375insertSorted(activepolygons, newactivepolygon, function(el1, el2) {4376var x1 = CSG.interpolateBetween2DPointsForY(4377el1.topleft, el1.bottomleft, middleycoordinate);4378var x2 = CSG.interpolateBetween2DPointsForY(4379el2.topleft, el2.bottomleft, middleycoordinate);4380if(x1 > x2) return 1;4381if(x1 < x2) return -1;4382return 0;4383});4384} // for(var polygonindex in startingpolygonindexes)4385} // yindex < ycoordinates.length-14386//if( (yindex == ycoordinates.length-1) || (nextycoordinate - ycoordinate > EPS) )4387if(true) {4388// Now activepolygons is up to date4389// Build the output polygons for the next row in newoutpolygonrow:4390for(var activepolygon_key in activepolygons) {4391var activepolygon = activepolygons[activepolygon_key];4392var polygonindex = activepolygon.polygonindex;4393var vertices2d = polygonvertices2d[polygonindex];4394var numvertices = vertices2d.length;4395
4396var x = CSG.interpolateBetween2DPointsForY(activepolygon.topleft, activepolygon.bottomleft, ycoordinate);4397var topleft = new CSG.Vector2D(x, ycoordinate);4398x = CSG.interpolateBetween2DPointsForY(activepolygon.topright, activepolygon.bottomright, ycoordinate);4399var topright = new CSG.Vector2D(x, ycoordinate);4400x = CSG.interpolateBetween2DPointsForY(activepolygon.topleft, activepolygon.bottomleft, nextycoordinate);4401var bottomleft = new CSG.Vector2D(x, nextycoordinate);4402x = CSG.interpolateBetween2DPointsForY(activepolygon.topright, activepolygon.bottomright, nextycoordinate);4403var bottomright = new CSG.Vector2D(x, nextycoordinate);4404var outpolygon = {4405topleft: topleft,4406topright: topright,4407bottomleft: bottomleft,4408bottomright: bottomright,4409leftline: CSG.Line2D.fromPoints(topleft, bottomleft),4410rightline: CSG.Line2D.fromPoints(bottomright, topright)4411};4412if(newoutpolygonrow.length > 0) {4413var prevoutpolygon = newoutpolygonrow[newoutpolygonrow.length - 1];4414var d1 = outpolygon.topleft.distanceTo(prevoutpolygon.topright);4415var d2 = outpolygon.bottomleft.distanceTo(prevoutpolygon.bottomright);4416if((d1 < EPS) && (d2 < EPS)) {4417// we can join this polygon with the one to the left:4418outpolygon.topleft = prevoutpolygon.topleft;4419outpolygon.leftline = prevoutpolygon.leftline;4420outpolygon.bottomleft = prevoutpolygon.bottomleft;4421newoutpolygonrow.splice(newoutpolygonrow.length - 1, 1);4422}4423}4424newoutpolygonrow.push(outpolygon);4425} // for(activepolygon in activepolygons)4426if(yindex > 0) {4427// try to match the new polygons against the previous row:4428var prevcontinuedindexes = {};4429var matchedindexes = {};4430for(var i = 0; i < newoutpolygonrow.length; i++) {4431var thispolygon = newoutpolygonrow[i];4432for(var ii = 0; ii < prevoutpolygonrow.length; ii++) {4433if(!matchedindexes[ii]) // not already processed?4434{4435// We have a match if the sidelines are equal or if the top coordinates4436// are on the sidelines of the previous polygon4437var prevpolygon = prevoutpolygonrow[ii];4438if(prevpolygon.bottomleft.distanceTo(thispolygon.topleft) < EPS) {4439if(prevpolygon.bottomright.distanceTo(thispolygon.topright) < EPS) {4440// Yes, the top of this polygon matches the bottom of the previous:4441matchedindexes[ii] = true;4442// Now check if the joined polygon would remain convex:4443var d1 = thispolygon.leftline.direction().x - prevpolygon.leftline.direction().x;4444var d2 = thispolygon.rightline.direction().x - prevpolygon.rightline.direction().x;4445var leftlinecontinues = Math.abs(d1) < EPS;4446var rightlinecontinues = Math.abs(d2) < EPS;4447var leftlineisconvex = leftlinecontinues || (d1 >= 0);4448var rightlineisconvex = rightlinecontinues || (d2 >= 0);4449if(leftlineisconvex && rightlineisconvex) {4450// yes, both sides have convex corners:4451// This polygon will continue the previous polygon4452thispolygon.outpolygon = prevpolygon.outpolygon;4453thispolygon.leftlinecontinues = leftlinecontinues;4454thispolygon.rightlinecontinues = rightlinecontinues;4455prevcontinuedindexes[ii] = true;4456}4457break;4458}4459}4460} // if(!prevcontinuedindexes[ii])4461} // for ii4462} // for i4463for(var ii = 0; ii < prevoutpolygonrow.length; ii++) {4464if(!prevcontinuedindexes[ii]) {4465// polygon ends here4466// Finish the polygon with the last point(s):4467var prevpolygon = prevoutpolygonrow[ii];4468prevpolygon.outpolygon.rightpoints.push(prevpolygon.bottomright);4469if(prevpolygon.bottomright.distanceTo(prevpolygon.bottomleft) > EPS) {4470// polygon ends with a horizontal line:4471prevpolygon.outpolygon.leftpoints.push(prevpolygon.bottomleft);4472}4473// reverse the left half so we get a counterclockwise circle:4474prevpolygon.outpolygon.leftpoints.reverse();4475var points2d = prevpolygon.outpolygon.rightpoints.concat(prevpolygon.outpolygon.leftpoints);4476var vertices3d = [];4477points2d.map(function(point2d) {4478var point3d = orthobasis.to3D(point2d);4479var vertex3d = new CSG.Vertex(point3d);4480vertices3d.push(vertex3d);4481});4482var polygon = new CSG.Polygon(vertices3d, shared, plane);4483destpolygons.push(polygon);4484}4485}4486} // if(yindex > 0)4487for(var i = 0; i < newoutpolygonrow.length; i++) {4488var thispolygon = newoutpolygonrow[i];4489if(!thispolygon.outpolygon) {4490// polygon starts here:4491thispolygon.outpolygon = {4492leftpoints: [],4493rightpoints: []4494};4495thispolygon.outpolygon.leftpoints.push(thispolygon.topleft);4496if(thispolygon.topleft.distanceTo(thispolygon.topright) > EPS) {4497// we have a horizontal line at the top:4498thispolygon.outpolygon.rightpoints.push(thispolygon.topright);4499}4500} else {4501// continuation of a previous row4502if(!thispolygon.leftlinecontinues) {4503thispolygon.outpolygon.leftpoints.push(thispolygon.topleft);4504}4505if(!thispolygon.rightlinecontinues) {4506thispolygon.outpolygon.rightpoints.push(thispolygon.topright);4507}4508}4509}4510prevoutpolygonrow = newoutpolygonrow;4511}4512} // for yindex4513} // if(numpolygons > 0)4514};4515
4516////////////////////////////////
4517// ## class fuzzyFactory
4518// This class acts as a factory for objects. We can search for an object with approximately
4519// the desired properties (say a rectangle with width 2 and height 1)
4520// The lookupOrCreate() method looks for an existing object (for example it may find an existing rectangle
4521// with width 2.0001 and height 0.999. If no object is found, the user supplied callback is
4522// called, which should generate a new object. The new object is inserted into the database
4523// so it can be found by future lookupOrCreate() calls.
4524// Constructor:
4525// numdimensions: the number of parameters for each object
4526// for example for a 2D rectangle this would be 2
4527// tolerance: The maximum difference for each parameter allowed to be considered a match
4528CSG.fuzzyFactory = function(numdimensions, tolerance) {4529var lookuptable = [];4530for(var i = 0; i < numdimensions; i++) {4531lookuptable.push({});4532}4533this.lookuptable = lookuptable;4534this.nextElementId = 1;4535this.multiplier = 1.0 / tolerance;4536this.objectTable = {};4537};4538
4539CSG.fuzzyFactory.prototype = {4540// var obj = f.lookupOrCreate([el1, el2, el3], function(elements) {/* create the new object */});4541// Performs a fuzzy lookup of the object with the specified elements.4542// If found, returns the existing object4543// If not found, calls the supplied callback function which should create a new object with4544// the specified properties. This object is inserted in the lookup database.4545lookupOrCreate: function(els, creatorCallback) {4546var object;4547var key = this.lookupKey(els);4548if(key === null) {4549object = creatorCallback(els);4550key = this.nextElementId++;4551this.objectTable[key] = object;4552for(var dimension = 0; dimension < els.length; dimension++) {4553var elementLookupTable = this.lookuptable[dimension];4554var value = els[dimension];4555var valueMultiplied = value * this.multiplier;4556var valueQuantized1 = Math.floor(valueMultiplied);4557var valueQuantized2 = Math.ceil(valueMultiplied);4558CSG.fuzzyFactory.insertKey(key, elementLookupTable, valueQuantized1);4559CSG.fuzzyFactory.insertKey(key, elementLookupTable, valueQuantized2);4560}4561} else {4562object = this.objectTable[key];4563}4564return object;4565},4566
4567// ----------- PRIVATE METHODS:4568lookupKey: function(els) {4569var keyset = {};4570for(var dimension = 0; dimension < els.length; dimension++) {4571var elementLookupTable = this.lookuptable[dimension];4572var value = els[dimension];4573var valueQuantized = Math.round(value * this.multiplier);4574valueQuantized += "";4575if(valueQuantized in elementLookupTable) {4576if(dimension === 0) {4577keyset = elementLookupTable[valueQuantized];4578} else {4579keyset = CSG.fuzzyFactory.intersectSets(keyset, elementLookupTable[valueQuantized]);4580}4581} else {4582return null;4583}4584if(CSG.fuzzyFactory.isEmptySet(keyset)) return null;4585}4586// return first matching key:4587for(var key in keyset) return key;4588return null;4589},4590
4591lookupKeySetForDimension: function(dimension, value) {4592var result;4593var elementLookupTable = this.lookuptable[dimension];4594var valueMultiplied = value * this.multiplier;4595var valueQuantized = Math.floor(value * this.multiplier);4596if(valueQuantized in elementLookupTable) {4597result = elementLookupTable[valueQuantized];4598} else {4599result = {};4600}4601return result;4602}4603};4604
4605CSG.fuzzyFactory.insertKey = function(key, lookuptable, quantizedvalue) {4606if(quantizedvalue in lookuptable) {4607lookuptable[quantizedvalue][key] = true;4608} else {4609var newset = {};4610newset[key] = true;4611lookuptable[quantizedvalue] = newset;4612}4613};4614
4615CSG.fuzzyFactory.isEmptySet = function(obj) {4616for(var key in obj) return false;4617return true;4618};4619
4620CSG.fuzzyFactory.intersectSets = function(set1, set2) {4621var result = {};4622for(var key in set1) {4623if(key in set2) {4624result[key] = true;4625}4626}4627return result;4628};4629
4630CSG.fuzzyFactory.joinSets = function(set1, set2) {4631var result = {}, key;4632for(key in set1) {4633result[key] = true;4634}4635for(key in set2) {4636result[key] = true;4637}4638return result;4639};4640
4641//////////////////////////////////////
4642CSG.fuzzyCSGFactory = function() {4643this.vertexfactory = new CSG.fuzzyFactory(3, 1e-5);4644this.planefactory = new CSG.fuzzyFactory(4, 1e-5);4645this.polygonsharedfactory = {};4646};4647
4648CSG.fuzzyCSGFactory.prototype = {4649getPolygonShared: function(sourceshared) {4650var hash = sourceshared.getHash();4651if(hash in this.polygonsharedfactory) {4652return this.polygonsharedfactory[hash];4653} else {4654this.polygonsharedfactory[hash] = sourceshared;4655return sourceshared;4656}4657},4658
4659getVertex: function(sourcevertex) {4660var elements = [sourcevertex.pos._x, sourcevertex.pos._y, sourcevertex.pos._z];4661var result = this.vertexfactory.lookupOrCreate(elements, function(els) {4662return sourcevertex;4663});4664return result;4665},4666
4667getPlane: function(sourceplane) {4668var elements = [sourceplane.normal._x, sourceplane.normal._y, sourceplane.normal._z, sourceplane.w];4669var result = this.planefactory.lookupOrCreate(elements, function(els) {4670return sourceplane;4671});4672return result;4673},4674
4675getPolygon: function(sourcepolygon) {4676var newplane = this.getPlane(sourcepolygon.plane);4677var newshared = this.getPolygonShared(sourcepolygon.shared);4678var _this = this;4679var newvertices = sourcepolygon.vertices.map(function(vertex) {4680return _this.getVertex(vertex);4681});4682return new CSG.Polygon(newvertices, newshared, newplane);4683},4684
4685getCSG: function(sourcecsg) {4686var _this = this;4687var newpolygons = sourcecsg.polygons.map(function(polygon) {4688return _this.getPolygon(polygon);4689});4690return CSG.fromPolygons(newpolygons);4691}4692};4693
4694//////////////////////////////////////
4695// Tag factory: we can request a unique tag through CSG.getTag()
4696CSG.staticTag = 1;4697
4698CSG.getTag = function() {4699return CSG.staticTag++;4700};4701
4702//////////////////////////////////////
4703// # Class Properties
4704// This class is used to store properties of a solid
4705// A property can for example be a CSG.Vertex, a CSG.Plane or a CSG.Line3D
4706// Whenever an affine transform is applied to the CSG solid, all its properties are
4707// transformed as well.
4708// The properties can be stored in a complex nested structure (using arrays and objects)
4709CSG.Properties = function() {};4710
4711CSG.Properties.prototype = {4712_transform: function(matrix4x4) {4713var result = new CSG.Properties();4714CSG.Properties.transformObj(this, result, matrix4x4);4715return result;4716},4717_merge: function(otherproperties) {4718var result = new CSG.Properties();4719CSG.Properties.cloneObj(this, result);4720CSG.Properties.addFrom(result, otherproperties);4721return result;4722}4723};4724
4725CSG.Properties.transformObj = function(source, result, matrix4x4) {4726for(var propertyname in source) {4727if(propertyname == "_transform") continue;4728if(propertyname == "_merge") continue;4729var propertyvalue = source[propertyname];4730var transformed = propertyvalue;4731if(typeof(propertyvalue) == "object") {4732if(('transform' in propertyvalue) && (typeof(propertyvalue.transform) == "function")) {4733transformed = propertyvalue.transform(matrix4x4);4734} else if(propertyvalue instanceof Array) {4735transformed = [];4736CSG.Properties.transformObj(propertyvalue, transformed, matrix4x4);4737} else if(propertyvalue instanceof CSG.Properties) {4738transformed = new CSG.Properties();4739CSG.Properties.transformObj(propertyvalue, transformed, matrix4x4);4740}4741}4742result[propertyname] = transformed;4743}4744};4745
4746CSG.Properties.cloneObj = function(source, result) {4747for(var propertyname in source) {4748if(propertyname == "_transform") continue;4749if(propertyname == "_merge") continue;4750var propertyvalue = source[propertyname];4751var cloned = propertyvalue;4752if(typeof(propertyvalue) == "object") {4753if(propertyvalue instanceof Array) {4754cloned = [];4755for(var i = 0; i < propertyvalue.length; i++) {4756cloned.push(propertyvalue[i]);4757}4758} else if(propertyvalue instanceof CSG.Properties) {4759cloned = new CSG.Properties();4760CSG.Properties.cloneObj(propertyvalue, cloned);4761}4762}4763result[propertyname] = cloned;4764}4765};4766
4767CSG.Properties.addFrom = function(result, otherproperties) {4768for(var propertyname in otherproperties) {4769if(propertyname == "_transform") continue;4770if(propertyname == "_merge") continue;4771if((propertyname in result) &&4772(typeof(result[propertyname]) == "object") &&4773(result[propertyname] instanceof CSG.Properties) &&4774(typeof(otherproperties[propertyname]) == "object") &&4775(otherproperties[propertyname] instanceof CSG.Properties)) {4776CSG.Properties.addFrom(result[propertyname], otherproperties[propertyname]);4777} else if(!(propertyname in result)) {4778result[propertyname] = otherproperties[propertyname];4779}4780}4781};4782
4783//////////////////////////////////////
4784// # class Connector
4785// A connector allows to attach two objects at predefined positions
4786// For example a servo motor and a servo horn:
4787// Both can have a Connector called 'shaft'
4788// The horn can be moved and rotated such that the two connectors match
4789// and the horn is attached to the servo motor at the proper position.
4790// Connectors are stored in the properties of a CSG solid so they are
4791// ge the same transformations applied as the solid
4792CSG.Connector = function(point, axisvector, normalvector) {4793this.point = new CSG.Vector3D(point);4794this.axisvector = new CSG.Vector3D(axisvector).unit();4795this.normalvector = new CSG.Vector3D(normalvector).unit();4796};4797
4798CSG.Connector.prototype = {4799normalized: function() {4800var axisvector = this.axisvector.unit();4801// make the normal vector truly normal:4802var n = this.normalvector.cross(axisvector).unit();4803var normalvector = axisvector.cross(n);4804return new CSG.Connector(this.point, axisvector, normalvector);4805},4806
4807transform: function(matrix4x4) {4808var point = this.point.multiply4x4(matrix4x4);4809var axisvector = this.point.plus(this.axisvector).multiply4x4(matrix4x4).minus(point);4810var normalvector = this.point.plus(this.normalvector).multiply4x4(matrix4x4).minus(point);4811return new CSG.Connector(point, axisvector, normalvector);4812},4813
4814// Get the transformation matrix to connect this Connector to another connector4815// other: a CSG.Connector to which this connector should be connected4816// mirror: false: the 'axis' vectors of the connectors should point in the same direction4817// true: the 'axis' vectors of the connectors should point in opposite direction4818// normalrotation: degrees of rotation between the 'normal' vectors of the two4819// connectors4820getTransformationTo: function(other, mirror, normalrotation) {4821mirror = mirror ? true : false;4822normalrotation = normalrotation ? Number(normalrotation) : 0;4823var us = this.normalized();4824other = other.normalized();4825// shift to the origin:4826var transformation = CSG.Matrix4x4.translation(this.point.negated());4827// construct the plane crossing through the origin and the two axes:4828var axesplane = CSG.Plane.anyPlaneFromVector3Ds(4829new CSG.Vector3D(0, 0, 0), us.axisvector, other.axisvector);4830var axesbasis = new CSG.OrthoNormalBasis(axesplane);4831var angle1 = axesbasis.to2D(us.axisvector).angle();4832var angle2 = axesbasis.to2D(other.axisvector).angle();4833var rotation = 180.0 * (angle2 - angle1) / Math.PI;4834if(mirror) rotation += 180.0;4835transformation = transformation.multiply(axesbasis.getProjectionMatrix());4836transformation = transformation.multiply(CSG.Matrix4x4.rotationZ(rotation));4837transformation = transformation.multiply(axesbasis.getInverseProjectionMatrix());4838var usAxesAligned = us.transform(transformation);4839// Now we have done the transformation for aligning the axes.4840// We still need to align the normals:4841var normalsplane = CSG.Plane.fromNormalAndPoint(other.axisvector, new CSG.Vector3D(0, 0, 0));4842var normalsbasis = new CSG.OrthoNormalBasis(normalsplane);4843angle1 = normalsbasis.to2D(usAxesAligned.normalvector).angle();4844angle2 = normalsbasis.to2D(other.normalvector).angle();4845rotation = 180.0 * (angle2 - angle1) / Math.PI;4846rotation += normalrotation;4847transformation = transformation.multiply(normalsbasis.getProjectionMatrix());4848transformation = transformation.multiply(CSG.Matrix4x4.rotationZ(rotation));4849transformation = transformation.multiply(normalsbasis.getInverseProjectionMatrix());4850// and translate to the destination point:4851transformation = transformation.multiply(CSG.Matrix4x4.translation(other.point));4852var usAligned = us.transform(transformation);4853return transformation;4854},4855
4856axisLine: function() {4857return new CSG.Line3D(this.point, this.axisvector);4858},4859
4860// creates a new Connector, with the connection point moved in the direction of the axisvector4861extend: function(distance) {4862var newpoint = this.point.plus(this.axisvector.unit().times(distance));4863return new CSG.Connector(newpoint, this.axisvector, this.normalvector);4864}4865};4866
4867
4868//////////////////////////////////////
4869// # Class Path2D
4870CSG.Path2D = function(points, closed) {4871closed = !! closed;4872points = points || [];4873// re-parse the points into CSG.Vector2D4874// and remove any duplicate points4875var prevpoint = null;4876if(closed && (points.length > 0)) {4877prevpoint = new CSG.Vector2D(points[points.length - 1]);4878}4879var newpoints = [];4880points.map(function(point) {4881point = new CSG.Vector2D(point);4882var skip = false;4883if(prevpoint !== null) {4884var distance = point.distanceTo(prevpoint);4885skip = distance < 1e-5;4886}4887if(!skip) newpoints.push(point);4888prevpoint = point;4889});4890this.points = newpoints;4891this.closed = closed;4892};4893
4894/*
4895Construct a (part of a) circle. Parameters:
4896options.center: the center point of the arc (CSG.Vector2D or array [x,y])
4897options.radius: the circle radius (float)
4898options.startangle: the starting angle of the arc, in degrees
48990 degrees corresponds to [1,0]
490090 degrees to [0,1]
4901and so on
4902options.endangle: the ending angle of the arc, in degrees
4903options.resolution: number of points per 360 degree of rotation
4904options.maketangent: adds two extra tiny line segments at both ends of the circle
4905this ensures that the gradients at the edges are tangent to the circle
4906Returns a CSG.Path2D. The path is not closed (even if it is a 360 degree arc).
4907close() the resultin path if you want to create a true circle.
4908*/
4909CSG.Path2D.arc = function(options) {4910var center = CSG.parseOptionAs2DVector(options, "center", 0);4911var radius = CSG.parseOptionAsFloat(options, "radius", 1);4912var startangle = CSG.parseOptionAsFloat(options, "startangle", 0);4913var endangle = CSG.parseOptionAsFloat(options, "endangle", 360);4914var resolution = CSG.parseOptionAsFloat(options, "resolution", CSG.defaultResolution2D);4915var maketangent = CSG.parseOptionAsBool(options, "maketangent", false);4916// no need to make multiple turns:4917while(endangle - startangle >= 720) {4918endangle -= 360;4919}4920while(endangle - startangle <= -720) {4921endangle += 360;4922}4923var points = [], point;4924var absangledif = Math.abs(endangle - startangle);4925if(absangledif < 1e-5) {4926point = CSG.Vector2D.fromAngle(startangle / 180.0 * Math.PI).times(radius);4927points.push(point.plus(center));4928} else {4929var numsteps = Math.floor(resolution * absangledif / 360) + 1;4930var edgestepsize = numsteps * 0.5 / absangledif; // step size for half a degree4931if(edgestepsize > 0.25) edgestepsize = 0.25;4932var numsteps_mod = maketangent ? (numsteps + 2) : numsteps;4933for(var i = 0; i <= numsteps_mod; i++) {4934var step = i;4935if(maketangent) {4936step = (i - 1) * (numsteps - 2 * edgestepsize) / numsteps + edgestepsize;4937if(step < 0) step = 0;4938if(step > numsteps) step = numsteps;4939}4940var angle = startangle + step * (endangle - startangle) / numsteps;4941point = CSG.Vector2D.fromAngle(angle / 180.0 * Math.PI).times(radius);4942points.push(point.plus(center));4943}4944}4945return new CSG.Path2D(points, false);4946};4947
4948CSG.Path2D.prototype = {4949concat: function(otherpath) {4950if(this.closed || otherpath.closed) {4951throw new Error("Paths must not be closed");4952}4953var newpoints = this.points.concat(otherpath.points);4954return new CSG.Path2D(newpoints);4955},4956
4957appendPoint: function(point) {4958if(this.closed) {4959throw new Error("Paths must not be closed");4960}4961var newpoints = this.points.concat([point]);4962return new CSG.Path2D(newpoints);4963},4964
4965close: function() {4966return new CSG.Path2D(this.points, true);4967},4968
4969// Extrude the path by following it with a rectangle (upright, perpendicular to the path direction)4970// Returns a CSG solid4971// width: width of the extrusion, in the z=0 plane4972// height: height of the extrusion in the z direction4973// resolution: number of segments per 360 degrees for the curve in a corner4974rectangularExtrude: function(width, height, resolution) {4975var cag = this.expandToCAG(width / 2, resolution);4976var result = cag.extrude({4977offset: [0, 0, height]4978});4979return result;4980},4981
4982// Expand the path to a CAG4983// This traces the path with a circle with radius pathradius4984expandToCAG: function(pathradius, resolution) {4985var sides = [];4986var numpoints = this.points.length;4987var startindex = 0;4988if(this.closed && (numpoints > 2)) startindex = -1;4989var prevvertex;4990for(var i = startindex; i < numpoints; i++) {4991var pointindex = i;4992if(pointindex < 0) pointindex = numpoints - 1;4993var point = this.points[pointindex];4994var vertex = new CAG.Vertex(point);4995if(i > startindex) {4996var side = new CAG.Side(prevvertex, vertex);4997sides.push(side);4998}4999prevvertex = vertex;5000}5001var shellcag = CAG.fromSides(sides);5002var expanded = shellcag.expandedShell(pathradius, resolution);5003return expanded;5004},5005
5006innerToCAG: function() {5007if(!this.closed) throw new Error("The path should be closed!");5008return CAG.fromPoints(this.points);5009},5010
5011transform: function(matrix4x4) {5012var newpoints = this.points.map(function(point) {5013return point.multiply4x4(matrix4x4);5014});5015return new CSG.Path2D(newpoints, this.closed);5016}5017};5018
5019// Add several convenience methods to the classes that support a transform() method:
5020CSG.addTransformationMethodsToPrototype = function(prot) {5021prot.mirrored = function(plane) {5022return this.transform(CSG.Matrix4x4.mirroring(plane));5023};5024
5025prot.mirroredX = function() {5026var plane = new CSG.Plane(new CSG.Vector3D(1, 0, 0), 0);5027return this.mirrored(plane);5028};5029
5030prot.mirroredY = function() {5031var plane = new CSG.Plane(new CSG.Vector3D(0, 1, 0), 0);5032return this.mirrored(plane);5033};5034
5035prot.mirroredZ = function() {5036var plane = new CSG.Plane(new CSG.Vector3D(0, 0, 1), 0);5037return this.mirrored(plane);5038};5039
5040prot.translate = function(v) {5041return this.transform(CSG.Matrix4x4.translation(v));5042};5043
5044prot.scale = function(f) {5045return this.transform(CSG.Matrix4x4.scaling(f));5046};5047
5048prot.rotateX = function(deg) {5049return this.transform(CSG.Matrix4x4.rotationX(deg));5050};5051
5052prot.rotateY = function(deg) {5053return this.transform(CSG.Matrix4x4.rotationY(deg));5054};5055
5056prot.rotateZ = function(deg) {5057return this.transform(CSG.Matrix4x4.rotationZ(deg));5058};5059
5060prot.rotate = function(rotationCenter, rotationAxis, degrees) {5061return this.transform(CSG.Matrix4x4.rotation(rotationCenter, rotationAxis, degrees));5062};5063};5064
5065//////////////////
5066// CAG: solid area geometry: like CSG but 2D
5067// Each area consists of a number of sides
5068// Each side is a line between 2 points
5069var CAG = function() {5070this.sides = [];5071};5072
5073// Construct a CAG from a list of `CAG.Side` instances.
5074CAG.fromSides = function(sides) {5075var cag = new CAG();5076cag.sides = sides;5077return cag;5078};5079
5080// Construct a CAG from a list of points (a polygon)
5081// Rotation direction of the points is not relevant. Points can be a convex or concave polygon.
5082// Polygon must not self intersect
5083CAG.fromPoints = function(points) {5084var numpoints = points.length;5085if(numpoints < 3) throw new Error("CAG shape needs at least 3 points");5086var sides = [];5087var prevpoint = new CSG.Vector2D(points[numpoints - 1]);5088var prevvertex = new CAG.Vertex(prevpoint);5089points.map(function(p) {5090var point = new CSG.Vector2D(p);5091var vertex = new CAG.Vertex(point);5092var side = new CAG.Side(prevvertex, vertex);5093sides.push(side);5094prevvertex = vertex;5095});5096var result = CAG.fromSides(sides);5097if(result.isSelfIntersecting()) {5098throw new Error("Polygon is self intersecting!");5099}5100var area = result.area();5101if(Math.abs(area) < 1e-5) {5102throw new Error("Degenerate polygon!");5103}5104if(area < 0) {5105result = result.flipped();5106}5107result = result.canonicalized();5108return result;5109};5110
5111// Like CAG.fromPoints but does not check if it's a valid polygon.
5112// Points should rotate counter clockwise
5113CAG.fromPointsNoCheck = function(points) {5114var sides = [];5115var prevpoint = new CSG.Vector2D(points[points.length - 1]);5116var prevvertex = new CAG.Vertex(prevpoint);5117points.map(function(p) {5118var point = new CSG.Vector2D(p);5119var vertex = new CAG.Vertex(point);5120var side = new CAG.Side(prevvertex, vertex);5121sides.push(side);5122prevvertex = vertex;5123});5124return CAG.fromSides(sides);5125};5126
5127// Converts a CSG to a CAG. The CSG must consist of polygons with only z coordinates +1 and -1
5128// as constructed by CAG.toCSG(-1, 1). This is so we can use the 3D union(), intersect() etc
5129CAG.fromFakeCSG = function(csg) {5130var sides = csg.polygons.map(function(p) {5131return CAG.Side.fromFakePolygon(p);5132});5133return CAG.fromSides(sides);5134};5135
5136// see if the line between p0start and p0end intersects with the line between p1start and p1end
5137// returns true if the lines strictly intersect, the end points are not counted!
5138CAG.linesIntersect = function(p0start, p0end, p1start, p1end) {5139if(p0end.equals(p1start) || p1end.equals(p0start)) {5140var d = p1end.minus(p1start).unit().plus(p0end.minus(p0start).unit()).length();5141if(d < 1e-5) {5142return true;5143}5144} else {5145var d0 = p0end.minus(p0start);5146var d1 = p1end.minus(p1start);5147if(Math.abs(d0.cross(d1)) < 1e-9) return false; // lines are parallel5148var alphas = CSG.solve2Linear(-d0.x, d1.x, -d0.y, d1.y, p0start.x - p1start.x, p0start.y - p1start.y);5149if((alphas[0] > 1e-6) && (alphas[0] < 0.999999) && (alphas[1] > 1e-5) && (alphas[1] < 0.999999)) return true;5150// if( (alphas[0] >= 0) && (alphas[0] <= 1) && (alphas[1] >= 0) && (alphas[1] <= 1) ) return true;5151}5152return false;5153};5154
5155/* Construct a circle
5156options:
5157center: a 2D center point
5158radius: a scalar
5159resolution: number of sides per 360 degree rotation
5160returns a CAG object
5161*/
5162CAG.circle = function(options) {5163options = options || {};5164var center = CSG.parseOptionAs2DVector(options, "center", [0, 0]);5165var radius = CSG.parseOptionAsFloat(options, "radius", 1);5166var resolution = CSG.parseOptionAsInt(options, "resolution", CSG.defaultResolution2D);5167var sides = [];5168var prevvertex;5169for(var i = 0; i <= resolution; i++) {5170var radians = 2 * Math.PI * i / resolution;5171var point = CSG.Vector2D.fromAngleRadians(radians).times(radius).plus(center);5172var vertex = new CAG.Vertex(point);5173if(i > 0) {5174sides.push(new CAG.Side(prevvertex, vertex));5175}5176prevvertex = vertex;5177}5178return CAG.fromSides(sides);5179};5180
5181/* Construct a rectangle
5182options:
5183center: a 2D center point
5184radius: a 2D vector with width and height
5185returns a CAG object
5186*/
5187CAG.rectangle = function(options) {5188options = options || {};5189var c = CSG.parseOptionAs2DVector(options, "center", [0, 0]);5190var r = CSG.parseOptionAs2DVector(options, "radius", [1, 1]);5191var rswap = new CSG.Vector2D(r.x, -r.y);5192var points = [5193c.plus(r), c.plus(rswap), c.minus(r), c.minus(rswap)];5194return CAG.fromPoints(points);5195};5196
5197// var r = CSG.roundedRectangle({
5198// center: [0, 0],
5199// radius: [2, 1],
5200// roundradius: 0.2,
5201// resolution: 8,
5202// });
5203CAG.roundedRectangle = function(options) {5204options = options || {};5205var center = CSG.parseOptionAs2DVector(options, "center", [0, 0]);5206var radius = CSG.parseOptionAs2DVector(options, "radius", [1, 1]);5207var roundradius = CSG.parseOptionAsFloat(options, "roundradius", 0.2);5208var resolution = CSG.parseOptionAsFloat(options, "resolution", CSG.defaultResolution2D);5209var maxroundradius = Math.min(radius.x, radius.y);5210maxroundradius -= 0.1;5211roundradius = Math.min(roundradius, maxroundradius);5212roundradius = Math.max(0, roundradius);5213radius = new CSG.Vector2D(radius.x - roundradius, radius.y - roundradius);5214var rect = CAG.rectangle({5215center: center,5216radius: radius5217});5218if(roundradius > 0) {5219rect = rect.expand(roundradius, resolution);5220}5221return rect;5222};5223
5224// Reconstruct a CAG from the output of toCompactBinary()
5225CAG.fromCompactBinary = function(bin) {5226if(bin['class'] != "CAG") throw new Error("Not a CAG");5227var vertices = [];5228var vertexData = bin.vertexData;5229var numvertices = vertexData.length / 2;5230var arrayindex = 0;5231for(var vertexindex = 0; vertexindex < numvertices; vertexindex++) {5232var x = vertexData[arrayindex++];5233var y = vertexData[arrayindex++];5234var pos = new CSG.Vector2D(x, y);5235var vertex = new CAG.Vertex(pos);5236vertices.push(vertex);5237}5238
5239var sides = [];5240var numsides = bin.sideVertexIndices.length / 2;5241arrayindex = 0;5242for(var sideindex = 0; sideindex < numsides; sideindex++) {5243var vertexindex0 = bin.sideVertexIndices[arrayindex++];5244var vertexindex1 = bin.sideVertexIndices[arrayindex++];5245var side = new CAG.Side(vertices[vertexindex0], vertices[vertexindex1]);5246sides.push(side);5247}5248var cag = CAG.fromSides(sides);5249cag.isCanonicalized = true;5250return cag;5251};5252
5253function fnSortByIndex(a, b) {5254return a.index - b.index;5255}
5256
5257CAG.prototype = {5258toString: function() {5259var result = "CAG (" + this.sides.length + " sides):\n";5260this.sides.map(function(side) {5261result += " " + side.toString() + "\n";5262});5263return result;5264},5265
5266toCSG: function(z0, z1) {5267var polygons = this.sides.map(function(side) {5268return side.toPolygon3D(z0, z1);5269});5270return CSG.fromPolygons(polygons);5271},5272
5273toDebugString1: function() {5274this.sides.sort(function(a, b) {5275return a.vertex0.pos.x - b.vertex0.pos.x;5276});5277var str = "";5278this.sides.map(function(side) {5279str += "(" + side.vertex0.pos.x + "," + side.vertex0.pos.y + ") - (" + side.vertex1.pos.x + "," + side.vertex1.pos.y + ")\n";5280});5281return str;5282},5283
5284toDebugString: function() {5285// this.sides.sort(function(a,b){5286// return a.vertex0.pos.x - b.vertex0.pos.x;5287// });5288var str = "CAG.fromSides([\n";5289this.sides.map(function(side) {5290str += " new CAG.Side(new CAG.Vertex(new CSG.Vector2D(" +5291side.vertex0.pos.x + "," + side.vertex0.pos.y +5292")), new CAG.Vertex(new CSG.Vector2D(" +5293side.vertex1.pos.x + "," + side.vertex1.pos.y + "))),\n";5294});5295str += "]);\n";5296return str;5297},5298
5299union: function(cag) {5300var cags;5301if(cag instanceof Array) {5302cags = cag;5303} else {5304cags = [cag];5305}5306var r = this.toCSG(-1, 1);5307cags.map(function(cag) {5308r = r.unionSub(cag.toCSG(-1, 1), false, false);5309});5310r = r.reTesselated();5311r = r.canonicalized();5312cag = CAG.fromFakeCSG(r);5313var cag_canonicalized = cag.canonicalized();5314return cag_canonicalized;5315},5316
5317subtract: function(cag) {5318var cags;5319if(cag instanceof Array) {5320cags = cag;5321} else {5322cags = [cag];5323}5324var r = this.toCSG(-1, 1);5325cags.map(function(cag) {5326r = r.subtractSub(cag.toCSG(-1, 1), false, false);5327});5328r = r.reTesselated();5329r = r.canonicalized();5330r = CAG.fromFakeCSG(r);5331r = r.canonicalized();5332return r;5333},5334
5335intersect: function(cag) {5336var cags;5337if(cag instanceof Array) {5338cags = cag;5339} else {5340cags = [cag];5341}5342var r = this.toCSG(-1, 1);5343cags.map(function(cag) {5344r = r.intersectSub(cag.toCSG(-1, 1), false, false);5345});5346r = r.reTesselated();5347r = r.canonicalized();5348r = CAG.fromFakeCSG(r);5349r = r.canonicalized();5350return r;5351},5352
5353transform: function(matrix4x4) {5354var ismirror = matrix4x4.isMirroring();5355var newsides = this.sides.map(function(side) {5356return side.transform(matrix4x4);5357});5358var result = CAG.fromSides(newsides);5359if(ismirror) {5360result = result.flipped();5361}5362return result;5363},5364
5365// see http://local.wasp.uwa.edu.au/~pbourke/geometry/polyarea/ :5366// Area of the polygon. For a counter clockwise rotating polygon the area is positive, otherwise negative5367area: function() {5368var polygonArea = 0;5369this.sides.map(function(side) {5370polygonArea += side.vertex0.pos.cross(side.vertex1.pos);5371});5372polygonArea *= 0.5;5373return polygonArea;5374},5375
5376flipped: function() {5377var newsides = this.sides.map(function(side) {5378return side.flipped();5379});5380newsides.reverse();5381return CAG.fromSides(newsides);5382},5383
5384getBounds: function() {5385var minpoint;5386if(this.sides.length === 0) {5387minpoint = new CSG.Vector2D(0, 0);5388} else {5389minpoint = this.sides[0].vertex0.pos;5390}5391var maxpoint = minpoint;5392this.sides.map(function(side) {5393minpoint = minpoint.min(side.vertex0.pos);5394minpoint = minpoint.min(side.vertex1.pos);5395maxpoint = maxpoint.max(side.vertex0.pos);5396maxpoint = maxpoint.max(side.vertex1.pos);5397});5398return [minpoint, maxpoint];5399},5400
5401center: function(c) {5402if(!c.length) c = [c,c];5403var b = this.getBounds();5404return this.translate([5405c[0]?-(b[1].x-b[0].x)/2-b[0].x:0,5406c[1]?-(b[1].y-b[0].y)/2-b[0].y:0]);5407},5408
5409isSelfIntersecting: function() {5410var numsides = this.sides.length;5411for(var i = 0; i < numsides; i++) {5412var side0 = this.sides[i];5413for(var ii = i + 1; ii < numsides; ii++) {5414var side1 = this.sides[ii];5415if(CAG.linesIntersect(side0.vertex0.pos, side0.vertex1.pos, side1.vertex0.pos, side1.vertex1.pos)) {5416return true;5417}5418}5419}5420return false;5421},5422
5423expandedShell: function(radius, resolution) {5424resolution = resolution || 8;5425if(resolution < 4) resolution = 4;5426var cags = [];5427var pointmap = {};5428var cag = this.canonicalized();5429cag.sides.map(function(side) {5430var d = side.vertex1.pos.minus(side.vertex0.pos);5431var dl = d.length();5432if(dl > 1e-5) {5433d = d.times(1.0 / dl);5434var normal = d.normal().times(radius);5435var shellpoints = [5436side.vertex1.pos.plus(normal),5437side.vertex1.pos.minus(normal),5438side.vertex0.pos.minus(normal),5439side.vertex0.pos.plus(normal)5440];5441// var newcag = CAG.fromPointsNoCheck(shellpoints);5442var newcag = CAG.fromPoints(shellpoints);5443cags.push(newcag);5444for(var step = 0; step < 2; step++) {5445var p1 = (step === 0) ? side.vertex0.pos : side.vertex1.pos;5446var p2 = (step === 0) ? side.vertex1.pos : side.vertex0.pos;5447var tag = p1.x + " " + p1.y;5448if(!(tag in pointmap)) {5449pointmap[tag] = [];5450}5451pointmap[tag].push({5452"p1": p1,5453"p2": p25454});5455}5456}5457});5458for(var tag in pointmap) {5459var m = pointmap[tag];5460var angle1, angle2;5461var pcenter = m[0].p1;5462if(m.length == 2) {5463var end1 = m[0].p2;5464var end2 = m[1].p2;5465angle1 = end1.minus(pcenter).angleDegrees();5466angle2 = end2.minus(pcenter).angleDegrees();5467if(angle2 < angle1) angle2 += 360;5468if(angle2 >= (angle1 + 360)) angle2 -= 360;5469if(angle2 < angle1 + 180) {5470var t = angle2;5471angle2 = angle1 + 360;5472angle1 = t;5473}5474angle1 += 90;5475angle2 -= 90;5476} else {5477angle1 = 0;5478angle2 = 360;5479}5480var fullcircle = (angle2 > angle1 + 359.999);5481if(fullcircle) {5482angle1 = 0;5483angle2 = 360;5484}5485if(angle2 > (angle1 + 1e-5)) {5486var points = [];5487if(!fullcircle) {5488points.push(pcenter);5489}5490var numsteps = Math.round(resolution * (angle2 - angle1) / 360);5491if(numsteps < 1) numsteps = 1;5492for(var step = 0; step <= numsteps; step++) {5493var angle = angle1 + step / numsteps * (angle2 - angle1);5494if(step == numsteps) angle = angle2; // prevent rounding errors5495var point = pcenter.plus(CSG.Vector2D.fromAngleDegrees(angle).times(radius));5496if((!fullcircle) || (step > 0)) {5497points.push(point);5498}5499}5500var newcag = CAG.fromPointsNoCheck(points);5501cags.push(newcag);5502}5503}5504var result = new CAG();5505result = result.union(cags);5506return result;5507},5508
5509expand: function(radius, resolution) {5510var result = this.union(this.expandedShell(radius, resolution));5511return result;5512},5513
5514contract: function(radius, resolution) {5515var result = this.subtract(this.expandedShell(radius, resolution));5516return result;5517},5518
5519// extruded=cag.extrude({offset: [0,0,10], twistangle: 360, twiststeps: 100});5520// linear extrusion of 2D shape, with optional twist5521// The 2d shape is placed in in z=0 plane and extruded into direction <offset> (a CSG.Vector3D)5522// The final face is rotated <twistangle> degrees. Rotation is done around the origin of the 2d shape (i.e. x=0, y=0)5523// twiststeps determines the resolution of the twist (should be >= 1)5524// returns a CSG object5525extrude: function(options) {5526if(this.sides.length == 0) {5527// empty!5528return new CSG();5529}5530var offsetvector = CSG.parseOptionAs3DVector(options, "offset", [0,0,1]);5531var twistangle = CSG.parseOptionAsFloat(options, "twistangle", 0);5532var twiststeps = CSG.parseOptionAsInt(options, "twiststeps", 10);5533
5534if(twistangle == 0) twiststeps = 1;5535if(twiststeps < 1) twiststeps = 1;5536
5537var newpolygons = [];5538var prevtransformedcag;5539var prevstepz;5540for(var step=0; step <= twiststeps; step++) {5541var stepfraction = step / twiststeps;5542var transformedcag = this;5543var angle = twistangle * stepfraction;5544if(angle != 0) {5545transformedcag = transformedcag.rotateZ(angle);5546}5547var translatevector = new CSG.Vector2D(offsetvector.x, offsetvector.y).times(stepfraction);5548transformedcag = transformedcag.translate(translatevector);5549var bounds = transformedcag.getBounds();5550bounds[0] = bounds[0].minus(new CSG.Vector2D(1,1));5551bounds[1] = bounds[1].plus(new CSG.Vector2D(1,1));5552var stepz = offsetvector.z * stepfraction;5553if( (step == 0) || (step == twiststeps) ) {5554// bottom or top face:5555var csgshell = transformedcag.toCSG(stepz-1, stepz+1);5556var csgplane = CSG.fromPolygons([new CSG.Polygon([5557new CSG.Vertex(new CSG.Vector3D(bounds[0].x, bounds[0].y, stepz)),5558new CSG.Vertex(new CSG.Vector3D(bounds[1].x, bounds[0].y, stepz)),5559new CSG.Vertex(new CSG.Vector3D(bounds[1].x, bounds[1].y, stepz)),5560new CSG.Vertex(new CSG.Vector3D(bounds[0].x, bounds[1].y, stepz))5561])]);5562var flip = (step == 0);5563if(offsetvector.z < 0) flip = !flip;5564if(flip) {5565csgplane = csgplane.inverse();5566}5567csgplane = csgplane.intersect(csgshell);5568// only keep the polygons in the z plane:5569csgplane.polygons.map(function(polygon){5570if(Math.abs(polygon.plane.normal.z) > 0.99) {5571newpolygons.push(polygon);5572}5573});5574}5575if(step > 0) {5576var numsides = transformedcag.sides.length;5577for(var sideindex = 0; sideindex < numsides; sideindex++) {5578var thisside = transformedcag.sides[sideindex];5579var prevside = prevtransformedcag.sides[sideindex];5580var p1 = new CSG.Polygon([5581new CSG.Vertex(thisside.vertex1.pos.toVector3D(stepz)),5582new CSG.Vertex(thisside.vertex0.pos.toVector3D(stepz)),5583new CSG.Vertex(prevside.vertex0.pos.toVector3D(prevstepz))5584]);5585var p2 = new CSG.Polygon([5586new CSG.Vertex(thisside.vertex1.pos.toVector3D(stepz)),5587new CSG.Vertex(prevside.vertex0.pos.toVector3D(prevstepz)),5588new CSG.Vertex(prevside.vertex1.pos.toVector3D(prevstepz))5589]);5590if(offsetvector.z < 0) {5591p1 = p1.flipped();5592p2 = p2.flipped();5593}5594newpolygons.push(p1);5595newpolygons.push(p2);5596}5597}5598prevtransformedcag = transformedcag;5599prevstepz = stepz;5600} // for step5601return CSG.fromPolygons(newpolygons);5602},5603
5604// check if we are a valid CAG (for debugging)5605check: function() {5606var errors = [];5607if(this.isSelfIntersecting()) {5608errors.push("Self intersects");5609}5610var pointcount = {};5611this.sides.map(function(side) {5612function mappoint(p) {5613var tag = p.x + " " + p.y;5614if(!(tag in pointcount)) pointcount[tag] = 0;5615pointcount[tag]++;5616}5617mappoint(side.vertex0.pos);5618mappoint(side.vertex1.pos);5619});5620for(var tag in pointcount) {5621var count = pointcount[tag];5622if(count & 1) {5623errors.push("Uneven number of sides (" + count + ") for point " + tag);5624}5625}5626var area = this.area();5627if(area < 1e-5) {5628errors.push("Area is " + area);5629}5630if(errors.length > 0) {5631var ertxt = "";5632errors.map(function(err) {5633ertxt += err + "\n";5634});5635throw new Error(ertxt);5636}5637},5638
5639canonicalized: function() {5640if(this.isCanonicalized) {5641return this;5642} else {5643var factory = new CAG.fuzzyCAGFactory();5644var result = factory.getCAG(this);5645result.isCanonicalized = true;5646return result;5647}5648},5649
5650toCompactBinary: function() {5651var cag = this.canonicalized();5652var numsides = cag.sides.length;5653var vertexmap = {};5654var vertices = [];5655var numvertices = 0;5656var sideVertexIndices = new Uint32Array(2 * numsides);5657var sidevertexindicesindex = 0;5658cag.sides.map(function(side) {5659[side.vertex0, side.vertex1].map(function(v) {5660var vertextag = v.getTag();5661var vertexindex;5662if(!(vertextag in vertexmap)) {5663vertexindex = numvertices++;5664vertexmap[vertextag] = vertexindex;5665vertices.push(v);5666} else {5667vertexindex = vertexmap[vertextag];5668}5669sideVertexIndices[sidevertexindicesindex++] = vertexindex;5670});5671});5672var vertexData = new Float64Array(numvertices * 2);5673var verticesArrayIndex = 0;5674vertices.map(function(v) {5675var pos = v.pos;5676vertexData[verticesArrayIndex++] = pos._x;5677vertexData[verticesArrayIndex++] = pos._y;5678});5679var result = {5680'class': "CAG",5681sideVertexIndices: sideVertexIndices,5682vertexData: vertexData5683};5684return result;5685},5686
5687getOutlinePaths: function() {5688var cag = this.canonicalized();5689var sideTagToSideMap = {};5690var startVertexTagToSideTagMap = {};5691cag.sides.map(function(side) {5692var sidetag = side.getTag();5693sideTagToSideMap[sidetag] = side;5694var startvertextag = side.vertex0.getTag();5695if(!(startvertextag in startVertexTagToSideTagMap)) {5696startVertexTagToSideTagMap[startvertextag] = [];5697}5698startVertexTagToSideTagMap[startvertextag].push(sidetag);5699});5700var paths = [];5701while(true) {5702var startsidetag = null;5703for(var aVertexTag in startVertexTagToSideTagMap) {5704var sidesForThisVertex = startVertexTagToSideTagMap[aVertexTag];5705startsidetag = sidesForThisVertex[0];5706sidesForThisVertex.splice(0, 1);5707if(sidesForThisVertex.length === 0) {5708delete startVertexTagToSideTagMap[aVertexTag];5709}5710break;5711}5712if(startsidetag === null) break; // we've had all sides5713var connectedVertexPoints = [];5714var sidetag = startsidetag;5715var thisside = sideTagToSideMap[sidetag];5716var startvertextag = thisside.vertex0.getTag();5717while(true) {5718connectedVertexPoints.push(thisside.vertex0.pos);5719var nextvertextag = thisside.vertex1.getTag();5720if(nextvertextag == startvertextag) break; // we've closed the polygon5721if(!(nextvertextag in startVertexTagToSideTagMap)) {5722throw new Error("Area is not closed!");5723}5724var nextpossiblesidetags = startVertexTagToSideTagMap[nextvertextag];5725var nextsideindex = -1;5726if(nextpossiblesidetags.length == 1) {5727nextsideindex = 0;5728} else {5729// more than one side starting at the same vertex. This means we have5730// two shapes touching at the same corner5731var bestangle = null;5732var thisangle = thisside.direction().angleDegrees();5733for(var sideindex = 0; sideindex < nextpossiblesidetags.length; sideindex++) {5734var nextpossiblesidetag = nextpossiblesidetags[sideindex];5735var possibleside = sideTagToSideMap[nextpossiblesidetag];5736var angle = possibleside.direction().angleDegrees();5737var angledif = angle - thisangle;5738if(angledif < -180) angledif += 360;5739if(angledif >= 180) angledif -= 360;5740if((nextsideindex < 0) || (angledif > bestangle)) {5741nextsideindex = sideindex;5742bestangle = angledif;5743}5744}5745}5746var nextsidetag = nextpossiblesidetags[nextsideindex];5747nextpossiblesidetags.splice(nextsideindex, 1);5748if(nextpossiblesidetags.length === 0) {5749delete startVertexTagToSideTagMap[nextvertextag];5750}5751thisside = sideTagToSideMap[nextsidetag];5752} // inner loop5753var path = new CSG.Path2D(connectedVertexPoints, true);5754paths.push(path);5755} // outer loop5756return paths;5757},5758
5759toDxf: function() {5760var paths = this.getOutlinePaths();5761return CAG.PathsToDxf(paths);5762}5763};5764
5765CAG.PathsToDxf = function(paths) {5766var str = "999\nDXF generated by OpenJsCad\n";5767str += " 0\nSECTION\n 2\nHEADER\n";5768str += " 0\nENDSEC\n";5769str += " 0\nSECTION\n 2\nTABLES\n";5770str += " 0\nTABLE\n 2\nLTYPE\n 70\n1\n";5771str += " 0\nLTYPE\n 2\nCONTINUOUS\n 3\nSolid Line\n 72\n65\n 73\n0\n 40\n0.0\n";5772str += " 0\nENDTAB\n";5773str += " 0\nTABLE\n 2\nLAYER\n 70\n1\n";5774str += " 0\nLAYER\n 2\nOpenJsCad\n 62\n7\n 6\ncontinuous\n";5775str += " 0\nENDTAB\n";5776str += " 0\nTABLE\n 2\nSTYLE\n 70\n0\n 0\nENDTAB\n";5777str += " 0\nTABLE\n 2\nVIEW\n 70\n0\n 0\nENDTAB\n";5778str += " 0\nENDSEC\n";5779str += " 0\nSECTION\n 2\nBLOCKS\n";5780str += " 0\nENDSEC\n";5781str += " 0\nSECTION\n 2\nENTITIES\n";5782paths.map(function(path) {5783var numpoints_closed = path.points.length + (path.closed ? 1 : 0);5784str += " 0\nLWPOLYLINE\n 8\nOpenJsCad\n 90\n" + numpoints_closed + "\n 70\n" + (path.closed ? 1 : 0) + "\n";5785for(var pointindex = 0; pointindex < numpoints_closed; pointindex++) {5786var pointindexwrapped = pointindex;5787if(pointindexwrapped >= path.points.length) pointindexwrapped -= path.points.length;5788var point = path.points[pointindexwrapped];5789str += " 10\n" + point.x + "\n 20\n" + point.y + "\n 30\n0.0\n";5790}5791});5792str += " 0\nENDSEC\n 0\nEOF\n";5793return new Blob([str], {5794type: "application/dxf"5795});5796};5797
5798CAG.Vertex = function(pos) {5799this.pos = pos;5800};5801
5802CAG.Vertex.prototype = {5803toString: function() {5804return "("+this.pos.x.toFixed(2)+","+this.pos.y.toFixed(2)+")";5805},5806getTag: function() {5807var result = this.tag;5808if(!result) {5809result = CSG.getTag();5810this.tag = result;5811}5812return result;5813}5814};5815
5816CAG.Side = function(vertex0, vertex1) {5817if(!(vertex0 instanceof CAG.Vertex)) throw new Error("Assertion failed");5818if(!(vertex1 instanceof CAG.Vertex)) throw new Error("Assertion failed");5819this.vertex0 = vertex0;5820this.vertex1 = vertex1;5821};5822
5823CAG.Side.fromFakePolygon = function(polygon) {5824if(polygon.vertices.length != 4) {5825throw new Error("Assertion failed - 1");5826}5827var pointsZeroZ = [];5828var indicesZeroZ = [];5829for(var i = 0; i < 4; i++) {5830var pos = polygon.vertices[i].pos;5831if((pos.z >= -1.001) && (pos.z < -0.999)) {5832} else if((pos.z >= 0.999) && (pos.z < 1.001)) {5833} else {5834throw new Error("Assertion failed - 2");5835}5836if(pos.z > 0) {5837pointsZeroZ.push(new CSG.Vector2D(pos.x, pos.y));5838indicesZeroZ.push(i);5839}5840}5841if(pointsZeroZ.length != 2) {5842throw new Error("Assertion failed - 3");5843}5844var d = indicesZeroZ[1] - indicesZeroZ[0];5845var p1, p2;5846if(d == 1) {5847p1 = pointsZeroZ[1];5848p2 = pointsZeroZ[0];5849} else if(d == 3) {5850p1 = pointsZeroZ[0];5851p2 = pointsZeroZ[1];5852} else {5853throw new Error("Assertion failed - 4");5854}5855var result = new CAG.Side(new CAG.Vertex(p1), new CAG.Vertex(p2));5856return result;5857};5858
5859CAG.Side.prototype = {5860toString: function() {5861return this.vertex0 + " -> "+ this.vertex1;5862},5863
5864toPolygon3D: function(z0, z1) {5865var vertices = [5866new CSG.Vertex(this.vertex0.pos.toVector3D(z0)),5867new CSG.Vertex(this.vertex1.pos.toVector3D(z0)),5868new CSG.Vertex(this.vertex1.pos.toVector3D(z1)),5869new CSG.Vertex(this.vertex0.pos.toVector3D(z1))5870];5871return new CSG.Polygon(vertices);5872},5873
5874transform: function(matrix4x4) {5875var newp1 = this.vertex0.pos.transform(matrix4x4);5876var newp2 = this.vertex1.pos.transform(matrix4x4);5877return new CAG.Side(new CAG.Vertex(newp1), new CAG.Vertex(newp2));5878},5879
5880flipped: function() {5881return new CAG.Side(this.vertex1, this.vertex0);5882},5883
5884direction: function() {5885return this.vertex1.pos.minus(this.vertex0.pos);5886},5887
5888getTag: function() {5889var result = this.tag;5890if(!result) {5891result = CSG.getTag();5892this.tag = result;5893}5894return result;5895},5896
5897lengthSquared: function() {5898var x = this.vertex1.pos.x - this.vertex0.pos.x,5899y = this.vertex1.pos.y - this.vertex0.pos.y;5900return x*x + y*y;5901},5902
5903length: function() {5904return Math.sqrt(this.lengthSquared());5905}5906};5907
5908//////////////////////////////////////
5909CAG.fuzzyCAGFactory = function() {5910this.vertexfactory = new CSG.fuzzyFactory(2, 1e-5);5911};5912
5913CAG.fuzzyCAGFactory.prototype = {5914getVertex: function(sourcevertex) {5915var elements = [sourcevertex.pos._x, sourcevertex.pos._y];5916var result = this.vertexfactory.lookupOrCreate(elements, function(els) {5917return sourcevertex;5918});5919return result;5920},5921
5922getSide: function(sourceside) {5923var vertex0 = this.getVertex(sourceside.vertex0);5924var vertex1 = this.getVertex(sourceside.vertex1);5925return new CAG.Side(vertex0, vertex1);5926},5927
5928getCAG: function(sourcecag) {5929var _this = this;5930var newsides = sourcecag.sides.map(function(side) {5931return _this.getSide(side);5932});5933return CAG.fromSides(newsides);5934}5935};5936
5937//////////////////////////////////////
5938CSG.addTransformationMethodsToPrototype(CSG.prototype);5939CSG.addTransformationMethodsToPrototype(CSG.Vector2D.prototype);5940CSG.addTransformationMethodsToPrototype(CSG.Vector3D.prototype);5941CSG.addTransformationMethodsToPrototype(CSG.Vertex.prototype);5942CSG.addTransformationMethodsToPrototype(CSG.Plane.prototype);5943CSG.addTransformationMethodsToPrototype(CSG.Polygon.prototype);5944CSG.addTransformationMethodsToPrototype(CSG.Line3D.prototype);5945CSG.addTransformationMethodsToPrototype(CSG.Connector.prototype);5946CSG.addTransformationMethodsToPrototype(CSG.Path2D.prototype);5947CSG.addTransformationMethodsToPrototype(CSG.Line2D.prototype);5948CSG.addTransformationMethodsToPrototype(CAG.prototype);5949CSG.addTransformationMethodsToPrototype(CAG.Side.prototype);5950
5951/*
59522D polygons are now supported through the CAG class.
5953With many improvements (see documentation):
5954- shapes do no longer have to be convex
5955- union/intersect/subtract is supported
5956- expand / contract are supported
5957
5958But we'll keep CSG.Polygon2D as a stub for backwards compatibility
5959*/
5960CSG.Polygon2D = function(points) {5961var cag = CAG.fromPoints(points);5962this.sides = cag.sides;5963};5964CSG.Polygon2D.prototype = CAG.prototype;5965
5966
5967module.CSG = CSG;5968module.CAG = CAG;5969})(this); //module to export to5970