From 353c50874235050dcaee5bbffcc4bc92e22be9d3 Mon Sep 17 00:00:00 2001 From: Val Erastov Date: Thu, 16 Mar 2017 00:16:00 -0700 Subject: [PATCH] don't need csg anymore --- web/lib/csg.js | 6515 ------------------------------------------------ 1 file changed, 6515 deletions(-) delete mode 100644 web/lib/csg.js diff --git a/web/lib/csg.js b/web/lib/csg.js deleted file mode 100644 index 93029ea7..00000000 --- a/web/lib/csg.js +++ /dev/null @@ -1,6515 +0,0 @@ -/* - - ## IMPORTANT NOTE --- IMPORTANT - The master for this file is located at: - https://github.com/joostn/openjscad/tree/gh-pages - That is the gh-pages branch of the joostn/openjscad project - If contributing from openjscad.org, please do NOT edit this local file but make pull requests against - above joostn/gh-pages branch. - ## IMPORTANT NOTE --- IMPORTANT NOTE - - - ## License - - Copyright (c) 2014 bebbi (elghatta@gmail.com) - Copyright (c) 2013 Eduard Bespalov (edwbes@gmail.com) - Copyright (c) 2012 Joost Nieuwenhuijse (joost@newhouse.nl) - Copyright (c) 2011 Evan Wallace (http://evanw.github.com/csg.js/) - Copyright (c) 2012 Alexandre Girard (https://github.com/alx) - - All code released under MIT license - - ## Overview - - For an overview of the CSG process see the original csg.js code: - http://evanw.github.com/csg.js/ - - CSG operations through BSP trees suffer from one problem: heavy fragmentation - of polygons. If two CSG solids of n polygons are unified, the resulting solid may have - in the order of n*n polygons, because each polygon is split by the planes of all other - polygons. After a few operations the number of polygons explodes. - - This version of CSG.js solves the problem in 3 ways: - - 1. Every polygon split is recorded in a tree (CSG.PolygonTreeNode). This is a separate - tree, not to be confused with the CSG tree. If a polygon is split into two parts but in - the end both fragments have not been discarded by the CSG operation, we can retrieve - the original unsplit polygon from the tree, instead of the two fragments. - - This does not completely solve the issue though: if a polygon is split multiple times - the number of fragments depends on the order of subsequent splits, and we might still - end up with unncessary splits: - Suppose a polygon is first split into A and B, and then into A1, B1, A2, B2. Suppose B2 is - discarded. We will end up with 2 polygons: A and B1. Depending on the actual split boundaries - we could still have joined A and B1 into one polygon. Therefore a second approach is used as well: - - 2. After CSG operations all coplanar polygon fragments are joined by a retesselating - operation. See CSG.reTesselated(). Retesselation is done through a - linear sweep over the polygon surface. The sweep line passes over the y coordinates - of all vertices in the polygon. Polygons are split at each sweep line, and the fragments - are joined horizontally and vertically into larger polygons (making sure that we - will end up with convex polygons). - This still doesn't solve the problem completely: due to floating point imprecisions - we may end up with small gaps between polygons, and polygons may not be exactly coplanar - anymore, and as a result the retesselation algorithm may fail to join those polygons. - Therefore: - - 3. A canonicalization algorithm is implemented: it looks for vertices that have - approximately the same coordinates (with a certain tolerance, say 1e-5) and replaces - them with the same vertex. If polygons share a vertex they will actually point to the - same CSG.Vertex instance. The same is done for polygon planes. See CSG.canonicalized(). - - - Performance improvements to the original CSG.js: - - Replaced the flip() and invert() methods by flipped() and inverted() which don't - modify the source object. This allows to get rid of all clone() calls, so that - multiple polygons can refer to the same CSG.Plane instance etc. - - The original union() used an extra invert(), clipTo(), invert() sequence just to remove the - coplanar front faces from b; this is now combined in a single b.clipTo(a, true) call. - - Detection whether a polygon is in front or in back of a plane: for each polygon - we are caching the coordinates of the bounding sphere. If the bounding sphere is - in front or in back of the plane we don't have to check the individual vertices - anymore. - - - Other additions to the original CSG.js: - - CSG.Vector class has been renamed into CSG.Vector3D - - Classes for 3D lines, 2D vectors, 2D lines, and methods to find the intersection of - a line and a plane etc. - - Transformations: CSG.transform(), CSG.translate(), CSG.rotate(), CSG.scale() - - Expanding or contracting a solid: CSG.expand() and CSG.contract(). Creates nice - smooth corners. - - The vertex normal has been removed since it complicates retesselation. It's not needed - for solid CAD anyway. - - */ - -(function(module) { - - var _CSGDEBUG = false; - - function fnNumberSort(a, b) { - return a - b; - } - - // # class CSG - // Holds a binary space partition tree representing a 3D solid. Two solids can - // be combined using the `union()`, `subtract()`, and `intersect()` methods. - var CSG = function() { - this.polygons = []; - this.properties = new CSG.Properties(); - this.isCanonicalized = true; - this.isRetesselated = true; - }; - - CSG.defaultResolution2D = 32; - CSG.defaultResolution3D = 12; - - // Construct a CSG solid from a list of `CSG.Polygon` instances. - CSG.fromPolygons = function(polygons) { - var csg = new CSG(); - csg.polygons = polygons; - csg.isCanonicalized = false; - csg.isRetesselated = false; - return csg; - }; - - // Construct a CSG solid from generated slices. - // Look at CSG.Polygon.prototype.solidFromSlices for details - CSG.fromSlices = function(options) { - return (new CSG.Polygon.createFromPoints([ - [0, 0, 0], - [1, 0, 0], - [1, 1, 0], - [0, 1, 0] - ])).solidFromSlices(options); - }; - - // create from an untyped object with identical property names: - CSG.fromObject = function(obj) { - var polygons = obj.polygons.map(function(p) { - return CSG.Polygon.fromObject(p); - }); - var csg = CSG.fromPolygons(polygons); - csg = csg.canonicalized(); - return csg; - }; - - // Reconstruct a CSG from the output of toCompactBinary() - CSG.fromCompactBinary = function(bin) { - if (bin['class'] != "CSG") throw new Error("Not a CSG"); - var planes = [], - planeData = bin.planeData, - numplanes = planeData.length / 4, - arrayindex = 0, - x, y, z, w, normal, plane; - for (var planeindex = 0; planeindex < numplanes; planeindex++) { - x = planeData[arrayindex++]; - y = planeData[arrayindex++]; - z = planeData[arrayindex++]; - w = planeData[arrayindex++]; - normal = CSG.Vector3D.Create(x, y, z); - plane = new CSG.Plane(normal, w); - planes.push(plane); - } - - var vertices = [], - vertexData = bin.vertexData, - numvertices = vertexData.length / 3, - pos, vertex; - arrayindex = 0; - for (var vertexindex = 0; vertexindex < numvertices; vertexindex++) { - x = vertexData[arrayindex++]; - y = vertexData[arrayindex++]; - z = vertexData[arrayindex++]; - pos = CSG.Vector3D.Create(x, y, z); - vertex = new CSG.Vertex(pos); - vertices.push(vertex); - } - - var shareds = bin.shared.map(function(shared) { - return CSG.Polygon.Shared.fromObject(shared); - }); - - var polygons = [], - numpolygons = bin.numPolygons, - numVerticesPerPolygon = bin.numVerticesPerPolygon, - polygonVertices = bin.polygonVertices, - polygonPlaneIndexes = bin.polygonPlaneIndexes, - polygonSharedIndexes = bin.polygonSharedIndexes, - numpolygonvertices, polygonvertices, shared, polygon; //already defined plane, - arrayindex = 0; - for (var polygonindex = 0; polygonindex < numpolygons; polygonindex++) { - numpolygonvertices = numVerticesPerPolygon[polygonindex]; - polygonvertices = []; - for (var i = 0; i < numpolygonvertices; i++) { - polygonvertices.push(vertices[polygonVertices[arrayindex++]]); - } - plane = planes[polygonPlaneIndexes[polygonindex]]; - shared = shareds[polygonSharedIndexes[polygonindex]]; - polygon = new CSG.Polygon(polygonvertices, shared, plane); - polygons.push(polygon); - } - var csg = CSG.fromPolygons(polygons); - csg.isCanonicalized = true; - csg.isRetesselated = true; - return csg; - }; - - CSG.prototype = { - toPolygons: function() { - return this.polygons; - }, - - // Return a new CSG solid representing space in either this solid or in the - // solid `csg`. Neither this solid nor the solid `csg` are modified. - // - // A.union(B) - // - // +-------+ +-------+ - // | | | | - // | A | | | - // | +--+----+ = | +----+ - // +----+--+ | +----+ | - // | B | | | - // | | | | - // +-------+ +-------+ - // - union: function(csg) { - var csgs; - if (csg instanceof Array) { - csgs = csg.slice(0); - csgs.push(this); - } else { - csgs = [this, csg]; - } - - // combine csg pairs in a way that forms a balanced binary tree pattern - for (var i = 1; i < csgs.length; i += 2) { - csgs.push(csgs[i-1].unionSub(csgs[i])); - } - - return csgs[i - 1].reTesselated().canonicalized(); - }, - - unionSub: function(csg, retesselate, canonicalize) { - if (!this.mayOverlap(csg)) { - return this.unionForNonIntersecting(csg); - } else { - var a = new CSG.Tree(this.polygons); - var b = new CSG.Tree(csg.polygons); - a.clipTo(b, false); - - // b.clipTo(a, true); // ERROR: this doesn't work - b.clipTo(a); - b.invert(); - b.clipTo(a); - b.invert(); - - var newpolygons = a.allPolygons().concat(b.allPolygons()); - var result = CSG.fromPolygons(newpolygons); - result.properties = this.properties._merge(csg.properties); - if (retesselate) result = result.reTesselated(); - if (canonicalize) result = result.canonicalized(); - return result; - } - }, - - // Like union, but when we know that the two solids are not intersecting - // Do not use if you are not completely sure that the solids do not intersect! - unionForNonIntersecting: function(csg) { - var newpolygons = this.polygons.concat(csg.polygons); - var result = CSG.fromPolygons(newpolygons); - result.properties = this.properties._merge(csg.properties); - result.isCanonicalized = this.isCanonicalized && csg.isCanonicalized; - result.isRetesselated = this.isRetesselated && csg.isRetesselated; - return result; - }, - - // Return a new CSG solid representing space in this solid but not in the - // solid `csg`. Neither this solid nor the solid `csg` are modified. - // - // A.subtract(B) - // - // +-------+ +-------+ - // | | | | - // | A | | | - // | +--+----+ = | +--+ - // +----+--+ | +----+ - // | B | - // | | - // +-------+ - // - subtract: function(csg) { - var csgs; - if (csg instanceof Array) { - csgs = csg; - } else { - csgs = [csg]; - } - var result = this; - for (var i = 0; i < csgs.length; i++) { - var islast = (i == (csgs.length - 1)); - result = result.subtractSub(csgs[i], islast, islast); - } - return result; - }, - - subtractSub: function(csg, retesselate, canonicalize) { - var a = new CSG.Tree(this.polygons); - var b = new CSG.Tree(csg.polygons); - a.invert(); - a.clipTo(b); - b.clipTo(a, true); - a.addPolygons(b.allPolygons()); - a.invert(); - var result = CSG.fromPolygons(a.allPolygons()); - result.properties = this.properties._merge(csg.properties); - if (retesselate) result = result.reTesselated(); - if (canonicalize) result = result.canonicalized(); - return result; - }, - - // Return a new CSG solid representing space both this solid and in the - // solid `csg`. Neither this solid nor the solid `csg` are modified. - // - // A.intersect(B) - // - // +-------+ - // | | - // | A | - // | +--+----+ = +--+ - // +----+--+ | +--+ - // | B | - // | | - // +-------+ - // - intersect: function(csg) { - var csgs; - if (csg instanceof Array) { - csgs = csg; - } else { - csgs = [csg]; - } - var result = this; - for (var i = 0; i < csgs.length; i++) { - var islast = (i == (csgs.length - 1)); - result = result.intersectSub(csgs[i], islast, islast); - } - return result; - }, - - intersectSub: function(csg, retesselate, canonicalize) { - var a = new CSG.Tree(this.polygons); - var b = new CSG.Tree(csg.polygons); - a.invert(); - b.clipTo(a); - b.invert(); - a.clipTo(b); - b.clipTo(a); - a.addPolygons(b.allPolygons()); - a.invert(); - var result = CSG.fromPolygons(a.allPolygons()); - result.properties = this.properties._merge(csg.properties); - if (retesselate) result = result.reTesselated(); - if (canonicalize) result = result.canonicalized(); - return result; - }, - - // Return a new CSG solid with solid and empty space switched. This solid is - // not modified. - invert: function() { - var flippedpolygons = this.polygons.map(function(p) { - return p.flipped(); - }); - return CSG.fromPolygons(flippedpolygons); - // TODO: flip properties? - }, - - // Affine transformation of CSG object. Returns a new CSG object - transform1: function(matrix4x4) { - var newpolygons = this.polygons.map(function(p) { - return p.transform(matrix4x4); - }); - var result = CSG.fromPolygons(newpolygons); - result.properties = this.properties._transform(matrix4x4); - result.isRetesselated = this.isRetesselated; - return result; - }, - - transform: function(matrix4x4) { - var ismirror = matrix4x4.isMirroring(); - var transformedvertices = {}; - var transformedplanes = {}; - var newpolygons = this.polygons.map(function(p) { - var newplane; - var plane = p.plane; - var planetag = plane.getTag(); - if (planetag in transformedplanes) { - newplane = transformedplanes[planetag]; - } else { - newplane = plane.transform(matrix4x4); - transformedplanes[planetag] = newplane; - } - var newvertices = p.vertices.map(function(v) { - var newvertex; - var vertextag = v.getTag(); - if (vertextag in transformedvertices) { - newvertex = transformedvertices[vertextag]; - } else { - newvertex = v.transform(matrix4x4); - transformedvertices[vertextag] = newvertex; - } - return newvertex; - }); - if (ismirror) newvertices.reverse(); - return new CSG.Polygon(newvertices, p.shared, newplane); - }); - var result = CSG.fromPolygons(newpolygons); - result.properties = this.properties._transform(matrix4x4); - result.isRetesselated = this.isRetesselated; - result.isCanonicalized = this.isCanonicalized; - return result; - }, - - toString: function() { - var result = "CSG solid:\n"; - this.polygons.map(function(p) { - result += p.toString(); - }); - return result; - }, - - // Expand the solid - // resolution: number of points per 360 degree for the rounded corners - expand: function(radius, resolution) { - var result = this.expandedShell(radius, resolution, true); - result = result.reTesselated(); - result.properties = this.properties; // keep original properties - return result; - }, - - // Contract the solid - // resolution: number of points per 360 degree for the rounded corners - contract: function(radius, resolution) { - var expandedshell = this.expandedShell(radius, resolution, false); - var result = this.subtract(expandedshell); - result = result.reTesselated(); - result.properties = this.properties; // keep original properties - return result; - }, - - // cut the solid at a plane, and stretch the cross-section found along plane normal - stretchAtPlane: function(normal, point, length) { - var plane = CSG.Plane.fromNormalAndPoint(normal, point); - var onb = new CSG.OrthoNormalBasis(plane); - var crosssect = this.sectionCut(onb); - var midpiece = crosssect.extrudeInOrthonormalBasis(onb, length); - var piece1 = this.cutByPlane(plane); - var piece2 = this.cutByPlane(plane.flipped()); - var result = piece1.union([midpiece, piece2.translate(plane.normal.times(length))]); - return result; - }, - - - // Create the expanded shell of the solid: - // All faces are extruded to get a thickness of 2*radius - // Cylinders are constructed around every side - // Spheres are placed on every vertex - // unionWithThis: if true, the resulting solid will be united with 'this' solid; - // the result is a true expansion of the solid - // If false, returns only the shell - expandedShell: function(radius, resolution, unionWithThis) { - var csg = this.reTesselated(); - var result; - if (unionWithThis) { - result = csg; - } else { - result = new CSG(); - } - - // first extrude all polygons: - csg.polygons.map(function(polygon) { - var extrudevector = polygon.plane.normal.unit().times(2 * radius); - var translatedpolygon = polygon.translate(extrudevector.times(-0.5)); - var extrudedface = translatedpolygon.extrude(extrudevector); - result = result.unionSub(extrudedface, false, false); - }); - - // Make a list of all unique vertex pairs (i.e. all sides of the solid) - // For each vertex pair we collect the following: - // v1: first coordinate - // v2: second coordinate - // planenormals: array of normal vectors of all planes touching this side - var vertexpairs = {}; // map of 'vertex pair tag' to {v1, v2, planenormals} - csg.polygons.map(function(polygon) { - var numvertices = polygon.vertices.length; - var prevvertex = polygon.vertices[numvertices - 1]; - var prevvertextag = prevvertex.getTag(); - for (var i = 0; i < numvertices; i++) { - var vertex = polygon.vertices[i]; - var vertextag = vertex.getTag(); - var vertextagpair; - if (vertextag < prevvertextag) { - vertextagpair = vertextag + "-" + prevvertextag; - } else { - vertextagpair = prevvertextag + "-" + vertextag; - } - var obj; - if (vertextagpair in vertexpairs) { - obj = vertexpairs[vertextagpair]; - } else { - obj = { - v1: prevvertex, - v2: vertex, - planenormals: [] - }; - vertexpairs[vertextagpair] = obj; - } - obj.planenormals.push(polygon.plane.normal); - - prevvertextag = vertextag; - prevvertex = vertex; - } - }); - - // now construct a cylinder on every side - // The cylinder is always an approximation of a true cylinder: it will have polygons - // around the sides. We will make sure though that the cylinder will have an edge at every - // face that touches this side. This ensures that we will get a smooth fill even - // if two edges are at, say, 10 degrees and the resolution is low. - // Note: the result is not retesselated yet but it really should be! - for (var vertextagpair in vertexpairs) { - var vertexpair = vertexpairs[vertextagpair], - startpoint = vertexpair.v1.pos, - endpoint = vertexpair.v2.pos, - // our x,y and z vectors: - zbase = endpoint.minus(startpoint).unit(), - xbase = vertexpair.planenormals[0].unit(), - ybase = xbase.cross(zbase), - - // make a list of angles that the cylinder should traverse: - angles = []; - - // first of all equally spaced around the cylinder: - for (var i = 0; i < resolution; i++) { - angles.push(i * Math.PI * 2 / resolution); - } - - // and also at every normal of all touching planes: - for (var i = 0, iMax = vertexpair.planenormals.length; i < iMax; i++) { - var planenormal = vertexpair.planenormals[i], - si = ybase.dot(planenormal), - co = xbase.dot(planenormal), - angle = Math.atan2(si, co); - - if (angle < 0) angle += Math.PI * 2; - angles.push(angle); - angle = Math.atan2(-si, -co); - if (angle < 0) angle += Math.PI * 2; - angles.push(angle); - } - - // this will result in some duplicate angles but we will get rid of those later. - // Sort: - angles = angles.sort(fnNumberSort); - - // Now construct the cylinder by traversing all angles: - var numangles = angles.length, - prevp1, prevp2, - startfacevertices = [], - endfacevertices = [], - polygons = []; - for (var i = -1; i < numangles; i++) { - var angle = angles[(i < 0) ? (i + numangles) : i], - si = Math.sin(angle), - co = Math.cos(angle), - p = xbase.times(co * radius).plus(ybase.times(si * radius)), - p1 = startpoint.plus(p), - p2 = endpoint.plus(p), - skip = false; - if (i >= 0) { - if (p1.distanceTo(prevp1) < 1e-5) { - skip = true; - } - } - if (!skip) { - if (i >= 0) { - startfacevertices.push(new CSG.Vertex(p1)); - endfacevertices.push(new CSG.Vertex(p2)); - var polygonvertices = [ - new CSG.Vertex(prevp2), - new CSG.Vertex(p2), - new CSG.Vertex(p1), - new CSG.Vertex(prevp1) - ]; - var polygon = new CSG.Polygon(polygonvertices); - polygons.push(polygon); - } - prevp1 = p1; - prevp2 = p2; - } - } - endfacevertices.reverse(); - polygons.push(new CSG.Polygon(startfacevertices)); - polygons.push(new CSG.Polygon(endfacevertices)); - var cylinder = CSG.fromPolygons(polygons); - result = result.unionSub(cylinder, false, false); - } - - // make a list of all unique vertices - // For each vertex we also collect the list of normals of the planes touching the vertices - var vertexmap = {}; - csg.polygons.map(function(polygon) { - polygon.vertices.map(function(vertex) { - var vertextag = vertex.getTag(); - var obj; - if (vertextag in vertexmap) { - obj = vertexmap[vertextag]; - } else { - obj = { - pos: vertex.pos, - normals: [] - }; - vertexmap[vertextag] = obj; - } - obj.normals.push(polygon.plane.normal); - }); - }); - - // and build spheres at each vertex - // We will try to set the x and z axis to the normals of 2 planes - // This will ensure that our sphere tesselation somewhat matches 2 planes - for (var vertextag in vertexmap) { - var vertexobj = vertexmap[vertextag]; - // use the first normal to be the x axis of our sphere: - var xaxis = vertexobj.normals[0].unit(); - // and find a suitable z axis. We will use the normal which is most perpendicular to the x axis: - var bestzaxis = null; - var bestzaxisorthogonality = 0; - for (var i = 1; i < vertexobj.normals.length; i++) { - var normal = vertexobj.normals[i].unit(); - var cross = xaxis.cross(normal); - var crosslength = cross.length(); - if (crosslength > 0.05) { - if (crosslength > bestzaxisorthogonality) { - bestzaxisorthogonality = crosslength; - bestzaxis = normal; - } - } - } - if (!bestzaxis) { - bestzaxis = xaxis.randomNonParallelVector(); - } - var yaxis = xaxis.cross(bestzaxis).unit(); - var zaxis = yaxis.cross(xaxis); - var sphere = CSG.sphere({ - center: vertexobj.pos, - radius: radius, - resolution: resolution, - axes: [xaxis, yaxis, zaxis] - }); - result = result.unionSub(sphere, false, false); - } - - return result; - }, - - canonicalized: function() { - if (this.isCanonicalized) { - return this; - } else { - var factory = new CSG.fuzzyCSGFactory(); - var result = factory.getCSG(this); - result.isCanonicalized = true; - result.isRetesselated = this.isRetesselated; - result.properties = this.properties; // keep original properties - return result; - } - }, - - reTesselated: function() { - if (this.isRetesselated) { - return this; - } else { - var csg = this; - var polygonsPerPlane = {}; - var isCanonicalized = csg.isCanonicalized; - var fuzzyfactory = new CSG.fuzzyCSGFactory(); - csg.polygons.map(function(polygon) { - var plane = polygon.plane; - var shared = polygon.shared; - if (!isCanonicalized) { - // in order to identify to polygons having the same plane, we need to canonicalize the planes - // We don't have to do a full canonizalization (including vertices), to save time only do the planes and the shared data: - plane = fuzzyfactory.getPlane(plane); - shared = fuzzyfactory.getPolygonShared(shared); - } - var tag = plane.getTag() + "/" + shared.getTag(); - if (!(tag in polygonsPerPlane)) { - polygonsPerPlane[tag] = [polygon]; - } else { - polygonsPerPlane[tag].push(polygon); - } - }); - var destpolygons = []; - for (var planetag in polygonsPerPlane) { - var sourcepolygons = polygonsPerPlane[planetag]; - if (sourcepolygons.length < 2) { - destpolygons = destpolygons.concat(sourcepolygons); - } else { - var retesselayedpolygons = []; - CSG.reTesselateCoplanarPolygons(sourcepolygons, retesselayedpolygons); - destpolygons = destpolygons.concat(retesselayedpolygons); - } - } - var result = CSG.fromPolygons(destpolygons); - result.isRetesselated = true; - // result = result.canonicalized(); - result.properties = this.properties; // keep original properties - return result; - } - }, - - // returns an array of two CSG.Vector3Ds (minimum coordinates and maximum coordinates) - getBounds: function() { - if (!this.cachedBoundingBox) { - var minpoint = new CSG.Vector3D(0, 0, 0); - var maxpoint = new CSG.Vector3D(0, 0, 0); - var polygons = this.polygons; - var numpolygons = polygons.length; - for (var i = 0; i < numpolygons; i++) { - var polygon = polygons[i]; - var bounds = polygon.boundingBox(); - if (i === 0) { - minpoint = bounds[0]; - maxpoint = bounds[1]; - } else { - minpoint = minpoint.min(bounds[0]); - maxpoint = maxpoint.max(bounds[1]); - } - } - this.cachedBoundingBox = [minpoint, maxpoint]; - } - return this.cachedBoundingBox; - }, - - // returns true if there is a possibility that the two solids overlap - // returns false if we can be sure that they do not overlap - mayOverlap: function(csg) { - if ((this.polygons.length === 0) || (csg.polygons.length === 0)) { - return false; - } else { - var mybounds = this.getBounds(); - var otherbounds = csg.getBounds(); - if (mybounds[1].x < otherbounds[0].x) return false; - if (mybounds[0].x > otherbounds[1].x) return false; - if (mybounds[1].y < otherbounds[0].y) return false; - if (mybounds[0].y > otherbounds[1].y) return false; - if (mybounds[1].z < otherbounds[0].z) return false; - if (mybounds[0].z > otherbounds[1].z) return false; - return true; - } - }, - - // Cut the solid by a plane. Returns the solid on the back side of the plane - cutByPlane: function(plane) { - if (this.polygons.length === 0) { - return new CSG(); - } - // Ideally we would like to do an intersection with a polygon of inifinite size - // but this is not supported by our implementation. As a workaround, we will create - // a cube, with one face on the plane, and a size larger enough so that the entire - // solid fits in the cube. - // find the max distance of any vertex to the center of the plane: - var planecenter = plane.normal.times(plane.w); - var maxdistance = 0; - this.polygons.map(function(polygon) { - polygon.vertices.map(function(vertex) { - var distance = vertex.pos.distanceToSquared(planecenter); - if (distance > maxdistance) maxdistance = distance; - }); - }); - maxdistance = Math.sqrt(maxdistance); - maxdistance *= 1.01; // make sure it's really larger - // Now build a polygon on the plane, at any point farther than maxdistance from the plane center: - var vertices = []; - var orthobasis = new CSG.OrthoNormalBasis(plane); - vertices.push(new CSG.Vertex(orthobasis.to3D(new CSG.Vector2D(maxdistance, -maxdistance)))); - vertices.push(new CSG.Vertex(orthobasis.to3D(new CSG.Vector2D(-maxdistance, -maxdistance)))); - vertices.push(new CSG.Vertex(orthobasis.to3D(new CSG.Vector2D(-maxdistance, maxdistance)))); - vertices.push(new CSG.Vertex(orthobasis.to3D(new CSG.Vector2D(maxdistance, maxdistance)))); - var polygon = new CSG.Polygon(vertices, null, plane.flipped()); - - // and extrude the polygon into a cube, backwards of the plane: - var cube = polygon.extrude(plane.normal.times(-maxdistance)); - - // Now we can do the intersection: - var result = this.intersect(cube); - result.properties = this.properties; // keep original properties - return result; - }, - - // Connect a solid to another solid, such that two CSG.Connectors become connected - // myConnector: a CSG.Connector of this solid - // otherConnector: a CSG.Connector to which myConnector should be connected - // mirror: false: the 'axis' vectors of the connectors should point in the same direction - // true: the 'axis' vectors of the connectors should point in opposite direction - // normalrotation: degrees of rotation between the 'normal' vectors of the two - // connectors - connectTo: function(myConnector, otherConnector, mirror, normalrotation) { - var matrix = myConnector.getTransformationTo(otherConnector, mirror, normalrotation); - return this.transform(matrix); - }, - - // set the .shared property of all polygons - // Returns a new CSG solid, the original is unmodified! - setShared: function(shared) { - var polygons = this.polygons.map(function(p) { - return new CSG.Polygon(p.vertices, shared, p.plane); - }); - var result = CSG.fromPolygons(polygons); - result.properties = this.properties; // keep original properties - result.isRetesselated = this.isRetesselated; - result.isCanonicalized = this.isCanonicalized; - return result; - }, - - setColor: function(args) { - var newshared = CSG.Polygon.Shared.fromColor.apply(this, arguments); - return this.setShared(newshared); - }, - - toCompactBinary: function() { - var csg = this.canonicalized(), - numpolygons = csg.polygons.length, - numpolygonvertices = 0, - numvertices = 0, - vertexmap = {}, - vertices = [], - numplanes = 0, - planemap = {}, - polygonindex = 0, - planes = [], - shareds = [], - sharedmap = {}, - numshared = 0; - // for (var i = 0, iMax = csg.polygons.length; i < iMax; i++) { - // var p = csg.polygons[i]; - // for (var j = 0, jMax = p.length; j < jMax; j++) { - // ++numpolygonvertices; - // var vertextag = p[j].getTag(); - // if(!(vertextag in vertexmap)) { - // vertexmap[vertextag] = numvertices++; - // vertices.push(p[j]); - // } - // } - csg.polygons.map(function(p) { - p.vertices.map(function(v) { - ++numpolygonvertices; - var vertextag = v.getTag(); - if (!(vertextag in vertexmap)) { - vertexmap[vertextag] = numvertices++; - vertices.push(v); - } - }); - - var planetag = p.plane.getTag(); - if (!(planetag in planemap)) { - planemap[planetag] = numplanes++; - planes.push(p.plane); - } - var sharedtag = p.shared.getTag(); - if (!(sharedtag in sharedmap)) { - sharedmap[sharedtag] = numshared++; - shareds.push(p.shared); - } - }); - var numVerticesPerPolygon = new Uint32Array(numpolygons), - polygonSharedIndexes = new Uint32Array(numpolygons), - polygonVertices = new Uint32Array(numpolygonvertices), - polygonPlaneIndexes = new Uint32Array(numpolygons), - vertexData = new Float64Array(numvertices * 3), - planeData = new Float64Array(numplanes * 4), - polygonVerticesIndex = 0; - for (var polygonindex = 0; polygonindex < numpolygons; ++polygonindex) { - var p = csg.polygons[polygonindex]; - numVerticesPerPolygon[polygonindex] = p.vertices.length; - p.vertices.map(function(v) { - var vertextag = v.getTag(); - var vertexindex = vertexmap[vertextag]; - polygonVertices[polygonVerticesIndex++] = vertexindex; - }); - var planetag = p.plane.getTag(); - var planeindex = planemap[planetag]; - polygonPlaneIndexes[polygonindex] = planeindex; - var sharedtag = p.shared.getTag(); - var sharedindex = sharedmap[sharedtag]; - polygonSharedIndexes[polygonindex] = sharedindex; - } - var verticesArrayIndex = 0; - vertices.map(function(v) { - var pos = v.pos; - vertexData[verticesArrayIndex++] = pos._x; - vertexData[verticesArrayIndex++] = pos._y; - vertexData[verticesArrayIndex++] = pos._z; - }); - var planesArrayIndex = 0; - planes.map(function(p) { - var normal = p.normal; - planeData[planesArrayIndex++] = normal._x; - planeData[planesArrayIndex++] = normal._y; - planeData[planesArrayIndex++] = normal._z; - planeData[planesArrayIndex++] = p.w; - }); - var result = { - "class": "CSG", - numPolygons: numpolygons, - numVerticesPerPolygon: numVerticesPerPolygon, - polygonPlaneIndexes: polygonPlaneIndexes, - polygonSharedIndexes: polygonSharedIndexes, - polygonVertices: polygonVertices, - vertexData: vertexData, - planeData: planeData, - shared: shareds - }; - return result; - }, - - // For debugging - // Creates a new solid with a tiny cube at every vertex of the source solid - toPointCloud: function(cuberadius) { - var csg = this.reTesselated(); - - var result = new CSG(); - - // make a list of all unique vertices - // For each vertex we also collect the list of normals of the planes touching the vertices - var vertexmap = {}; - csg.polygons.map(function(polygon) { - polygon.vertices.map(function(vertex) { - vertexmap[vertex.getTag()] = vertex.pos; - }); - }); - - for (var vertextag in vertexmap) { - var pos = vertexmap[vertextag]; - var cube = CSG.cube({ - center: pos, - radius: cuberadius - }); - result = result.unionSub(cube, false, false); - } - result = result.reTesselated(); - return result; - }, - - // Get the transformation that transforms this CSG such that it is lying on the z=0 plane, - // as flat as possible (i.e. the least z-height). - // So that it is in an orientation suitable for CNC milling - getTransformationAndInverseTransformationToFlatLying: function() { - if (this.polygons.length === 0) { - return new CSG.Matrix4x4(); // unity - } else { - // get a list of unique planes in the CSG: - var csg = this.canonicalized(); - var planemap = {}; - csg.polygons.map(function(polygon) { - planemap[polygon.plane.getTag()] = polygon.plane; - }); - // try each plane in the CSG and find the plane that, when we align it flat onto z=0, - // gives the least height in z-direction. - // If two planes give the same height, pick the plane that originally had a normal closest - // to [0,0,-1]. - var xvector = new CSG.Vector3D(1, 0, 0); - var yvector = new CSG.Vector3D(0, 1, 0); - var zvector = new CSG.Vector3D(0, 0, 1); - var z0connectorx = new CSG.Connector([0, 0, 0], [0, 0, -1], xvector); - var z0connectory = new CSG.Connector([0, 0, 0], [0, 0, -1], yvector); - var isfirst = true; - var minheight = 0; - var maxdotz = 0; - var besttransformation, bestinversetransformation; - for (var planetag in planemap) { - var plane = planemap[planetag]; - var pointonplane = plane.normal.times(plane.w); - var transformation, inversetransformation; - // We need a normal vecrtor for the transformation - // determine which is more perpendicular to the plane normal: x or y? - // we will align this as much as possible to the x or y axis vector - var xorthogonality = plane.normal.cross(xvector).length(); - var yorthogonality = plane.normal.cross(yvector).length(); - if (xorthogonality > yorthogonality) { - // x is better: - var planeconnector = new CSG.Connector(pointonplane, plane.normal, xvector); - transformation = planeconnector.getTransformationTo(z0connectorx, false, 0); - inversetransformation = z0connectorx.getTransformationTo(planeconnector, false, 0); - } else { - // y is better: - var planeconnector = new CSG.Connector(pointonplane, plane.normal, yvector); - transformation = planeconnector.getTransformationTo(z0connectory, false, 0); - inversetransformation = z0connectory.getTransformationTo(planeconnector, false, 0); - } - var transformedcsg = csg.transform(transformation); - var dotz = -plane.normal.dot(zvector); - var bounds = transformedcsg.getBounds(); - var zheight = bounds[1].z - bounds[0].z; - var isbetter = isfirst; - if (!isbetter) { - if (zheight < minheight) { - isbetter = true; - } else if (zheight == minheight) { - if (dotz > maxdotz) isbetter = true; - } - } - if (isbetter) { - // translate the transformation around the z-axis and onto the z plane: - var translation = new CSG.Vector3D([-0.5 * (bounds[1].x + bounds[0].x), -0.5 * (bounds[1].y + bounds[0].y), -bounds[0].z]); - transformation = transformation.multiply(CSG.Matrix4x4.translation(translation)); - inversetransformation = CSG.Matrix4x4.translation(translation.negated()).multiply(inversetransformation); - minheight = zheight; - maxdotz = dotz; - besttransformation = transformation; - bestinversetransformation = inversetransformation; - } - isfirst = false; - } - return [besttransformation, bestinversetransformation]; - } - }, - - getTransformationToFlatLying: function() { - var result = this.getTransformationAndInverseTransformationToFlatLying(); - return result[0]; - }, - - lieFlat: function() { - var transformation = this.getTransformationToFlatLying(); - return this.transform(transformation); - }, - - // project the 3D CSG onto a plane - // This returns a 2D CAG with the 'shadow' shape of the 3D solid when projected onto the - // plane represented by the orthonormal basis - projectToOrthoNormalBasis: function(orthobasis) { - var EPS = 1e-5; - var cags = []; - this.polygons.filter(function(p) { - // only return polys in plane, others may disturb result - return p.plane.normal.minus(orthobasis.plane.normal).lengthSquared() < EPS*EPS; - }) - .map(function(polygon) { - var cag = polygon.projectToOrthoNormalBasis(orthobasis); - if (cag.sides.length > 0) { - cags.push(cag); - } - }); - var result = new CAG().union(cags); - return result; - }, - - sectionCut: function(orthobasis) { - var EPS = 1e-5; - var plane1 = orthobasis.plane; - var plane2 = orthobasis.plane.flipped(); - plane1 = new CSG.Plane(plane1.normal, plane1.w); - plane2 = new CSG.Plane(plane2.normal, plane2.w + 5*EPS); - var cut3d = this.cutByPlane(plane1); - cut3d = cut3d.cutByPlane(plane2); - return cut3d.projectToOrthoNormalBasis(orthobasis); - }, - - /* - fixTJunctions: - - Suppose we have two polygons ACDB and EDGF: - - A-----B - | | - | E--F - | | | - C-----D--G - - Note that vertex E forms a T-junction on the side BD. In this case some STL slicers will complain - that the solid is not watertight. This is because the watertightness check is done by checking if - each side DE is matched by another side ED. - - This function will return a new solid with ACDB replaced by ACDEB - - Note that this can create polygons that are slightly non-convex (due to rounding errors). Therefore the result should - not be used for further CSG operations! - */ - fixTJunctions: function() { - var csg = this.canonicalized(); - var sidemap = {}; - for (var polygonindex = 0; polygonindex < csg.polygons.length; polygonindex++) { - var polygon = csg.polygons[polygonindex]; - var numvertices = polygon.vertices.length; - if (numvertices >= 3) // should be true - { - var vertex = polygon.vertices[0]; - var vertextag = vertex.getTag(); - for (var vertexindex = 0; vertexindex < numvertices; vertexindex++) { - var nextvertexindex = vertexindex + 1; - if (nextvertexindex == numvertices) nextvertexindex = 0; - var nextvertex = polygon.vertices[nextvertexindex]; - var nextvertextag = nextvertex.getTag(); - var sidetag = vertextag + "/" + nextvertextag; - var reversesidetag = nextvertextag + "/" + vertextag; - if (reversesidetag in sidemap) { - // this side matches the same side in another polygon. Remove from sidemap: - var ar = sidemap[reversesidetag]; - ar.splice(-1, 1); - if (ar.length === 0) { - delete sidemap[reversesidetag]; - } - } else { - var sideobj = { - vertex0: vertex, - vertex1: nextvertex, - polygonindex: polygonindex - }; - if (!(sidetag in sidemap)) { - sidemap[sidetag] = [sideobj]; - } else { - sidemap[sidetag].push(sideobj); - } - } - vertex = nextvertex; - vertextag = nextvertextag; - } - } - } - // now sidemap contains 'unmatched' sides - // i.e. side AB in one polygon does not have a matching side BA in another polygon - var vertextag2sidestart = {}; - var vertextag2sideend = {}; - var sidestocheck = {}; - var sidemapisempty = true; - for (var sidetag in sidemap) { - sidemapisempty = false; - sidestocheck[sidetag] = true; - sidemap[sidetag].map(function(sideobj) { - var starttag = sideobj.vertex0.getTag(); - var endtag = sideobj.vertex1.getTag(); - if (starttag in vertextag2sidestart) { - vertextag2sidestart[starttag].push(sidetag); - } else { - vertextag2sidestart[starttag] = [sidetag]; - } - if (endtag in vertextag2sideend) { - vertextag2sideend[endtag].push(sidetag); - } else { - vertextag2sideend[endtag] = [sidetag]; - } - }); - } - - if (!sidemapisempty) { - // make a copy of the polygons array, since we are going to modify it: - var polygons = csg.polygons.slice(0); - - function addSide(vertex0, vertex1, polygonindex) { - var starttag = vertex0.getTag(); - var endtag = vertex1.getTag(); - if (starttag == endtag) throw new Error("Assertion failed"); - var newsidetag = starttag + "/" + endtag; - var reversesidetag = endtag + "/" + starttag; - if (reversesidetag in sidemap) { - // we have a matching reverse oriented side. - // Instead of adding the new side, cancel out the reverse side: - // console.log("addSide("+newsidetag+") has reverse side:"); - deleteSide(vertex1, vertex0, null); - return null; - } - // console.log("addSide("+newsidetag+")"); - var newsideobj = { - vertex0: vertex0, - vertex1: vertex1, - polygonindex: polygonindex - }; - if (!(newsidetag in sidemap)) { - sidemap[newsidetag] = [newsideobj]; - } else { - sidemap[newsidetag].push(newsideobj); - } - if (starttag in vertextag2sidestart) { - vertextag2sidestart[starttag].push(newsidetag); - } else { - vertextag2sidestart[starttag] = [newsidetag]; - } - if (endtag in vertextag2sideend) { - vertextag2sideend[endtag].push(newsidetag); - } else { - vertextag2sideend[endtag] = [newsidetag]; - } - return newsidetag; - } - - function deleteSide(vertex0, vertex1, polygonindex) { - var starttag = vertex0.getTag(); - var endtag = vertex1.getTag(); - var sidetag = starttag + "/" + endtag; - // console.log("deleteSide("+sidetag+")"); - if (!(sidetag in sidemap)) throw new Error("Assertion failed"); - var idx = -1; - var sideobjs = sidemap[sidetag]; - for (var i = 0; i < sideobjs.length; i++) { - var sideobj = sideobjs[i]; - if (sideobj.vertex0 != vertex0) continue; - if (sideobj.vertex1 != vertex1) continue; - if (polygonindex !== null) { - if (sideobj.polygonindex != polygonindex) continue; - } - idx = i; - break; - } - if (idx < 0) throw new Error("Assertion failed"); - sideobjs.splice(idx, 1); - if (sideobjs.length === 0) { - delete sidemap[sidetag]; - } - idx = vertextag2sidestart[starttag].indexOf(sidetag); - if (idx < 0) throw new Error("Assertion failed"); - vertextag2sidestart[starttag].splice(idx, 1); - if (vertextag2sidestart[starttag].length === 0) { - delete vertextag2sidestart[starttag]; - } - - idx = vertextag2sideend[endtag].indexOf(sidetag); - if (idx < 0) throw new Error("Assertion failed"); - vertextag2sideend[endtag].splice(idx, 1); - if (vertextag2sideend[endtag].length === 0) { - delete vertextag2sideend[endtag]; - } - } - - - while (true) { - var sidemapisempty = true; - for (var sidetag in sidemap) { - sidemapisempty = false; - sidestocheck[sidetag] = true; - } - if (sidemapisempty) break; - var donesomething = false; - while (true) { - var sidetagtocheck = null; - for (var sidetag in sidestocheck) { - sidetagtocheck = sidetag; - break; - } - if (sidetagtocheck === null) break; // sidestocheck is empty, we're done! - var donewithside = true; - if (sidetagtocheck in sidemap) { - var sideobjs = sidemap[sidetagtocheck]; - if (sideobjs.length === 0) throw new Error("Assertion failed"); - var sideobj = sideobjs[0]; - for (var directionindex = 0; directionindex < 2; directionindex++) { - var startvertex = (directionindex === 0) ? sideobj.vertex0 : sideobj.vertex1; - var endvertex = (directionindex === 0) ? sideobj.vertex1 : sideobj.vertex0; - var startvertextag = startvertex.getTag(); - var endvertextag = endvertex.getTag(); - var matchingsides = []; - if (directionindex === 0) { - if (startvertextag in vertextag2sideend) { - matchingsides = vertextag2sideend[startvertextag]; - } - } else { - if (startvertextag in vertextag2sidestart) { - matchingsides = vertextag2sidestart[startvertextag]; - } - } - for (var matchingsideindex = 0; matchingsideindex < matchingsides.length; matchingsideindex++) { - var matchingsidetag = matchingsides[matchingsideindex]; - var matchingside = sidemap[matchingsidetag][0]; - var matchingsidestartvertex = (directionindex === 0) ? matchingside.vertex0 : matchingside.vertex1; - var matchingsideendvertex = (directionindex === 0) ? matchingside.vertex1 : matchingside.vertex0; - var matchingsidestartvertextag = matchingsidestartvertex.getTag(); - var matchingsideendvertextag = matchingsideendvertex.getTag(); - if (matchingsideendvertextag != startvertextag) throw new Error("Assertion failed"); - if (matchingsidestartvertextag == endvertextag) { - // matchingside cancels sidetagtocheck - deleteSide(startvertex, endvertex, null); - deleteSide(endvertex, startvertex, null); - donewithside = false; - directionindex = 2; // skip reverse direction check - donesomething = true; - break; - } else { - var startpos = startvertex.pos; - var endpos = endvertex.pos; - var checkpos = matchingsidestartvertex.pos; - var direction = checkpos.minus(startpos); - // Now we need to check if endpos is on the line startpos-checkpos: - var t = endpos.minus(startpos).dot(direction) / direction.dot(direction); - if ((t > 0) && (t < 1)) { - var closestpoint = startpos.plus(direction.times(t)); - var distancesquared = closestpoint.distanceToSquared(endpos); - if (distancesquared < 1e-10) { - // Yes it's a t-junction! We need to split matchingside in two: - var polygonindex = matchingside.polygonindex; - var polygon = polygons[polygonindex]; - // find the index of startvertextag in polygon: - var insertionvertextag = matchingside.vertex1.getTag(); - var insertionvertextagindex = -1; - for (var i = 0; i < polygon.vertices.length; i++) { - if (polygon.vertices[i].getTag() == insertionvertextag) { - insertionvertextagindex = i; - break; - } - } - if (insertionvertextagindex < 0) throw new Error("Assertion failed"); - // split the side by inserting the vertex: - var newvertices = polygon.vertices.slice(0); - newvertices.splice(insertionvertextagindex, 0, endvertex); - var newpolygon = new CSG.Polygon(newvertices, polygon.shared /*polygon.plane*/ ); - polygons[polygonindex] = newpolygon; - - // remove the original sides from our maps: - // deleteSide(sideobj.vertex0, sideobj.vertex1, null); - deleteSide(matchingside.vertex0, matchingside.vertex1, polygonindex); - var newsidetag1 = addSide(matchingside.vertex0, endvertex, polygonindex); - var newsidetag2 = addSide(endvertex, matchingside.vertex1, polygonindex); - if (newsidetag1 !== null) sidestocheck[newsidetag1] = true; - if (newsidetag2 !== null) sidestocheck[newsidetag2] = true; - donewithside = false; - directionindex = 2; // skip reverse direction check - donesomething = true; - break; - } // if(distancesquared < 1e-10) - } // if( (t > 0) && (t < 1) ) - } // if(endingstidestartvertextag == endvertextag) - } // for matchingsideindex - } // for directionindex - } // if(sidetagtocheck in sidemap) - if (donewithside) { - delete sidestocheck[sidetag]; - } - } - if (!donesomething) break; - } - var newcsg = CSG.fromPolygons(polygons); - newcsg.properties = csg.properties; - newcsg.isCanonicalized = true; - newcsg.isRetesselated = true; - csg = newcsg; - } // if(!sidemapisempty) - var sidemapisempty = true; - for (var sidetag in sidemap) { - sidemapisempty = false; - break; - } - if (!sidemapisempty) { - // throw new Error("!sidemapisempty"); - OpenJsCad.log("!sidemapisempty"); - } - return csg; - }, - - toTriangles: function() { - var polygons = []; - this.polygons.forEach(function(poly) { - var firstVertex = poly.vertices[0]; - for (var i = poly.vertices.length - 3; i >= 0; i--) { - polygons.push(new CSG.Polygon([ - firstVertex, poly.vertices[i + 1], poly.vertices[i + 2] - ], - poly.shared, poly.plane)); - } - }); - return polygons; - }, - - // features: string, or array containing 1 or more strings of: 'volume', 'area' - // more could be added here (Fourier coeff, moments) - getFeatures: function(features) { - if (!(features instanceof Array)) { - features = [features]; - } - var result = this.toTriangles().map(function(triPoly) { - return triPoly.getTetraFeatures(features); - }) - .reduce(function(pv, v) { - return v.map(function(feat, i) { - return feat + (pv === 0 ? 0 : pv[i]); - }); - }, 0); - return (result.length == 1) ? result[0] : result; - } - }; - - // Parse an option from the options object - // If the option is not present, return the default value - CSG.parseOption = function(options, optionname, defaultvalue) { - var result = defaultvalue; - if (options) { - if (optionname in options) { - result = options[optionname]; - } - } - return result; - }; - - // Parse an option and force into a CSG.Vector3D. If a scalar is passed it is converted - // into a vector with equal x,y,z - CSG.parseOptionAs3DVector = function(options, optionname, defaultvalue) { - var result = CSG.parseOption(options, optionname, defaultvalue); - result = new CSG.Vector3D(result); - return result; - }; - - CSG.parseOptionAs3DVectorList = function(options, optionname, defaultvalue) { - var result = CSG.parseOption(options, optionname, defaultvalue); - return result.map(function(res) { - return new CSG.Vector3D(res); - }); - }; - - // Parse an option and force into a CSG.Vector2D. If a scalar is passed it is converted - // into a vector with equal x,y - CSG.parseOptionAs2DVector = function(options, optionname, defaultvalue) { - var result = CSG.parseOption(options, optionname, defaultvalue); - result = new CSG.Vector2D(result); - return result; - }; - - CSG.parseOptionAsFloat = function(options, optionname, defaultvalue) { - var result = CSG.parseOption(options, optionname, defaultvalue); - if (typeof(result) == "string") { - result = Number(result); - } - if (isNaN(result) || typeof(result) != "number") { - throw new Error("Parameter " + optionname + " should be a number"); - } - return result; - }; - - CSG.parseOptionAsInt = function(options, optionname, defaultvalue) { - var result = CSG.parseOption(options, optionname, defaultvalue); - result = Number(Math.floor(result)); - if (isNaN(result)) { - throw new Error("Parameter " + optionname + " should be a number"); - } - return result; - }; - - CSG.parseOptionAsBool = function(options, optionname, defaultvalue) { - var result = CSG.parseOption(options, optionname, defaultvalue); - if (typeof(result) == "string") { - if (result == "true") result = true; - else if (result == "false") result = false; - else if (result == 0) result = false; - } - result = !!result; - return result; - }; - - // Construct an axis-aligned solid cuboid. - // Parameters: - // center: center of cube (default [0,0,0]) - // radius: radius of cube (default [1,1,1]), can be specified as scalar or as 3D vector - // - // Example code: - // - // var cube = CSG.cube({ - // center: [0, 0, 0], - // radius: 1 - // }); - CSG.cube = function(options) { - var c, r; - options = options || {}; - if (('corner1' in options) || ('corner2' in options)) { - if (('center' in options) || ('radius' in options)) { - throw new Error("cube: should either give a radius and center parameter, or a corner1 and corner2 parameter") - } - corner1 = CSG.parseOptionAs3DVector(options, "corner1", [0, 0, 0]); - corner2 = CSG.parseOptionAs3DVector(options, "corner2", [1, 1, 1]); - c = corner1.plus(corner2).times(0.5); - r = corner2.minus(corner1).times(0.5); - } else { - c = CSG.parseOptionAs3DVector(options, "center", [0, 0, 0]); - r = CSG.parseOptionAs3DVector(options, "radius", [1, 1, 1]); - } - r = r.abs(); // negative radii make no sense - var result = CSG.fromPolygons([ - [ - [0, 4, 6, 2], - [-1, 0, 0] - ], - [ - [1, 3, 7, 5], - [+1, 0, 0] - ], - [ - [0, 1, 5, 4], - [0, -1, 0] - ], - [ - [2, 6, 7, 3], - [0, +1, 0] - ], - [ - [0, 2, 3, 1], - [0, 0, -1] - ], - [ - [4, 5, 7, 6], - [0, 0, +1] - ] - ].map(function(info) { - //var normal = new CSG.Vector3D(info[1]); - //var plane = new CSG.Plane(normal, 1); - var vertices = info[0].map(function(i) { - var pos = new CSG.Vector3D( - c.x + r.x * (2 * !!(i & 1) - 1), c.y + r.y * (2 * !!(i & 2) - 1), c.z + r.z * (2 * !!(i & 4) - 1)); - return new CSG.Vertex(pos); - }); - return new CSG.Polygon(vertices, null /* , plane */ ); - })); - result.properties.cube = new CSG.Properties(); - result.properties.cube.center = new CSG.Vector3D(c); - // add 6 connectors, at the centers of each face: - result.properties.cube.facecenters = [ - new CSG.Connector(new CSG.Vector3D([r.x, 0, 0]).plus(c), [1, 0, 0], [0, 0, 1]), - new CSG.Connector(new CSG.Vector3D([-r.x, 0, 0]).plus(c), [-1, 0, 0], [0, 0, 1]), - new CSG.Connector(new CSG.Vector3D([0, r.y, 0]).plus(c), [0, 1, 0], [0, 0, 1]), - new CSG.Connector(new CSG.Vector3D([0, -r.y, 0]).plus(c), [0, -1, 0], [0, 0, 1]), - new CSG.Connector(new CSG.Vector3D([0, 0, r.z]).plus(c), [0, 0, 1], [1, 0, 0]), - new CSG.Connector(new CSG.Vector3D([0, 0, -r.z]).plus(c), [0, 0, -1], [1, 0, 0]) - ]; - return result; - }; - - // Construct a solid sphere - // - // Parameters: - // center: center of sphere (default [0,0,0]) - // radius: radius of sphere (default 1), must be a scalar - // resolution: determines the number of polygons per 360 degree revolution (default 12) - // axes: (optional) an array with 3 vectors for the x, y and z base vectors - // - // Example usage: - // - // var sphere = CSG.sphere({ - // center: [0, 0, 0], - // radius: 2, - // resolution: 32, - // }); - CSG.sphere = function(options) { - options = options || {}; - var center = CSG.parseOptionAs3DVector(options, "center", [0, 0, 0]); - var radius = CSG.parseOptionAsFloat(options, "radius", 1); - var resolution = CSG.parseOptionAsInt(options, "resolution", CSG.defaultResolution3D); - var xvector, yvector, zvector; - if ('axes' in options) { - xvector = options.axes[0].unit().times(radius); - yvector = options.axes[1].unit().times(radius); - zvector = options.axes[2].unit().times(radius); - } else { - xvector = new CSG.Vector3D([1, 0, 0]).times(radius); - yvector = new CSG.Vector3D([0, -1, 0]).times(radius); - zvector = new CSG.Vector3D([0, 0, 1]).times(radius); - } - if (resolution < 4) resolution = 4; - var qresolution = Math.round(resolution / 4); - var prevcylinderpoint; - var polygons = []; - for (var slice1 = 0; slice1 <= resolution; slice1++) { - var angle = Math.PI * 2.0 * slice1 / resolution; - var cylinderpoint = xvector.times(Math.cos(angle)).plus(yvector.times(Math.sin(angle))); - if (slice1 > 0) { - // cylinder vertices: - var vertices = []; - var prevcospitch, prevsinpitch; - for (var slice2 = 0; slice2 <= qresolution; slice2++) { - var pitch = 0.5 * Math.PI * slice2 / qresolution; - var cospitch = Math.cos(pitch); - var sinpitch = Math.sin(pitch); - if (slice2 > 0) { - vertices = []; - vertices.push(new CSG.Vertex(center.plus(prevcylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch))))); - vertices.push(new CSG.Vertex(center.plus(cylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch))))); - if (slice2 < qresolution) { - vertices.push(new CSG.Vertex(center.plus(cylinderpoint.times(cospitch).minus(zvector.times(sinpitch))))); - } - vertices.push(new CSG.Vertex(center.plus(prevcylinderpoint.times(cospitch).minus(zvector.times(sinpitch))))); - polygons.push(new CSG.Polygon(vertices)); - vertices = []; - vertices.push(new CSG.Vertex(center.plus(prevcylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch))))); - vertices.push(new CSG.Vertex(center.plus(cylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch))))); - if (slice2 < qresolution) { - vertices.push(new CSG.Vertex(center.plus(cylinderpoint.times(cospitch).plus(zvector.times(sinpitch))))); - } - vertices.push(new CSG.Vertex(center.plus(prevcylinderpoint.times(cospitch).plus(zvector.times(sinpitch))))); - vertices.reverse(); - polygons.push(new CSG.Polygon(vertices)); - } - prevcospitch = cospitch; - prevsinpitch = sinpitch; - } - } - prevcylinderpoint = cylinderpoint; - } - var result = CSG.fromPolygons(polygons); - result.properties.sphere = new CSG.Properties(); - result.properties.sphere.center = new CSG.Vector3D(center); - result.properties.sphere.facepoint = center.plus(xvector); - return result; - }; - - // Construct a solid cylinder. - // - // Parameters: - // start: start point of cylinder (default [0, -1, 0]) - // end: end point of cylinder (default [0, 1, 0]) - // radius: radius of cylinder (default 1), must be a scalar - // resolution: determines the number of polygons per 360 degree revolution (default 12) - // - // Example usage: - // - // var cylinder = CSG.cylinder({ - // start: [0, -1, 0], - // end: [0, 1, 0], - // radius: 1, - // resolution: 16 - // }); - CSG.cylinder = function(options) { - var s = CSG.parseOptionAs3DVector(options, "start", [0, -1, 0]); - var e = CSG.parseOptionAs3DVector(options, "end", [0, 1, 0]); - var r = CSG.parseOptionAsFloat(options, "radius", 1); - var rEnd = CSG.parseOptionAsFloat(options, "radiusEnd", r); - var rStart = CSG.parseOptionAsFloat(options, "radiusStart", r); - var alpha = CSG.parseOptionAsFloat(options, "sectorAngle", 360); - alpha = alpha > 360 ? alpha % 360 : alpha; - - if ((rEnd < 0) || (rStart < 0)) { - throw new Error("Radius should be non-negative"); - } - if ((rEnd === 0) && (rStart === 0)) { - throw new Error("Either radiusStart or radiusEnd should be positive"); - } - - var slices = CSG.parseOptionAsInt(options, "resolution", CSG.defaultResolution2D); - var ray = e.minus(s); - var axisZ = ray.unit(); //, isY = (Math.abs(axisZ.y) > 0.5); - var axisX = axisZ.randomNonParallelVector().unit(); - - // var axisX = new CSG.Vector3D(isY, !isY, 0).cross(axisZ).unit(); - var axisY = axisX.cross(axisZ).unit(); - var start = new CSG.Vertex(s); - var end = new CSG.Vertex(e); - var polygons = []; - - function point(stack, slice, radius) { - var angle = slice * Math.PI * alpha / 180; - var out = axisX.times(Math.cos(angle)).plus(axisY.times(Math.sin(angle))); - var pos = s.plus(ray.times(stack)).plus(out.times(radius)); - return new CSG.Vertex(pos); - } - if (alpha > 0) { - for (var i = 0; i < slices; i++) { - var t0 = i / slices, - t1 = (i + 1) / slices; - if (rEnd == rStart) { - polygons.push(new CSG.Polygon([start, point(0, t0, rEnd), point(0, t1, rEnd)])); - polygons.push(new CSG.Polygon([point(0, t1, rEnd), point(0, t0, rEnd), point(1, t0, rEnd), point(1, t1, rEnd)])); - polygons.push(new CSG.Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)])); - } else { - if (rStart > 0) { - polygons.push(new CSG.Polygon([start, point(0, t0, rStart), point(0, t1, rStart)])); - polygons.push(new CSG.Polygon([point(0, t0, rStart), point(1, t0, rEnd), point(0, t1, rStart)])); - } - if (rEnd > 0) { - polygons.push(new CSG.Polygon([end, point(1, t1, rEnd), point(1, t0, rEnd)])); - polygons.push(new CSG.Polygon([point(1, t0, rEnd), point(1, t1, rEnd), point(0, t1, rStart)])); - } - } - } - if (alpha < 360) { - polygons.push(new CSG.Polygon([start, end, point(0, 0, rStart)])); - polygons.push(new CSG.Polygon([point(0, 0, rStart), end, point(1, 0, rEnd)])); - polygons.push(new CSG.Polygon([start, point(0, 1, rStart), end])); - polygons.push(new CSG.Polygon([point(0, 1, rStart), point(1, 1, rEnd), end])); - } - } - var result = CSG.fromPolygons(polygons); - result.properties.cylinder = new CSG.Properties(); - result.properties.cylinder.start = new CSG.Connector(s, axisZ.negated(), axisX); - result.properties.cylinder.end = new CSG.Connector(e, axisZ, axisX); - var cylCenter = s.plus(ray.times(0.5)); - var fptVec = axisX.rotate(s, axisZ, -alpha / 2).times((rStart + rEnd) / 2); - var fptVec90 = fptVec.cross(axisZ); - // note this one is NOT a face normal for a cone. - It's horizontal from cyl perspective - result.properties.cylinder.facepointH = new CSG.Connector(cylCenter.plus(fptVec), fptVec, axisZ); - result.properties.cylinder.facepointH90 = new CSG.Connector(cylCenter.plus(fptVec90), fptVec90, axisZ); - return result; - }; - - // Like a cylinder, but with rounded ends instead of flat - // - // Parameters: - // start: start point of cylinder (default [0, -1, 0]) - // end: end point of cylinder (default [0, 1, 0]) - // radius: radius of cylinder (default 1), must be a scalar - // resolution: determines the number of polygons per 360 degree revolution (default 12) - // normal: a vector determining the starting angle for tesselation. Should be non-parallel to start.minus(end) - // - // Example usage: - // - // var cylinder = CSG.roundedCylinder({ - // start: [0, -1, 0], - // end: [0, 1, 0], - // radius: 1, - // resolution: 16 - // }); - CSG.roundedCylinder = function(options) { - var p1 = CSG.parseOptionAs3DVector(options, "start", [0, -1, 0]); - var p2 = CSG.parseOptionAs3DVector(options, "end", [0, 1, 0]); - var radius = CSG.parseOptionAsFloat(options, "radius", 1); - var direction = p2.minus(p1); - var defaultnormal; - if (Math.abs(direction.x) > Math.abs(direction.y)) { - defaultnormal = new CSG.Vector3D(0, 1, 0); - } else { - defaultnormal = new CSG.Vector3D(1, 0, 0); - } - var normal = CSG.parseOptionAs3DVector(options, "normal", defaultnormal); - var resolution = CSG.parseOptionAsInt(options, "resolution", CSG.defaultResolution3D); - if (resolution < 4) resolution = 4; - var polygons = []; - var qresolution = Math.floor(0.25 * resolution); - var length = direction.length(); - if (length < 1e-10) { - return CSG.sphere({ - center: p1, - radius: radius, - resolution: resolution - }); - } - var zvector = direction.unit().times(radius); - var xvector = zvector.cross(normal).unit().times(radius); - var yvector = xvector.cross(zvector).unit().times(radius); - var prevcylinderpoint; - for (var slice1 = 0; slice1 <= resolution; slice1++) { - var angle = Math.PI * 2.0 * slice1 / resolution; - var cylinderpoint = xvector.times(Math.cos(angle)).plus(yvector.times(Math.sin(angle))); - if (slice1 > 0) { - // cylinder vertices: - var vertices = []; - vertices.push(new CSG.Vertex(p1.plus(cylinderpoint))); - vertices.push(new CSG.Vertex(p1.plus(prevcylinderpoint))); - vertices.push(new CSG.Vertex(p2.plus(prevcylinderpoint))); - vertices.push(new CSG.Vertex(p2.plus(cylinderpoint))); - polygons.push(new CSG.Polygon(vertices)); - var prevcospitch, prevsinpitch; - for (var slice2 = 0; slice2 <= qresolution; slice2++) { - var pitch = 0.5 * Math.PI * slice2 / qresolution; - //var pitch = Math.asin(slice2/qresolution); - var cospitch = Math.cos(pitch); - var sinpitch = Math.sin(pitch); - if (slice2 > 0) { - vertices = []; - vertices.push(new CSG.Vertex(p1.plus(prevcylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch))))); - vertices.push(new CSG.Vertex(p1.plus(cylinderpoint.times(prevcospitch).minus(zvector.times(prevsinpitch))))); - if (slice2 < qresolution) { - vertices.push(new CSG.Vertex(p1.plus(cylinderpoint.times(cospitch).minus(zvector.times(sinpitch))))); - } - vertices.push(new CSG.Vertex(p1.plus(prevcylinderpoint.times(cospitch).minus(zvector.times(sinpitch))))); - polygons.push(new CSG.Polygon(vertices)); - vertices = []; - vertices.push(new CSG.Vertex(p2.plus(prevcylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch))))); - vertices.push(new CSG.Vertex(p2.plus(cylinderpoint.times(prevcospitch).plus(zvector.times(prevsinpitch))))); - if (slice2 < qresolution) { - vertices.push(new CSG.Vertex(p2.plus(cylinderpoint.times(cospitch).plus(zvector.times(sinpitch))))); - } - vertices.push(new CSG.Vertex(p2.plus(prevcylinderpoint.times(cospitch).plus(zvector.times(sinpitch))))); - vertices.reverse(); - polygons.push(new CSG.Polygon(vertices)); - } - prevcospitch = cospitch; - prevsinpitch = sinpitch; - } - } - prevcylinderpoint = cylinderpoint; - } - var result = CSG.fromPolygons(polygons); - var ray = zvector.unit(); - var axisX = xvector.unit(); - result.properties.roundedCylinder = new CSG.Properties(); - result.properties.roundedCylinder.start = new CSG.Connector(p1, ray.negated(), axisX); - result.properties.roundedCylinder.end = new CSG.Connector(p2, ray, axisX); - result.properties.roundedCylinder.facepoint = p1.plus(xvector); - return result; - }; - - // Construct an axis-aligned solid rounded cuboid. - // Parameters: - // center: center of cube (default [0,0,0]) - // radius: radius of cube (default [1,1,1]), can be specified as scalar or as 3D vector - // roundradius: radius of rounded corners (default 0.2), must be a scalar - // resolution: determines the number of polygons per 360 degree revolution (default 8) - // - // Example code: - // - // var cube = CSG.roundedCube({ - // center: [0, 0, 0], - // radius: 1, - // roundradius: 0.2, - // resolution: 8, - // }); - CSG.roundedCube = function(options) { - var EPS = 1e-5; - var minRR = 1e-2; //minroundradius 1e-3 gives rounding errors already - var center, cuberadius; - options = options || {}; - if (('corner1' in options) || ('corner2' in options)) { - if (('center' in options) || ('radius' in options)) { - throw new Error("roundedCube: should either give a radius and center parameter, or a corner1 and corner2 parameter"); - } - corner1 = CSG.parseOptionAs3DVector(options, "corner1", [0, 0, 0]); - corner2 = CSG.parseOptionAs3DVector(options, "corner2", [1, 1, 1]); - center = corner1.plus(corner2).times(0.5); - cuberadius = corner2.minus(corner1).times(0.5); - } else { - center = CSG.parseOptionAs3DVector(options, "center", [0, 0, 0]); - cuberadius = CSG.parseOptionAs3DVector(options, "radius", [1, 1, 1]); - } - cuberadius = cuberadius.abs(); // negative radii make no sense - var resolution = CSG.parseOptionAsInt(options, "resolution", CSG.defaultResolution3D); - if (resolution < 4) resolution = 4; - if (resolution%2 == 1 && resolution < 8) resolution = 8; // avoid ugly - var roundradius = CSG.parseOptionAs3DVector(options, "roundradius", [0.2, 0.2, 0.2]); - // slight hack for now - total radius stays ok - roundradius = CSG.Vector3D.Create(Math.max(roundradius.x, minRR), Math.max(roundradius.y, minRR), Math.max(roundradius.z, minRR)); - var innerradius = cuberadius.minus(roundradius); - if (innerradius.x < 0 || innerradius.y < 0 || innerradius.z < 0) { - throw('roundradius <= radius!'); - } - var res = CSG.sphere({radius:1, resolution:resolution}); - res = res.scale(roundradius); - innerradius.x > EPS && (res = res.stretchAtPlane([1, 0, 0], [0, 0, 0], 2*innerradius.x)); - innerradius.y > EPS && (res = res.stretchAtPlane([0, 1, 0], [0, 0, 0], 2*innerradius.y)); - innerradius.z > EPS && (res = res.stretchAtPlane([0, 0, 1], [0, 0, 0], 2*innerradius.z)); - res = res.translate([-innerradius.x+center.x, -innerradius.y+center.y, -innerradius.z+center.z]); - res = res.reTesselated(); - res.properties.roundedCube = new CSG.Properties(); - res.properties.roundedCube.center = new CSG.Vertex(center); - res.properties.roundedCube.facecenters = [ - new CSG.Connector(new CSG.Vector3D([cuberadius.x, 0, 0]).plus(center), [1, 0, 0], [0, 0, 1]), - new CSG.Connector(new CSG.Vector3D([-cuberadius.x, 0, 0]).plus(center), [-1, 0, 0], [0, 0, 1]), - new CSG.Connector(new CSG.Vector3D([0, cuberadius.y, 0]).plus(center), [0, 1, 0], [0, 0, 1]), - new CSG.Connector(new CSG.Vector3D([0, -cuberadius.y, 0]).plus(center), [0, -1, 0], [0, 0, 1]), - new CSG.Connector(new CSG.Vector3D([0, 0, cuberadius.z]).plus(center), [0, 0, 1], [1, 0, 0]), - new CSG.Connector(new CSG.Vector3D([0, 0, -cuberadius.z]).plus(center), [0, 0, -1], [1, 0, 0]) - ]; - return res; - }; - - /** - * polyhedron accepts openscad style arguments. I.e. define face vertices clockwise looking from outside - */ - CSG.polyhedron = function(options) { - options = options || {}; - if (('points' in options) !== ('faces' in options)) { - throw new Error("polyhedron needs 'points' and 'faces' arrays"); - } - var vertices = CSG.parseOptionAs3DVectorList(options, "points", [ - [1, 1, 0], - [1, -1, 0], - [-1, -1, 0], - [-1, 1, 0], - [0, 0, 1] - ]) - .map(function(pt) { - return new CSG.Vertex(pt); - }); - var faces = CSG.parseOption(options, "faces", [ - [0, 1, 4], - [1, 2, 4], - [2, 3, 4], - [3, 0, 4], - [1, 0, 3], - [2, 1, 3] - ]); - // openscad convention defines inward normals - so we have to invert here - faces.forEach(function(face) { - face.reverse(); - }); - var polygons = faces.map(function(face) { - return new CSG.Polygon(face.map(function(idx) { - return vertices[idx]; - })); - }); - - // TODO: facecenters as connectors? probably overkill. Maybe centroid - // the re-tesselation here happens because it's so easy for a user to - // create parametrized polyhedrons that end up with 1-2 dimensional polygons. - // These will create infinite loops at CSG.Tree() - return CSG.fromPolygons(polygons).reTesselated(); - }; - - CSG.IsFloat = function(n) { - return (!isNaN(n)) || (n === Infinity) || (n === -Infinity); - }; - - // solve 2x2 linear equation: - // [ab][x] = [u] - // [cd][y] [v] - CSG.solve2Linear = function(a, b, c, d, u, v) { - var det = a * d - b * c; - var invdet = 1.0 / det; - var x = u * d - b * v; - var y = -u * c + a * v; - x *= invdet; - y *= invdet; - return [x, y]; - }; - - // # class Vector3D - // Represents a 3D vector. - // - // Example usage: - // - // new CSG.Vector3D(1, 2, 3); - // new CSG.Vector3D([1, 2, 3]); - // new CSG.Vector3D({ x: 1, y: 2, z: 3 }); - // new CSG.Vector3D(1, 2); // assumes z=0 - // new CSG.Vector3D([1, 2]); // assumes z=0 - CSG.Vector3D = function(x, y, z) { - if (arguments.length == 3) { - this._x = parseFloat(x); - this._y = parseFloat(y); - this._z = parseFloat(z); - } else if (arguments.length == 2) { - this._x = parseFloat(x); - this._y = parseFloat(y); - this._z = 0; - } else { - var ok = true; - if (arguments.length == 1) { - if (typeof(x) == "object") { - if (x instanceof CSG.Vector3D) { - this._x = x._x; - this._y = x._y; - this._z = x._z; - } else if (x instanceof CSG.Vector2D) { - this._x = x._x; - this._y = x._y; - this._z = 0; - } else if (x instanceof Array) { - if ((x.length < 2) || (x.length > 3)) { - ok = false; - } else { - this._x = parseFloat(x[0]); - this._y = parseFloat(x[1]); - if (x.length == 3) { - this._z = parseFloat(x[2]); - } else { - this._z = 0; - } - } - } else if (('x' in x) && ('y' in x)) { - this._x = parseFloat(x.x); - this._y = parseFloat(x.y); - if ('z' in x) { - this._z = parseFloat(x.z); - } else { - this._z = 0; - } - } else ok = false; - } else { - var v = parseFloat(x); - this._x = v; - this._y = v; - this._z = v; - } - } else ok = false; - if (ok) { - if ((!CSG.IsFloat(this._x)) || (!CSG.IsFloat(this._y)) || (!CSG.IsFloat(this._z))) ok = false; - } - if (!ok) { - throw new Error("wrong arguments"); - } - } - }; - - // This does the same as new CSG.Vector3D(x,y,z) but it doesn't go through the constructor - // and the parameters are not validated. Is much faster. - CSG.Vector3D.Create = function(x, y, z) { - var result = Object.create(CSG.Vector3D.prototype); - result._x = x; - result._y = y; - result._z = z; - return result; - }; - - CSG.Vector3D.prototype = { - get x() { - return this._x; - }, - get y() { - return this._y; - }, - get z() { - return this._z; - }, - - set x(v) { - throw new Error("Vector3D is immutable"); - }, - set y(v) { - throw new Error("Vector3D is immutable"); - }, - set z(v) { - throw new Error("Vector3D is immutable"); - }, - - clone: function() { - return CSG.Vector3D.Create(this._x, this._y, this._z); - }, - - negated: function() { - return CSG.Vector3D.Create(-this._x, -this._y, -this._z); - }, - - abs: function() { - return CSG.Vector3D.Create(Math.abs(this._x), Math.abs(this._y), Math.abs(this._z)); - }, - - plus: function(a) { - return CSG.Vector3D.Create(this._x + a._x, this._y + a._y, this._z + a._z); - }, - - minus: function(a) { - return CSG.Vector3D.Create(this._x - a._x, this._y - a._y, this._z - a._z); - }, - - times: function(a) { - return CSG.Vector3D.Create(this._x * a, this._y * a, this._z * a); - }, - - dividedBy: function(a) { - return CSG.Vector3D.Create(this._x / a, this._y / a, this._z / a); - }, - - dot: function(a) { - return this._x * a._x + this._y * a._y + this._z * a._z; - }, - - lerp: function(a, t) { - return this.plus(a.minus(this).times(t)); - }, - - lengthSquared: function() { - return this.dot(this); - }, - - length: function() { - return Math.sqrt(this.lengthSquared()); - }, - - unit: function() { - return this.dividedBy(this.length()); - }, - - cross: function(a) { - return CSG.Vector3D.Create( - this._y * a._z - this._z * a._y, this._z * a._x - this._x * a._z, this._x * a._y - this._y * a._x); - }, - - distanceTo: function(a) { - return this.minus(a).length(); - }, - - distanceToSquared: function(a) { - return this.minus(a).lengthSquared(); - }, - - equals: function(a) { - return (this._x == a._x) && (this._y == a._y) && (this._z == a._z); - }, - - // Right multiply by a 4x4 matrix (the vector is interpreted as a row vector) - // Returns a new CSG.Vector3D - multiply4x4: function(matrix4x4) { - return matrix4x4.leftMultiply1x3Vector(this); - }, - - transform: function(matrix4x4) { - return matrix4x4.leftMultiply1x3Vector(this); - }, - - toString: function() { - return "(" + this._x.toFixed(2) + ", " + this._y.toFixed(2) + ", " + this._z.toFixed(2) + ")"; - }, - - // find a vector that is somewhat perpendicular to this one - randomNonParallelVector: function() { - var abs = this.abs(); - if ((abs._x <= abs._y) && (abs._x <= abs._z)) { - return CSG.Vector3D.Create(1, 0, 0); - } else if ((abs._y <= abs._x) && (abs._y <= abs._z)) { - return CSG.Vector3D.Create(0, 1, 0); - } else { - return CSG.Vector3D.Create(0, 0, 1); - } - }, - - min: function(p) { - return CSG.Vector3D.Create( - Math.min(this._x, p._x), Math.min(this._y, p._y), Math.min(this._z, p._z)); - }, - - max: function(p) { - return CSG.Vector3D.Create( - Math.max(this._x, p._x), Math.max(this._y, p._y), Math.max(this._z, p._z)); - } - }; - - // # class Vertex - // Represents a vertex of a polygon. Use your own vertex class instead of this - // one to provide additional features like texture coordinates and vertex - // colors. Custom vertex classes need to provide a `pos` property - // `flipped()`, and `interpolate()` methods that behave analogous to the ones - // defined by `CSG.Vertex`. - CSG.Vertex = function(pos) { - this.pos = pos; - }; - - // create from an untyped object with identical property names: - CSG.Vertex.fromObject = function(obj) { - var pos = new CSG.Vector3D(obj.pos); - return new CSG.Vertex(pos); - }; - - CSG.Vertex.prototype = { - // Return a vertex with all orientation-specific data (e.g. vertex normal) flipped. Called when the - // orientation of a polygon is flipped. - flipped: function() { - return this; - }, - - getTag: function() { - var result = this.tag; - if (!result) { - result = CSG.getTag(); - this.tag = result; - } - return result; - }, - - // Create a new vertex between this vertex and `other` by linearly - // interpolating all properties using a parameter of `t`. Subclasses should - // override this to interpolate additional properties. - interpolate: function(other, t) { - var newpos = this.pos.lerp(other.pos, t); - return new CSG.Vertex(newpos); - }, - - // Affine transformation of vertex. Returns a new CSG.Vertex - transform: function(matrix4x4) { - var newpos = this.pos.multiply4x4(matrix4x4); - return new CSG.Vertex(newpos); - }, - - toString: function() { - return this.pos.toString(); - } - }; - - // # class Plane - // Represents a plane in 3D space. - CSG.Plane = function(normal, w) { - this.normal = normal; - this.w = w; - }; - - // create from an untyped object with identical property names: - CSG.Plane.fromObject = function(obj) { - var normal = new CSG.Vector3D(obj.normal); - var w = parseFloat(obj.w); - return new CSG.Plane(normal, w); - }; - - // `CSG.Plane.EPSILON` is the tolerance used by `splitPolygon()` to decide if a - // point is on the plane. - CSG.Plane.EPSILON = 1e-5; - - CSG.Plane.fromVector3Ds = function(a, b, c) { - var n = b.minus(a).cross(c.minus(a)).unit(); - return new CSG.Plane(n, n.dot(a)); - }; - - // like fromVector3Ds, but allow the vectors to be on one point or one line - // in such a case a random plane through the given points is constructed - CSG.Plane.anyPlaneFromVector3Ds = function(a, b, c) { - var v1 = b.minus(a); - var v2 = c.minus(a); - if (v1.length() < 1e-5) { - v1 = v2.randomNonParallelVector(); - } - if (v2.length() < 1e-5) { - v2 = v1.randomNonParallelVector(); - } - var normal = v1.cross(v2); - if (normal.length() < 1e-5) { - // this would mean that v1 == v2.negated() - v2 = v1.randomNonParallelVector(); - normal = v1.cross(v2); - } - normal = normal.unit(); - return new CSG.Plane(normal, normal.dot(a)); - }; - - CSG.Plane.fromPoints = function(a, b, c) { - a = new CSG.Vector3D(a); - b = new CSG.Vector3D(b); - c = new CSG.Vector3D(c); - return CSG.Plane.fromVector3Ds(a, b, c); - }; - - CSG.Plane.fromNormalAndPoint = function(normal, point) { - normal = new CSG.Vector3D(normal); - point = new CSG.Vector3D(point); - normal = normal.unit(); - var w = point.dot(normal); - return new CSG.Plane(normal, w); - }; - - CSG.Plane.prototype = { - flipped: function() { - return new CSG.Plane(this.normal.negated(), -this.w); - }, - - getTag: function() { - var result = this.tag; - if (!result) { - result = CSG.getTag(); - this.tag = result; - } - return result; - }, - - equals: function(n) { - return this.normal.equals(n.normal) && this.w == n.w; - }, - - transform: function(matrix4x4) { - var ismirror = matrix4x4.isMirroring(); - // get two vectors in the plane: - var r = this.normal.randomNonParallelVector(); - var u = this.normal.cross(r); - var v = this.normal.cross(u); - // get 3 points in the plane: - var point1 = this.normal.times(this.w); - var point2 = point1.plus(u); - var point3 = point1.plus(v); - // transform the points: - point1 = point1.multiply4x4(matrix4x4); - point2 = point2.multiply4x4(matrix4x4); - point3 = point3.multiply4x4(matrix4x4); - // and create a new plane from the transformed points: - var newplane = CSG.Plane.fromVector3Ds(point1, point2, point3); - if (ismirror) { - // the transform is mirroring - // We should mirror the plane: - newplane = newplane.flipped(); - } - return newplane; - }, - - // Returns object: - // .type: - // 0: coplanar-front - // 1: coplanar-back - // 2: front - // 3: back - // 4: spanning - // In case the polygon is spanning, returns: - // .front: a CSG.Polygon of the front part - // .back: a CSG.Polygon of the back part - splitPolygon: function(polygon) { - var result = { - type: null, - front: null, - back: null - }; - // cache in local vars (speedup): - var planenormal = this.normal; - var vertices = polygon.vertices; - var numvertices = vertices.length; - if (polygon.plane.equals(this)) { - result.type = 0; - } else { - var EPS = CSG.Plane.EPSILON; - var thisw = this.w; - var hasfront = false; - var hasback = false; - var vertexIsBack = []; - var MINEPS = -EPS; - for (var i = 0; i < numvertices; i++) { - var t = planenormal.dot(vertices[i].pos) - thisw; - var isback = (t < 0); - vertexIsBack.push(isback); - if (t > EPS) hasfront = true; - if (t < MINEPS) hasback = true; - } - if ((!hasfront) && (!hasback)) { - // all points coplanar - var t = planenormal.dot(polygon.plane.normal); - result.type = (t >= 0) ? 0 : 1; - } else if (!hasback) { - result.type = 2; - } else if (!hasfront) { - result.type = 3; - } else { - // spanning - result.type = 4; - var frontvertices = [], - backvertices = []; - var isback = vertexIsBack[0]; - for (var vertexindex = 0; vertexindex < numvertices; vertexindex++) { - var vertex = vertices[vertexindex]; - var nextvertexindex = vertexindex + 1; - if (nextvertexindex >= numvertices) nextvertexindex = 0; - var nextisback = vertexIsBack[nextvertexindex]; - if (isback == nextisback) { - // line segment is on one side of the plane: - if (isback) { - backvertices.push(vertex); - } else { - frontvertices.push(vertex); - } - } else { - // line segment intersects plane: - var point = vertex.pos; - var nextpoint = vertices[nextvertexindex].pos; - var intersectionpoint = this.splitLineBetweenPoints(point, nextpoint); - var intersectionvertex = new CSG.Vertex(intersectionpoint); - if (isback) { - backvertices.push(vertex); - backvertices.push(intersectionvertex); - frontvertices.push(intersectionvertex); - } else { - frontvertices.push(vertex); - frontvertices.push(intersectionvertex); - backvertices.push(intersectionvertex); - } - } - isback = nextisback; - } // for vertexindex - // remove duplicate vertices: - var EPS_SQUARED = CSG.Plane.EPSILON * CSG.Plane.EPSILON; - if (backvertices.length >= 3) { - var prevvertex = backvertices[backvertices.length - 1]; - for (var vertexindex = 0; vertexindex < backvertices.length; vertexindex++) { - var vertex = backvertices[vertexindex]; - if (vertex.pos.distanceToSquared(prevvertex.pos) < EPS_SQUARED) { - backvertices.splice(vertexindex, 1); - vertexindex--; - } - prevvertex = vertex; - } - } - if (frontvertices.length >= 3) { - var prevvertex = frontvertices[frontvertices.length - 1]; - for (var vertexindex = 0; vertexindex < frontvertices.length; vertexindex++) { - var vertex = frontvertices[vertexindex]; - if (vertex.pos.distanceToSquared(prevvertex.pos) < EPS_SQUARED) { - frontvertices.splice(vertexindex, 1); - vertexindex--; - } - prevvertex = vertex; - } - } - if (frontvertices.length >= 3) { - result.front = new CSG.Polygon(frontvertices, polygon.shared, polygon.plane); - } - if (backvertices.length >= 3) { - result.back = new CSG.Polygon(backvertices, polygon.shared, polygon.plane); - } - } - } - return result; - }, - - // robust splitting of a line by a plane - // will work even if the line is parallel to the plane - splitLineBetweenPoints: function(p1, p2) { - var direction = p2.minus(p1); - var labda = (this.w - this.normal.dot(p1)) / this.normal.dot(direction); - if (isNaN(labda)) labda = 0; - if (labda > 1) labda = 1; - if (labda < 0) labda = 0; - var result = p1.plus(direction.times(labda)); - return result; - }, - - // returns CSG.Vector3D - intersectWithLine: function(line3d) { - return line3d.intersectWithPlane(this); - }, - - // intersection of two planes - intersectWithPlane: function(plane) { - return CSG.Line3D.fromPlanes(this, plane); - }, - - signedDistanceToPoint: function(point) { - var t = this.normal.dot(point) - this.w; - return t; - }, - - toString: function() { - return "[normal: " + this.normal.toString() + ", w: " + this.w + "]"; - }, - - mirrorPoint: function(point3d) { - var distance = this.signedDistanceToPoint(point3d); - var mirrored = point3d.minus(this.normal.times(distance * 2.0)); - return mirrored; - } - }; - - - // # class Polygon - // Represents a convex polygon. The vertices used to initialize a polygon must - // be coplanar and form a convex loop. They do not have to be `CSG.Vertex` - // instances but they must behave similarly (duck typing can be used for - // customization). - // - // Each convex polygon has a `shared` property, which is shared between all - // polygons that are clones of each other or were split from the same polygon. - // This can be used to define per-polygon properties (such as surface color). - // - // The plane of the polygon is calculated from the vertex coordinates - // To avoid unnecessary recalculation, the plane can alternatively be - // passed as the third argument - CSG.Polygon = function(vertices, shared, plane) { - this.vertices = vertices; - if (!shared) shared = CSG.Polygon.defaultShared; - this.shared = shared; - //var numvertices = vertices.length; - - if (arguments.length >= 3) { - this.plane = plane; - } else { - this.plane = CSG.Plane.fromVector3Ds(vertices[0].pos, vertices[1].pos, vertices[2].pos); - } - - if (_CSGDEBUG) { - this.checkIfConvex(); - } - }; - - // create from an untyped object with identical property names: - CSG.Polygon.fromObject = function(obj) { - var vertices = obj.vertices.map(function(v) { - return CSG.Vertex.fromObject(v); - }); - var shared = CSG.Polygon.Shared.fromObject(obj.shared); - var plane = CSG.Plane.fromObject(obj.plane); - return new CSG.Polygon(vertices, shared, plane); - }; - - CSG.Polygon.prototype = { - // check whether the polygon is convex (it should be, otherwise we will get unexpected results) - checkIfConvex: function() { - if (!CSG.Polygon.verticesConvex(this.vertices, this.plane.normal)) { - CSG.Polygon.verticesConvex(this.vertices, this.plane.normal); - throw new Error("Not convex!"); - } - }, - - setColor: function(args) { - var newshared = CSG.Polygon.Shared.fromColor.apply(this, arguments); - this.shared = newshared; - return this; - }, - - getSignedVolume: function() { - var signedVolume = 0; - for (var i = 0; i < this.vertices.length - 2; i++) { - signedVolume += this.vertices[0].pos.dot(this.vertices[i+1].pos - .cross(this.vertices[i+2].pos)); - } - signedVolume /= 6; - return signedVolume; - }, - - // Note: could calculate vectors only once to speed up - getArea: function() { - var polygonArea = 0; - for (var i = 0; i < this.vertices.length - 2; i++) { - polygonArea += this.vertices[i+1].pos.minus(this.vertices[0].pos) - .cross(this.vertices[i+2].pos.minus(this.vertices[i+1].pos)).length(); - } - polygonArea /= 2; - return polygonArea; - }, - - - // accepts array of features to calculate - // returns array of results - getTetraFeatures: function(features) { - var result = []; - features.forEach(function(feature) { - if (feature == 'volume') { - result.push(this.getSignedVolume()); - } else if (feature == 'area') { - result.push(this.getArea()); - } - }, this); - return result; - }, - - // Extrude a polygon into the direction offsetvector - // Returns a CSG object - extrude: function(offsetvector) { - var newpolygons = []; - - var polygon1 = this; - var direction = polygon1.plane.normal.dot(offsetvector); - if (direction > 0) { - polygon1 = polygon1.flipped(); - } - newpolygons.push(polygon1); - var polygon2 = polygon1.translate(offsetvector); - var numvertices = this.vertices.length; - for (var i = 0; i < numvertices; i++) { - var sidefacepoints = []; - var nexti = (i < (numvertices - 1)) ? i + 1 : 0; - sidefacepoints.push(polygon1.vertices[i].pos); - sidefacepoints.push(polygon2.vertices[i].pos); - sidefacepoints.push(polygon2.vertices[nexti].pos); - sidefacepoints.push(polygon1.vertices[nexti].pos); - var sidefacepolygon = CSG.Polygon.createFromPoints(sidefacepoints, this.shared); - newpolygons.push(sidefacepolygon); - } - polygon2 = polygon2.flipped(); - newpolygons.push(polygon2); - return CSG.fromPolygons(newpolygons); - }, - - translate: function(offset) { - return this.transform(CSG.Matrix4x4.translation(offset)); - }, - - // returns an array with a CSG.Vector3D (center point) and a radius - boundingSphere: function() { - if (!this.cachedBoundingSphere) { - var box = this.boundingBox(); - var middle = box[0].plus(box[1]).times(0.5); - var radius3 = box[1].minus(middle); - var radius = radius3.length(); - this.cachedBoundingSphere = [middle, radius]; - } - return this.cachedBoundingSphere; - }, - - // returns an array of two CSG.Vector3Ds (minimum coordinates and maximum coordinates) - boundingBox: function() { - if (!this.cachedBoundingBox) { - var minpoint, maxpoint; - var vertices = this.vertices; - var numvertices = vertices.length; - if (numvertices === 0) { - minpoint = new CSG.Vector3D(0, 0, 0); - } else { - minpoint = vertices[0].pos; - } - maxpoint = minpoint; - for (var i = 1; i < numvertices; i++) { - var point = vertices[i].pos; - minpoint = minpoint.min(point); - maxpoint = maxpoint.max(point); - } - this.cachedBoundingBox = [minpoint, maxpoint]; - } - return this.cachedBoundingBox; - }, - - flipped: function() { - var newvertices = this.vertices.map(function(v) { - return v.flipped(); - }); - newvertices.reverse(); - var newplane = this.plane.flipped(); - return new CSG.Polygon(newvertices, this.shared, newplane); - }, - - // Affine transformation of polygon. Returns a new CSG.Polygon - transform: function(matrix4x4) { - var newvertices = this.vertices.map(function(v) { - return v.transform(matrix4x4); - }); - var newplane = this.plane.transform(matrix4x4); - if (matrix4x4.isMirroring()) { - // need to reverse the vertex order - // in order to preserve the inside/outside orientation: - newvertices.reverse(); - } - return new CSG.Polygon(newvertices, this.shared, newplane); - }, - - toString: function() { - var result = "Polygon plane: " + this.plane.toString() + "\n"; - this.vertices.map(function(vertex) { - result += " " + vertex.toString() + "\n"; - }); - return result; - }, - - // project the 3D polygon onto a plane - projectToOrthoNormalBasis: function(orthobasis) { - var points2d = this.vertices.map(function(vertex) { - return orthobasis.to2D(vertex.pos); - }); - var result = CAG.fromPointsNoCheck(points2d); - var area = result.area(); - if (Math.abs(area) < 1e-5) { - // the polygon was perpendicular to the orthnormal plane. The resulting 2D polygon would be degenerate - // return an empty area instead: - result = new CAG(); - } else if (area < 0) { - result = result.flipped(); - } - return result; - }, - - /** - * Creates solid from slices (CSG.Polygon) by generating walls - * @param {Object} options Solid generating options - * - numslices {Number} Number of slices to be generated - * - callback(t, slice) {Function} Callback function generating slices. - * arguments: t = [0..1], slice = [0..numslices - 1] - * return: CSG.Polygon or null to skip - * - loop {Boolean} no flats, only walls, it's used to generate solids like a tor - */ - solidFromSlices: function(options) { - var polygons = [], - csg = null, - prev = null, - bottom = null, - top = null, - numSlices = 2, - bLoop = false, - fnCallback, - flipped = null; - - if (options) { - bLoop = Boolean(options['loop']); - - if (options.numslices) - numSlices = options.numslices; - - if (options.callback) - fnCallback = options.callback; - } - if (!fnCallback) { - var square = new CSG.Polygon.createFromPoints([ - [0, 0, 0], - [1, 0, 0], - [1, 1, 0], - [0, 1, 0] - ]); - fnCallback = function(t, slice) { - return t == 0 || t == 1 ? square.translate([0, 0, t]) : null; - } - } - for (var i = 0, iMax = numSlices - 1; i <= iMax; i++) { - csg = fnCallback.call(this, i / iMax, i); - if (csg) { - if (!(csg instanceof CSG.Polygon)) { - throw new Error("CSG.Polygon.solidFromSlices callback error: CSG.Polygon expected"); - } - csg.checkIfConvex(); - - if (prev) { //generate walls - if (flipped === null) { //not generated yet - flipped = prev.plane.signedDistanceToPoint(csg.vertices[0].pos) < 0; - } - this._addWalls(polygons, prev, csg, flipped); - - } else { //the first - will be a bottom - bottom = csg; - } - prev = csg; - } //callback can return null to skip that slice - } - top = csg; - - if (bLoop) { - var bSameTopBottom = bottom.vertices.length == top.vertices.length && - bottom.vertices.every(function(v, index) { - return v.pos.equals(top.vertices[index].pos) - }); - //if top and bottom are not the same - - //generate walls between them - if (!bSameTopBottom) { - this._addWalls(polygons, top, bottom, flipped); - } //else - already generated - } else { - //save top and bottom - //TODO: flip if necessary - polygons.unshift(flipped ? bottom : bottom.flipped()); - polygons.push(flipped ? top.flipped() : top); - } - return CSG.fromPolygons(polygons); - }, - /** - * - * @param walls Array of wall polygons - * @param bottom Bottom polygon - * @param top Top polygon - */ - _addWalls: function(walls, bottom, top, bFlipped) { - var bottomPoints = bottom.vertices.slice(0), //make a copy - topPoints = top.vertices.slice(0), //make a copy - color = top.shared || null; - - //check if bottom perimeter is closed - if (!bottomPoints[0].pos.equals(bottomPoints[bottomPoints.length - 1].pos)) { - bottomPoints.push(bottomPoints[0]); - } - - //check if top perimeter is closed - if (!topPoints[0].pos.equals(topPoints[topPoints.length - 1].pos)) { - topPoints.push(topPoints[0]); - } - if (bFlipped) { - bottomPoints = bottomPoints.reverse(); - topPoints = topPoints.reverse(); - } - - var iTopLen = topPoints.length - 1, - iBotLen = bottomPoints.length - 1, - iExtra = iTopLen - iBotLen, //how many extra triangles we need - bMoreTops = iExtra > 0, - bMoreBottoms = iExtra < 0; - - var aMin = []; //indexes to start extra triangles (polygon with minimal square) - //init - we need exactly /iExtra/ small triangles - for (var i = Math.abs(iExtra); i > 0; i--) { - aMin.push({ - len: Infinity, - index: -1 - }); - } - - var len; - if (bMoreBottoms) { - for (var i = 0; i < iBotLen; i++) { - len = bottomPoints[i].pos.distanceToSquared(bottomPoints[i + 1].pos); - //find the element to replace - for (var j = aMin.length - 1; j >= 0; j--) { - if (aMin[j].len > len) { - aMin[j].len = len; - aMin.index = j; - break; - } - } //for - } - } else if (bMoreTops) { - for (var i = 0; i < iTopLen; i++) { - len = topPoints[i].pos.distanceToSquared(topPoints[i + 1].pos); - //find the element to replace - for (var j = aMin.length - 1; j >= 0; j--) { - if (aMin[j].len > len) { - aMin[j].len = len; - aMin.index = j; - break; - } - } //for - } - } //if - //sort by index - aMin.sort(fnSortByIndex); - var getTriangle = function addWallsPutTriangle(pointA, pointB, pointC, color) { - return new CSG.Polygon([pointA, pointB, pointC], color); - //return bFlipped ? triangle.flipped() : triangle; - }; - - var bpoint = bottomPoints[0], - tpoint = topPoints[0], - secondPoint, - nBotFacet, nTopFacet; //length of triangle facet side - for (var iB = 0, iT = 0, iMax = iTopLen + iBotLen; iB + iT < iMax;) { - if (aMin.length) { - if (bMoreTops && iT == aMin[0].index) { //one vertex is on the bottom, 2 - on the top - secondPoint = topPoints[++iT]; - //console.log('<<< extra top: ' + secondPoint + ', ' + tpoint + ', bottom: ' + bpoint); - walls.push(getTriangle( - secondPoint, tpoint, bpoint, color - )); - tpoint = secondPoint; - aMin.shift(); - continue; - } else if (bMoreBottoms && iB == aMin[0].index) { - secondPoint = bottomPoints[++iB]; - walls.push(getTriangle( - tpoint, bpoint, secondPoint, color - )); - bpoint = secondPoint; - aMin.shift(); - continue; - } - } - //choose the shortest path - if (iB < iBotLen) { //one vertex is on the top, 2 - on the bottom - nBotFacet = tpoint.pos.distanceToSquared(bottomPoints[iB + 1].pos); - } else { - nBotFacet = Infinity; - } - if (iT < iTopLen) { //one vertex is on the bottom, 2 - on the top - nTopFacet = bpoint.pos.distanceToSquared(topPoints[iT + 1].pos); - } else { - nTopFacet = Infinity; - } - if (nBotFacet <= nTopFacet) { - secondPoint = bottomPoints[++iB]; - walls.push(getTriangle( - tpoint, bpoint, secondPoint, color - )); - bpoint = secondPoint; - } else if (iT < iTopLen) { //nTopFacet < Infinity - secondPoint = topPoints[++iT]; - //console.log('<<< top: ' + secondPoint + ', ' + tpoint + ', bottom: ' + bpoint); - walls.push(getTriangle( - secondPoint, tpoint, bpoint, color - )); - tpoint = secondPoint; - }; - } - return walls; - } - }; - - CSG.Polygon.verticesConvex = function(vertices, planenormal) { - var numvertices = vertices.length; - if (numvertices > 2) { - var prevprevpos = vertices[numvertices - 2].pos; - var prevpos = vertices[numvertices - 1].pos; - for (var i = 0; i < numvertices; i++) { - var pos = vertices[i].pos; - if (!CSG.Polygon.isConvexPoint(prevprevpos, prevpos, pos, planenormal)) { - return false; - } - prevprevpos = prevpos; - prevpos = pos; - } - } - return true; - }; - - // Create a polygon from the given points - CSG.Polygon.createFromPoints = function(points, shared, plane) { - var normal; - if (arguments.length < 3) { - // initially set a dummy vertex normal: - normal = new CSG.Vector3D(0, 0, 0); - } else { - normal = plane.normal; - } - var vertices = []; - points.map(function(p) { - var vec = new CSG.Vector3D(p); - var vertex = new CSG.Vertex(vec); - vertices.push(vertex); - }); - var polygon; - if (arguments.length < 3) { - polygon = new CSG.Polygon(vertices, shared); - } else { - polygon = new CSG.Polygon(vertices, shared, plane); - } - return polygon; - }; - - // calculate whether three points form a convex corner - // prevpoint, point, nextpoint: the 3 coordinates (CSG.Vector3D instances) - // normal: the normal vector of the plane - CSG.Polygon.isConvexPoint = function(prevpoint, point, nextpoint, normal) { - var crossproduct = point.minus(prevpoint).cross(nextpoint.minus(point)); - var crossdotnormal = crossproduct.dot(normal); - return (crossdotnormal >= 0); - }; - - CSG.Polygon.isStrictlyConvexPoint = function(prevpoint, point, nextpoint, normal) { - var crossproduct = point.minus(prevpoint).cross(nextpoint.minus(point)); - var crossdotnormal = crossproduct.dot(normal); - return (crossdotnormal >= 1e-5); - }; - - // # class CSG.Polygon.Shared - // Holds the shared properties for each polygon (currently only color) - // Constructor expects a 4 element array [r,g,b,a], values from 0 to 1, or null - CSG.Polygon.Shared = function(color) { - if(color !== null) - { - if (color.length != 4) { - throw new Error("Expecting 4 element array"); - } - } - this.color = color; - }; - - CSG.Polygon.Shared.fromObject = function(obj) { - return new CSG.Polygon.Shared(obj.color); - }; - - // Create CSG.Polygon.Shared from a color, can be called as follows: - // var s = CSG.Polygon.Shared.fromColor(r,g,b [,a]) - // var s = CSG.Polygon.Shared.fromColor([r,g,b [,a]]) - CSG.Polygon.Shared.fromColor = function(args) { - var color; - if(arguments.length == 1) { - color = arguments[0].slice(); // make deep copy - } - else { - color = []; - for(var i=0; i < arguments.length; i++) { - color.push(arguments[i]); - } - } - if(color.length == 3) { - color.push(1); - } else if(color.length != 4) { - throw new Error("setColor expects either an array with 3 or 4 elements, or 3 or 4 parameters."); - } - return new CSG.Polygon.Shared(color); - }; - - CSG.Polygon.Shared.prototype = { - getTag: function() { - var result = this.tag; - if (!result) { - result = CSG.getTag(); - this.tag = result; - } - return result; - }, - // get a string uniquely identifying this object - getHash: function() { - if (!this.color) return "null"; - return this.color.join("/"); - } - }; - - CSG.Polygon.defaultShared = new CSG.Polygon.Shared(null); - - // # class PolygonTreeNode - // This class manages hierarchical splits of polygons - // At the top is a root node which doesn hold a polygon, only child PolygonTreeNodes - // Below that are zero or more 'top' nodes; each holds a polygon. The polygons can be in different planes - // splitByPlane() splits a node by a plane. If the plane intersects the polygon, two new child nodes - // are created holding the splitted polygon. - // getPolygons() retrieves the polygon from the tree. If for PolygonTreeNode the polygon is split but - // the two split parts (child nodes) are still intact, then the unsplit polygon is returned. - // This ensures that we can safely split a polygon into many fragments. If the fragments are untouched, - // getPolygons() will return the original unsplit polygon instead of the fragments. - // remove() removes a polygon from the tree. Once a polygon is removed, the parent polygons are invalidated - // since they are no longer intact. - // constructor creates the root node: - CSG.PolygonTreeNode = function() { - this.parent = null; - this.children = []; - this.polygon = null; - this.removed = false; - }; - - CSG.PolygonTreeNode.prototype = { - // fill the tree with polygons. Should be called on the root node only; child nodes must - // always be a derivate (split) of the parent node. - addPolygons: function(polygons) { - if (!this.isRootNode()) - // new polygons can only be added to root node; children can only be splitted polygons - throw new Error("Assertion failed"); - var _this = this; - polygons.map(function(polygon) { - _this.addChild(polygon); - }); - }, - - // remove a node - // - the siblings become toplevel nodes - // - the parent is removed recursively - remove: function() { - if (!this.removed) { - this.removed = true; - - if (_CSGDEBUG) { - if (this.isRootNode()) throw new Error("Assertion failed"); // can't remove root node - if (this.children.length) throw new Error("Assertion failed"); // we shouldn't remove nodes with children - } - - // remove ourselves from the parent's children list: - var parentschildren = this.parent.children; - var i = parentschildren.indexOf(this); - if (i < 0) throw new Error("Assertion failed"); - parentschildren.splice(i, 1); - - // invalidate the parent's polygon, and of all parents above it: - this.parent.recursivelyInvalidatePolygon(); - } - }, - - isRemoved: function() { - return this.removed; - }, - - isRootNode: function() { - return !this.parent; - }, - - // invert all polygons in the tree. Call on the root node - invert: function() { - if (!this.isRootNode()) throw new Error("Assertion failed"); // can only call this on the root node - this.invertSub(); - }, - - getPolygon: function() { - if (!this.polygon) throw new Error("Assertion failed"); // doesn't have a polygon, which means that it has been broken down - return this.polygon; - }, - - getPolygons: function(result) { - var children = [this]; - var queue = [children]; - var i, j, l, node; - for (i = 0; i < queue.length; ++i ) { // queue size can change in loop, don't cache length - children = queue[i]; - for (j = 0, l = children.length; j < l; j++) { // ok to cache length - node = children[j]; - if (node.polygon) { - // the polygon hasn't been broken yet. We can ignore the children and return our polygon: - result.push(node.polygon); - } else { - // our polygon has been split up and broken, so gather all subpolygons from the children - queue.push(node.children); - } - } - } - }, - - // split the node by a plane; add the resulting nodes to the frontnodes and backnodes array - // If the plane doesn't intersect the polygon, the 'this' object is added to one of the arrays - // If the plane does intersect the polygon, two new child nodes are created for the front and back fragments, - // and added to both arrays. - splitByPlane: function(plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes) { - if (this.children.length) { - var queue = [this.children], i, j, l, node, nodes; - for (i = 0; i < queue.length; i++) { // queue.length can increase, do not cache - nodes = queue[i]; - for (j = 0, l = nodes.length; j < l; j++) { // ok to cache length - node = nodes[j]; - if (node.children.length) { - queue.push(node.children); - } else { - // no children. Split the polygon: - node._splitByPlane(plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes); - } - } - } - } else { - this._splitByPlane(plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes); - } - }, - - // only to be called for nodes with no children - _splitByPlane: function (plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes) { - var polygon = this.polygon; - if (polygon) { - var bound = polygon.boundingSphere(); - var sphereradius = bound[1] + 1e-4; - var planenormal = plane.normal; - var spherecenter = bound[0]; - var d = planenormal.dot(spherecenter) - plane.w; - if (d > sphereradius) { - frontnodes.push(this); - } else if (d < -sphereradius) { - backnodes.push(this); - } else { - var splitresult = plane.splitPolygon(polygon); - switch (splitresult.type) { - case 0: - // coplanar front: - coplanarfrontnodes.push(this); - break; - - case 1: - // coplanar back: - coplanarbacknodes.push(this); - break; - - case 2: - // front: - frontnodes.push(this); - break; - - case 3: - // back: - backnodes.push(this); - break; - - case 4: - // spanning: - if (splitresult.front) { - var frontnode = this.addChild(splitresult.front); - frontnodes.push(frontnode); - } - if (splitresult.back) { - var backnode = this.addChild(splitresult.back); - backnodes.push(backnode); - } - break; - } - } - } - }, - - - // PRIVATE methods from here: - // add child to a node - // this should be called whenever the polygon is split - // a child should be created for every fragment of the split polygon - // returns the newly created child - addChild: function(polygon) { - var newchild = new CSG.PolygonTreeNode(); - newchild.parent = this; - newchild.polygon = polygon; - this.children.push(newchild); - return newchild; - }, - - invertSub: function() { - var children = [this]; - var queue = [children]; - var i, j, l, node; - for (i = 0; i < queue.length; i++) { - children = queue[i]; - for (j = 0, l = children.length; j < l; j++) { - node = children[j]; - if (node.polygon) { - node.polygon = node.polygon.flipped(); - } - queue.push(node.children); - } - } - }, - - recursivelyInvalidatePolygon: function() { - var node = this; - while (node.polygon) { - node.polygon = null; - if (node.parent) { - node = node.parent; - } - } - } - }; - - - - // # class Tree - // This is the root of a BSP tree - // We are using this separate class for the root of the tree, to hold the PolygonTreeNode root - // The actual tree is kept in this.rootnode - CSG.Tree = function(polygons) { - this.polygonTree = new CSG.PolygonTreeNode(); - this.rootnode = new CSG.Node(null); - if (polygons) this.addPolygons(polygons); - }; - - CSG.Tree.prototype = { - invert: function() { - this.polygonTree.invert(); - this.rootnode.invert(); - }, - - // Remove all polygons in this BSP tree that are inside the other BSP tree - // `tree`. - clipTo: function(tree, alsoRemovecoplanarFront) { - alsoRemovecoplanarFront = alsoRemovecoplanarFront ? true : false; - this.rootnode.clipTo(tree, alsoRemovecoplanarFront); - }, - - allPolygons: function() { - var result = []; - this.polygonTree.getPolygons(result); - return result; - }, - - addPolygons: function(polygons) { - var _this = this; - var polygontreenodes = polygons.map(function(p) { - return _this.polygonTree.addChild(p); - }); - this.rootnode.addPolygonTreeNodes(polygontreenodes); - } - }; - - // # class Node - // Holds a node in a BSP tree. A BSP tree is built from a collection of polygons - // by picking a polygon to split along. - // Polygons are not stored directly in the tree, but in PolygonTreeNodes, stored in - // this.polygontreenodes. Those PolygonTreeNodes are children of the owning - // CSG.Tree.polygonTree - // This is not a leafy BSP tree since there is - // no distinction between internal and leaf nodes. - CSG.Node = function(parent) { - this.plane = null; - this.front = null; - this.back = null; - this.polygontreenodes = []; - this.parent = parent; - }; - - CSG.Node.prototype = { - // Convert solid space to empty space and empty space to solid space. - invert: function() { - var queue = [this]; - var i, node; - for (var i = 0; i < queue.length; i++) { - node = queue[i]; - if(node.plane) node.plane = node.plane.flipped(); - if(node.front) queue.push(node.front); - if(node.back) queue.push(node.back); - var temp = node.front; - node.front = node.back; - node.back = temp; - } - }, - - // clip polygontreenodes to our plane - // calls remove() for all clipped PolygonTreeNodes - clipPolygons: function(polygontreenodes, alsoRemovecoplanarFront) { - var args = {'node': this, 'polygontreenodes': polygontreenodes } - var node; - var stack = []; - - do { - node = args.node; - polygontreenodes = args.polygontreenodes; - - // begin "function" - if(node.plane) { - var backnodes = []; - var frontnodes = []; - var coplanarfrontnodes = alsoRemovecoplanarFront ? backnodes : frontnodes; - var plane = node.plane; - var numpolygontreenodes = polygontreenodes.length; - for(i = 0; i < numpolygontreenodes; i++) { - var node1 = polygontreenodes[i]; - if(!node1.isRemoved()) { - node1.splitByPlane(plane, coplanarfrontnodes, backnodes, frontnodes, backnodes); - } - } - - if(node.front && (frontnodes.length > 0)) { - stack.push({'node': node.front, 'polygontreenodes': frontnodes}); - } - var numbacknodes = backnodes.length; - if (node.back && (numbacknodes > 0)) { - stack.push({'node': node.back, 'polygontreenodes': backnodes}); - } else { - // there's nothing behind this plane. Delete the nodes behind this plane: - for (var i = 0; i < numbacknodes; i++) { - backnodes[i].remove(); - } - } - } - args = stack.pop(); - } while (typeof(args) !== 'undefined'); - }, - - // Remove all polygons in this BSP tree that are inside the other BSP tree - // `tree`. - clipTo: function(tree, alsoRemovecoplanarFront) { - var node = this, stack = []; - do { - if(node.polygontreenodes.length > 0) { - tree.rootnode.clipPolygons(node.polygontreenodes, alsoRemovecoplanarFront); - } - if(node.front) stack.push(node.front); - if(node.back) stack.push(node.back); - node = stack.pop(); - } while(typeof(node) !== 'undefined'); - }, - - addPolygonTreeNodes: function(polygontreenodes) { - var args = {'node': this, 'polygontreenodes': polygontreenodes }; - var node; - var stack = []; - do { - node = args.node; - polygontreenodes = args.polygontreenodes; - - if (polygontreenodes.length === 0) { - args = stack.pop(); - continue; - } - var _this = node; - if (!node.plane) { - var bestplane = polygontreenodes[0].getPolygon().plane; - node.plane = bestplane; - } - var frontnodes = []; - var backnodes = []; - - for (var i = 0, n = polygontreenodes.length ; i < n; ++i) { - polygontreenodes[i].splitByPlane(_this.plane, _this.polygontreenodes, backnodes, frontnodes, backnodes); - } - - if (frontnodes.length > 0) { - if (!node.front) node.front = new CSG.Node(node); - stack.push({'node': node.front, 'polygontreenodes': frontnodes}); - } - if (backnodes.length > 0) { - if (!node.back) node.back = new CSG.Node(node); - stack.push({'node': node.back, 'polygontreenodes': backnodes}); - } - - args = stack.pop(); - } while (typeof(args) !== 'undefined'); - }, - - getParentPlaneNormals: function(normals, maxdepth) { - if (maxdepth > 0) { - if (this.parent) { - normals.push(this.parent.plane.normal); - this.parent.getParentPlaneNormals(normals, maxdepth - 1); - } - } - } - }; - - ////////// - // # class Matrix4x4: - // Represents a 4x4 matrix. Elements are specified in row order - CSG.Matrix4x4 = function(elements) { - if (arguments.length >= 1) { - this.elements = elements; - } else { - // if no arguments passed: create unity matrix - this.elements = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; - } - }; - - CSG.Matrix4x4.prototype = { - plus: function(m) { - var r = []; - for (var i = 0; i < 16; i++) { - r[i] = this.elements[i] + m.elements[i]; - } - return new CSG.Matrix4x4(r); - }, - - minus: function(m) { - var r = []; - for (var i = 0; i < 16; i++) { - r[i] = this.elements[i] - m.elements[i]; - } - return new CSG.Matrix4x4(r); - }, - - // right multiply by another 4x4 matrix: - multiply: function(m) { - // cache elements in local variables, for speedup: - var this0 = this.elements[0]; - var this1 = this.elements[1]; - var this2 = this.elements[2]; - var this3 = this.elements[3]; - var this4 = this.elements[4]; - var this5 = this.elements[5]; - var this6 = this.elements[6]; - var this7 = this.elements[7]; - var this8 = this.elements[8]; - var this9 = this.elements[9]; - var this10 = this.elements[10]; - var this11 = this.elements[11]; - var this12 = this.elements[12]; - var this13 = this.elements[13]; - var this14 = this.elements[14]; - var this15 = this.elements[15]; - var m0 = m.elements[0]; - var m1 = m.elements[1]; - var m2 = m.elements[2]; - var m3 = m.elements[3]; - var m4 = m.elements[4]; - var m5 = m.elements[5]; - var m6 = m.elements[6]; - var m7 = m.elements[7]; - var m8 = m.elements[8]; - var m9 = m.elements[9]; - var m10 = m.elements[10]; - var m11 = m.elements[11]; - var m12 = m.elements[12]; - var m13 = m.elements[13]; - var m14 = m.elements[14]; - var m15 = m.elements[15]; - - var result = []; - result[0] = this0 * m0 + this1 * m4 + this2 * m8 + this3 * m12; - result[1] = this0 * m1 + this1 * m5 + this2 * m9 + this3 * m13; - result[2] = this0 * m2 + this1 * m6 + this2 * m10 + this3 * m14; - result[3] = this0 * m3 + this1 * m7 + this2 * m11 + this3 * m15; - result[4] = this4 * m0 + this5 * m4 + this6 * m8 + this7 * m12; - result[5] = this4 * m1 + this5 * m5 + this6 * m9 + this7 * m13; - result[6] = this4 * m2 + this5 * m6 + this6 * m10 + this7 * m14; - result[7] = this4 * m3 + this5 * m7 + this6 * m11 + this7 * m15; - result[8] = this8 * m0 + this9 * m4 + this10 * m8 + this11 * m12; - result[9] = this8 * m1 + this9 * m5 + this10 * m9 + this11 * m13; - result[10] = this8 * m2 + this9 * m6 + this10 * m10 + this11 * m14; - result[11] = this8 * m3 + this9 * m7 + this10 * m11 + this11 * m15; - result[12] = this12 * m0 + this13 * m4 + this14 * m8 + this15 * m12; - result[13] = this12 * m1 + this13 * m5 + this14 * m9 + this15 * m13; - result[14] = this12 * m2 + this13 * m6 + this14 * m10 + this15 * m14; - result[15] = this12 * m3 + this13 * m7 + this14 * m11 + this15 * m15; - return new CSG.Matrix4x4(result); - }, - - clone: function() { - var elements = this.elements.map(function(p) { - return p; - }); - return new CSG.Matrix4x4(elements); - }, - - // Right multiply the matrix by a CSG.Vector3D (interpreted as 3 row, 1 column) - // (result = M*v) - // Fourth element is taken as 1 - rightMultiply1x3Vector: function(v) { - var v0 = v._x; - var v1 = v._y; - var v2 = v._z; - var v3 = 1; - var x = v0 * this.elements[0] + v1 * this.elements[1] + v2 * this.elements[2] + v3 * this.elements[3]; - var y = v0 * this.elements[4] + v1 * this.elements[5] + v2 * this.elements[6] + v3 * this.elements[7]; - var z = v0 * this.elements[8] + v1 * this.elements[9] + v2 * this.elements[10] + v3 * this.elements[11]; - var w = v0 * this.elements[12] + v1 * this.elements[13] + v2 * this.elements[14] + v3 * this.elements[15]; - // scale such that fourth element becomes 1: - if (w != 1) { - var invw = 1.0 / w; - x *= invw; - y *= invw; - z *= invw; - } - return new CSG.Vector3D(x, y, z); - }, - - // Multiply a CSG.Vector3D (interpreted as 3 column, 1 row) by this matrix - // (result = v*M) - // Fourth element is taken as 1 - leftMultiply1x3Vector: function(v) { - var v0 = v._x; - var v1 = v._y; - var v2 = v._z; - var v3 = 1; - var x = v0 * this.elements[0] + v1 * this.elements[4] + v2 * this.elements[8] + v3 * this.elements[12]; - var y = v0 * this.elements[1] + v1 * this.elements[5] + v2 * this.elements[9] + v3 * this.elements[13]; - var z = v0 * this.elements[2] + v1 * this.elements[6] + v2 * this.elements[10] + v3 * this.elements[14]; - var w = v0 * this.elements[3] + v1 * this.elements[7] + v2 * this.elements[11] + v3 * this.elements[15]; - // scale such that fourth element becomes 1: - if (w != 1) { - var invw = 1.0 / w; - x *= invw; - y *= invw; - z *= invw; - } - return new CSG.Vector3D(x, y, z); - }, - - // Right multiply the matrix by a CSG.Vector2D (interpreted as 2 row, 1 column) - // (result = M*v) - // Fourth element is taken as 1 - rightMultiply1x2Vector: function(v) { - var v0 = v.x; - var v1 = v.y; - var v2 = 0; - var v3 = 1; - var x = v0 * this.elements[0] + v1 * this.elements[1] + v2 * this.elements[2] + v3 * this.elements[3]; - var y = v0 * this.elements[4] + v1 * this.elements[5] + v2 * this.elements[6] + v3 * this.elements[7]; - var z = v0 * this.elements[8] + v1 * this.elements[9] + v2 * this.elements[10] + v3 * this.elements[11]; - var w = v0 * this.elements[12] + v1 * this.elements[13] + v2 * this.elements[14] + v3 * this.elements[15]; - // scale such that fourth element becomes 1: - if (w != 1) { - var invw = 1.0 / w; - x *= invw; - y *= invw; - z *= invw; - } - return new CSG.Vector2D(x, y); - }, - - // Multiply a CSG.Vector2D (interpreted as 2 column, 1 row) by this matrix - // (result = v*M) - // Fourth element is taken as 1 - leftMultiply1x2Vector: function(v) { - var v0 = v.x; - var v1 = v.y; - var v2 = 0; - var v3 = 1; - var x = v0 * this.elements[0] + v1 * this.elements[4] + v2 * this.elements[8] + v3 * this.elements[12]; - var y = v0 * this.elements[1] + v1 * this.elements[5] + v2 * this.elements[9] + v3 * this.elements[13]; - var z = v0 * this.elements[2] + v1 * this.elements[6] + v2 * this.elements[10] + v3 * this.elements[14]; - var w = v0 * this.elements[3] + v1 * this.elements[7] + v2 * this.elements[11] + v3 * this.elements[15]; - // scale such that fourth element becomes 1: - if (w != 1) { - var invw = 1.0 / w; - x *= invw; - y *= invw; - z *= invw; - } - return new CSG.Vector2D(x, y); - }, - - // determine whether this matrix is a mirroring transformation - isMirroring: function() { - var u = new CSG.Vector3D(this.elements[0], this.elements[4], this.elements[8]); - var v = new CSG.Vector3D(this.elements[1], this.elements[5], this.elements[9]); - var w = new CSG.Vector3D(this.elements[2], this.elements[6], this.elements[10]); - - // for a true orthogonal, non-mirrored base, u.cross(v) == w - // If they have an opposite direction then we are mirroring - var mirrorvalue = u.cross(v).dot(w); - var ismirror = (mirrorvalue < 0); - return ismirror; - } - }; - - // return the unity matrix - CSG.Matrix4x4.unity = function() { - return new CSG.Matrix4x4(); - }; - - // Create a rotation matrix for rotating around the x axis - CSG.Matrix4x4.rotationX = function(degrees) { - var radians = degrees * Math.PI * (1.0 / 180.0); - var cos = Math.cos(radians); - var sin = Math.sin(radians); - var els = [ - 1, 0, 0, 0, 0, cos, sin, 0, 0, -sin, cos, 0, 0, 0, 0, 1 - ]; - return new CSG.Matrix4x4(els); - }; - - // Create a rotation matrix for rotating around the y axis - CSG.Matrix4x4.rotationY = function(degrees) { - var radians = degrees * Math.PI * (1.0 / 180.0); - var cos = Math.cos(radians); - var sin = Math.sin(radians); - var els = [ - cos, 0, -sin, 0, 0, 1, 0, 0, sin, 0, cos, 0, 0, 0, 0, 1 - ]; - return new CSG.Matrix4x4(els); - }; - - // Create a rotation matrix for rotating around the z axis - CSG.Matrix4x4.rotationZ = function(degrees) { - var radians = degrees * Math.PI * (1.0 / 180.0); - var cos = Math.cos(radians); - var sin = Math.sin(radians); - var els = [ - cos, sin, 0, 0, -sin, cos, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 - ]; - return new CSG.Matrix4x4(els); - }; - - // Matrix for rotation about arbitrary point and axis - CSG.Matrix4x4.rotation = function(rotationCenter, rotationAxis, degrees) { - rotationCenter = new CSG.Vector3D(rotationCenter); - rotationAxis = new CSG.Vector3D(rotationAxis); - var rotationPlane = CSG.Plane.fromNormalAndPoint(rotationAxis, rotationCenter); - var orthobasis = new CSG.OrthoNormalBasis(rotationPlane); - var transformation = CSG.Matrix4x4.translation(rotationCenter.negated()); - transformation = transformation.multiply(orthobasis.getProjectionMatrix()); - transformation = transformation.multiply(CSG.Matrix4x4.rotationZ(degrees)); - transformation = transformation.multiply(orthobasis.getInverseProjectionMatrix()); - transformation = transformation.multiply(CSG.Matrix4x4.translation(rotationCenter)); - return transformation; - }; - - // Create an affine matrix for translation: - CSG.Matrix4x4.translation = function(v) { - // parse as CSG.Vector3D, so we can pass an array or a CSG.Vector3D - var vec = new CSG.Vector3D(v); - var els = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, vec.x, vec.y, vec.z, 1]; - return new CSG.Matrix4x4(els); - }; - - // Create an affine matrix for mirroring into an arbitrary plane: - CSG.Matrix4x4.mirroring = function(plane) { - var nx = plane.normal.x; - var ny = plane.normal.y; - var nz = plane.normal.z; - var w = plane.w; - var els = [ - (1.0 - 2.0 * nx * nx), (-2.0 * ny * nx), (-2.0 * nz * nx), 0, - (-2.0 * nx * ny), (1.0 - 2.0 * ny * ny), (-2.0 * nz * ny), 0, - (-2.0 * nx * nz), (-2.0 * ny * nz), (1.0 - 2.0 * nz * nz), 0, - (2.0 * nx * w), (2.0 * ny * w), (2.0 * nz * w), 1 - ]; - return new CSG.Matrix4x4(els); - }; - - // Create an affine matrix for scaling: - CSG.Matrix4x4.scaling = function(v) { - // parse as CSG.Vector3D, so we can pass an array or a CSG.Vector3D - var vec = new CSG.Vector3D(v); - var els = [ - vec.x, 0, 0, 0, 0, vec.y, 0, 0, 0, 0, vec.z, 0, 0, 0, 0, 1 - ]; - return new CSG.Matrix4x4(els); - }; - - /////////////////////////////////////////////////// - // # class Vector2D: - // Represents a 2 element vector - CSG.Vector2D = function(x, y) { - if (arguments.length == 2) { - this._x = parseFloat(x); - this._y = parseFloat(y); - } else { - var ok = true; - if (arguments.length == 1) { - if (typeof(x) == "object") { - if (x instanceof CSG.Vector2D) { - this._x = x._x; - this._y = x._y; - } else if (x instanceof Array) { - this._x = parseFloat(x[0]); - this._y = parseFloat(x[1]); - } else if (('x' in x) && ('y' in x)) { - this._x = parseFloat(x.x); - this._y = parseFloat(x.y); - } else ok = false; - } else { - var v = parseFloat(x); - this._x = v; - this._y = v; - } - } else ok = false; - if (ok) { - if ((!CSG.IsFloat(this._x)) || (!CSG.IsFloat(this._y))) ok = false; - } - if (!ok) { - throw new Error("wrong arguments"); - } - } - }; - - CSG.Vector2D.fromAngle = function(radians) { - return CSG.Vector2D.fromAngleRadians(radians); - }; - - CSG.Vector2D.fromAngleDegrees = function(degrees) { - var radians = Math.PI * degrees / 180; - return CSG.Vector2D.fromAngleRadians(radians); - }; - - CSG.Vector2D.fromAngleRadians = function(radians) { - return CSG.Vector2D.Create(Math.cos(radians), Math.sin(radians)); - }; - - // This does the same as new CSG.Vector2D(x,y) but it doesn't go through the constructor - // and the parameters are not validated. Is much faster. - CSG.Vector2D.Create = function(x, y) { - var result = Object.create(CSG.Vector2D.prototype); - result._x = x; - result._y = y; - return result; - }; - - CSG.Vector2D.prototype = { - get x() { - return this._x; - }, - get y() { - return this._y; - }, - - set x(v) { - throw new Error("Vector2D is immutable"); - }, - set y(v) { - throw new Error("Vector2D is immutable"); - }, - - // extend to a 3D vector by adding a z coordinate: - toVector3D: function(z) { - return new CSG.Vector3D(this._x, this._y, z); - }, - - equals: function(a) { - return (this._x == a._x) && (this._y == a._y); - }, - - clone: function() { - return CSG.Vector2D.Create(this._x, this._y); - }, - - negated: function() { - return CSG.Vector2D.Create(-this._x, -this._y); - }, - - plus: function(a) { - return CSG.Vector2D.Create(this._x + a._x, this._y + a._y); - }, - - minus: function(a) { - return CSG.Vector2D.Create(this._x - a._x, this._y - a._y); - }, - - times: function(a) { - return CSG.Vector2D.Create(this._x * a, this._y * a); - }, - - dividedBy: function(a) { - return CSG.Vector2D.Create(this._x / a, this._y / a); - }, - - dot: function(a) { - return this._x * a._x + this._y * a._y; - }, - - lerp: function(a, t) { - return this.plus(a.minus(this).times(t)); - }, - - length: function() { - return Math.sqrt(this.dot(this)); - }, - - distanceTo: function(a) { - return this.minus(a).length(); - }, - - distanceToSquared: function(a) { - return this.minus(a).lengthSquared(); - }, - - lengthSquared: function() { - return this.dot(this); - }, - - unit: function() { - return this.dividedBy(this.length()); - }, - - cross: function(a) { - return this._x * a._y - this._y * a._x; - }, - - // returns the vector rotated by 90 degrees clockwise - normal: function() { - return CSG.Vector2D.Create(this._y, -this._x); - }, - - // Right multiply by a 4x4 matrix (the vector is interpreted as a row vector) - // Returns a new CSG.Vector2D - multiply4x4: function(matrix4x4) { - return matrix4x4.leftMultiply1x2Vector(this); - }, - - transform: function(matrix4x4) { - return matrix4x4.leftMultiply1x2Vector(this); - }, - - angle: function() { - return this.angleRadians(); - }, - - angleDegrees: function() { - var radians = this.angleRadians(); - return 180 * radians / Math.PI; - }, - - angleRadians: function() { - // y=sin, x=cos - return Math.atan2(this._y, this._x); - }, - - min: function(p) { - return CSG.Vector2D.Create( - Math.min(this._x, p._x), Math.min(this._y, p._y)); - }, - - max: function(p) { - return CSG.Vector2D.Create( - Math.max(this._x, p._x), Math.max(this._y, p._y)); - }, - - toString: function() { - return "(" + this._x.toFixed(2) + ", " + this._y.toFixed(2) + ")"; - }, - - abs: function() { - return CSG.Vector2D.Create(Math.abs(this._x), Math.abs(this._y)); - }, - }; - - - // # class Line2D - // Represents a directional line in 2D space - // A line is parametrized by its normal vector (perpendicular to the line, rotated 90 degrees counter clockwise) - // and w. The line passes through the point .times(w). - // normal must be a unit vector! - // Equation: p is on line if normal.dot(p)==w - CSG.Line2D = function(normal, w) { - normal = new CSG.Vector2D(normal); - w = parseFloat(w); - var l = normal.length(); - // normalize: - w *= l; - normal = normal.times(1.0 / l); - this.normal = normal; - this.w = w; - }; - - CSG.Line2D.fromPoints = function(p1, p2) { - p1 = new CSG.Vector2D(p1); - p2 = new CSG.Vector2D(p2); - var direction = p2.minus(p1); - var normal = direction.normal().negated().unit(); - var w = p1.dot(normal); - return new CSG.Line2D(normal, w); - }; - - CSG.Line2D.prototype = { - // same line but opposite direction: - reverse: function() { - return new CSG.Line2D(this.normal.negated(), -this.w); - }, - - equals: function(l) { - return (l.normal.equals(this.normal) && (l.w == this.w)); - }, - - origin: function() { - return this.normal.times(this.w); - }, - - direction: function() { - return this.normal.normal(); - }, - - xAtY: function(y) { - // (py == y) && (normal * p == w) - // -> px = (w - normal._y * y) / normal.x - var x = (this.w - this.normal._y * y) / this.normal.x; - return x; - }, - - absDistanceToPoint: function(point) { - point = new CSG.Vector2D(point); - var point_projected = point.dot(this.normal); - var distance = Math.abs(point_projected - this.w); - return distance; - }, - /*FIXME: has error - origin is not defined, the method is never used - closestPoint: function(point) { - point = new CSG.Vector2D(point); - var vector = point.dot(this.direction()); - return origin.plus(vector); - }, - */ - - // intersection between two lines, returns point as Vector2D - intersectWithLine: function(line2d) { - var point = CSG.solve2Linear(this.normal.x, this.normal.y, line2d.normal.x, line2d.normal.y, this.w, line2d.w); - point = new CSG.Vector2D(point); // make vector2d - return point; - }, - - transform: function(matrix4x4) { - var origin = new CSG.Vector2D(0, 0); - var pointOnPlane = this.normal.times(this.w); - var neworigin = origin.multiply4x4(matrix4x4); - var neworiginPlusNormal = this.normal.multiply4x4(matrix4x4); - var newnormal = neworiginPlusNormal.minus(neworigin); - var newpointOnPlane = pointOnPlane.multiply4x4(matrix4x4); - var neww = newnormal.dot(newpointOnPlane); - return new CSG.Line2D(newnormal, neww); - } - }; - - // # class Line3D - // Represents a line in 3D space - // direction must be a unit vector - // point is a random point on the line - CSG.Line3D = function(point, direction) { - point = new CSG.Vector3D(point); - direction = new CSG.Vector3D(direction); - this.point = point; - this.direction = direction.unit(); - }; - - CSG.Line3D.fromPoints = function(p1, p2) { - p1 = new CSG.Vector3D(p1); - p2 = new CSG.Vector3D(p2); - var direction = p2.minus(p1); - return new CSG.Line3D(p1, direction); - }; - - CSG.Line3D.fromPlanes = function(p1, p2) { - var direction = p1.normal.cross(p2.normal); - var l = direction.length(); - if (l < 1e-10) { - throw new Error("Parallel planes"); - } - direction = direction.times(1.0 / l); - - var mabsx = Math.abs(direction.x); - var mabsy = Math.abs(direction.y); - var mabsz = Math.abs(direction.z); - var origin; - if ((mabsx >= mabsy) && (mabsx >= mabsz)) { - // direction vector is mostly pointing towards x - // find a point p for which x is zero: - var r = CSG.solve2Linear(p1.normal.y, p1.normal.z, p2.normal.y, p2.normal.z, p1.w, p2.w); - origin = new CSG.Vector3D(0, r[0], r[1]); - } else if ((mabsy >= mabsx) && (mabsy >= mabsz)) { - // find a point p for which y is zero: - var r = CSG.solve2Linear(p1.normal.x, p1.normal.z, p2.normal.x, p2.normal.z, p1.w, p2.w); - origin = new CSG.Vector3D(r[0], 0, r[1]); - } else { - // find a point p for which z is zero: - var r = CSG.solve2Linear(p1.normal.x, p1.normal.y, p2.normal.x, p2.normal.y, p1.w, p2.w); - origin = new CSG.Vector3D(r[0], r[1], 0); - } - return new CSG.Line3D(origin, direction); - }; - - - CSG.Line3D.prototype = { - intersectWithPlane: function(plane) { - // plane: plane.normal * p = plane.w - // line: p=line.point + labda * line.direction - var labda = (plane.w - plane.normal.dot(this.point)) / plane.normal.dot(this.direction); - var point = this.point.plus(this.direction.times(labda)); - return point; - }, - - clone: function(line) { - return new CSG.Line3D(this.point.clone(), this.direction.clone()); - }, - - reverse: function() { - return new CSG.Line3D(this.point.clone(), this.direction.negated()); - }, - - transform: function(matrix4x4) { - var newpoint = this.point.multiply4x4(matrix4x4); - var pointPlusDirection = this.point.plus(this.direction); - var newPointPlusDirection = pointPlusDirection.multiply4x4(matrix4x4); - var newdirection = newPointPlusDirection.minus(newpoint); - return new CSG.Line3D(newpoint, newdirection); - }, - - closestPointOnLine: function(point) { - point = new CSG.Vector3D(point); - var t = point.minus(this.point).dot(this.direction) / this.direction.dot(this.direction); - var closestpoint = this.point.plus(this.direction.times(t)); - return closestpoint; - }, - - distanceToPoint: function(point) { - point = new CSG.Vector3D(point); - var closestpoint = this.closestPointOnLine(point); - var distancevector = point.minus(closestpoint); - var distance = distancevector.length(); - return distance; - }, - - equals: function(line3d) { - if (!this.direction.equals(line3d.direction)) return false; - var distance = this.distanceToPoint(line3d.point); - if (distance > 1e-8) return false; - return true; - } - }; - - - // # class OrthoNormalBasis - // Reprojects points on a 3D plane onto a 2D plane - // or from a 2D plane back onto the 3D plane - CSG.OrthoNormalBasis = function(plane, rightvector) { - if (arguments.length < 2) { - // choose an arbitrary right hand vector, making sure it is somewhat orthogonal to the plane normal: - rightvector = plane.normal.randomNonParallelVector(); - } else { - rightvector = new CSG.Vector3D(rightvector); - } - this.v = plane.normal.cross(rightvector).unit(); - this.u = this.v.cross(plane.normal); - this.plane = plane; - this.planeorigin = plane.normal.times(plane.w); - }; - - // Get an orthonormal basis for the standard XYZ planes. - // Parameters: the names of two 3D axes. The 2d x axis will map to the first given 3D axis, the 2d y - // axis will map to the second. - // Prepend the axis with a "-" to invert the direction of this axis. - // For example: CSG.OrthoNormalBasis.GetCartesian("-Y","Z") - // will return an orthonormal basis where the 2d X axis maps to the 3D inverted Y axis, and - // the 2d Y axis maps to the 3D Z axis. - CSG.OrthoNormalBasis.GetCartesian = function(xaxisid, yaxisid) { - var axisid = xaxisid + "/" + yaxisid; - var planenormal, rightvector; - if (axisid == "X/Y") { - planenormal = [0, 0, 1]; - rightvector = [1, 0, 0]; - } else if (axisid == "Y/-X") { - planenormal = [0, 0, 1]; - rightvector = [0, 1, 0]; - } else if (axisid == "-X/-Y") { - planenormal = [0, 0, 1]; - rightvector = [-1, 0, 0]; - } else if (axisid == "-Y/X") { - planenormal = [0, 0, 1]; - rightvector = [0, -1, 0]; - } else if (axisid == "-X/Y") { - planenormal = [0, 0, -1]; - rightvector = [-1, 0, 0]; - } else if (axisid == "-Y/-X") { - planenormal = [0, 0, -1]; - rightvector = [0, -1, 0]; - } else if (axisid == "X/-Y") { - planenormal = [0, 0, -1]; - rightvector = [1, 0, 0]; - } else if (axisid == "Y/X") { - planenormal = [0, 0, -1]; - rightvector = [0, 1, 0]; - } else if (axisid == "X/Z") { - planenormal = [0, -1, 0]; - rightvector = [1, 0, 0]; - } else if (axisid == "Z/-X") { - planenormal = [0, -1, 0]; - rightvector = [0, 0, 1]; - } else if (axisid == "-X/-Z") { - planenormal = [0, -1, 0]; - rightvector = [-1, 0, 0]; - } else if (axisid == "-Z/X") { - planenormal = [0, -1, 0]; - rightvector = [0, 0, -1]; - } else if (axisid == "-X/Z") { - planenormal = [0, 1, 0]; - rightvector = [-1, 0, 0]; - } else if (axisid == "-Z/-X") { - planenormal = [0, 1, 0]; - rightvector = [0, 0, -1]; - } else if (axisid == "X/-Z") { - planenormal = [0, 1, 0]; - rightvector = [1, 0, 0]; - } else if (axisid == "Z/X") { - planenormal = [0, 1, 0]; - rightvector = [0, 0, 1]; - } else if (axisid == "Y/Z") { - planenormal = [1, 0, 0]; - rightvector = [0, 1, 0]; - } else if (axisid == "Z/-Y") { - planenormal = [1, 0, 0]; - rightvector = [0, 0, 1]; - } else if (axisid == "-Y/-Z") { - planenormal = [1, 0, 0]; - rightvector = [0, -1, 0]; - } else if (axisid == "-Z/Y") { - planenormal = [1, 0, 0]; - rightvector = [0, 0, -1]; - } else if (axisid == "-Y/Z") { - planenormal = [-1, 0, 0]; - rightvector = [0, -1, 0]; - } else if (axisid == "-Z/-Y") { - planenormal = [-1, 0, 0]; - rightvector = [0, 0, -1]; - } else if (axisid == "Y/-Z") { - planenormal = [-1, 0, 0]; - rightvector = [0, 1, 0]; - } else if (axisid == "Z/Y") { - planenormal = [-1, 0, 0]; - rightvector = [0, 0, 1]; - } else { - throw new Error("CSG.OrthoNormalBasis.GetCartesian: invalid combination of axis identifiers. Should pass two string arguments from [X,Y,Z,-X,-Y,-Z], being two different axes."); - } - return new CSG.OrthoNormalBasis(new CSG.Plane(new CSG.Vector3D(planenormal), 0), new CSG.Vector3D(rightvector)); - }; - - /* - // test code for CSG.OrthoNormalBasis.GetCartesian() - CSG.OrthoNormalBasis.GetCartesian_Test=function() { - var axisnames=["X","Y","Z","-X","-Y","-Z"]; - var axisvectors=[[1,0,0], [0,1,0], [0,0,1], [-1,0,0], [0,-1,0], [0,0,-1]]; - for(var axis1=0; axis1 < 3; axis1++) { - for(var axis1inverted=0; axis1inverted < 2; axis1inverted++) { - var axis1name=axisnames[axis1+3*axis1inverted]; - var axis1vector=axisvectors[axis1+3*axis1inverted]; - for(var axis2=0; axis2 < 3; axis2++) { - if(axis2 != axis1) { - for(var axis2inverted=0; axis2inverted < 2; axis2inverted++) { - var axis2name=axisnames[axis2+3*axis2inverted]; - var axis2vector=axisvectors[axis2+3*axis2inverted]; - var orthobasis=CSG.OrthoNormalBasis.GetCartesian(axis1name, axis2name); - var test1=orthobasis.to3D(new CSG.Vector2D([1,0])); - var test2=orthobasis.to3D(new CSG.Vector2D([0,1])); - var expected1=new CSG.Vector3D(axis1vector); - var expected2=new CSG.Vector3D(axis2vector); - var d1=test1.distanceTo(expected1); - var d2=test2.distanceTo(expected2); - if( (d1 > 0.01) || (d2 > 0.01) ) { - throw new Error("Wrong!"); - }}}}}} - throw new Error("OK"); - }; - */ - - // The z=0 plane, with the 3D x and y vectors mapped to the 2D x and y vector - CSG.OrthoNormalBasis.Z0Plane = function() { - var plane = new CSG.Plane(new CSG.Vector3D([0, 0, 1]), 0); - return new CSG.OrthoNormalBasis(plane, new CSG.Vector3D([1, 0, 0])); - }; - - CSG.OrthoNormalBasis.prototype = { - getProjectionMatrix: function() { - return new CSG.Matrix4x4([ - this.u.x, this.v.x, this.plane.normal.x, 0, - this.u.y, this.v.y, this.plane.normal.y, 0, - this.u.z, this.v.z, this.plane.normal.z, 0, - 0, 0, -this.plane.w, 1 - ]); - }, - - getInverseProjectionMatrix: function() { - var p = this.plane.normal.times(this.plane.w); - return new CSG.Matrix4x4([ - this.u.x, this.u.y, this.u.z, 0, - this.v.x, this.v.y, this.v.z, 0, - this.plane.normal.x, this.plane.normal.y, this.plane.normal.z, 0, - p.x, p.y, p.z, 1 - ]); - }, - - to2D: function(vec3) { - return new CSG.Vector2D(vec3.dot(this.u), vec3.dot(this.v)); - }, - - to3D: function(vec2) { - return this.planeorigin.plus(this.u.times(vec2.x)).plus(this.v.times(vec2.y)); - }, - - line3Dto2D: function(line3d) { - var a = line3d.point; - var b = line3d.direction.plus(a); - var a2d = this.to2D(a); - var b2d = this.to2D(b); - return CSG.Line2D.fromPoints(a2d, b2d); - }, - - line2Dto3D: function(line2d) { - var a = line2d.origin(); - var b = line2d.direction().plus(a); - var a3d = this.to3D(a); - var b3d = this.to3D(b); - return CSG.Line3D.fromPoints(a3d, b3d); - }, - - transform: function(matrix4x4) { - // todo: this may not work properly in case of mirroring - var newplane = this.plane.transform(matrix4x4); - var rightpoint_transformed = this.u.transform(matrix4x4); - var origin_transformed = new CSG.Vector3D(0, 0, 0).transform(matrix4x4); - var newrighthandvector = rightpoint_transformed.minus(origin_transformed); - var newbasis = new CSG.OrthoNormalBasis(newplane, newrighthandvector); - return newbasis; - } - }; - - function insertSorted(array, element, comparefunc) { - var leftbound = 0; - var rightbound = array.length; - while (rightbound > leftbound) { - var testindex = Math.floor((leftbound + rightbound) / 2); - var testelement = array[testindex]; - var compareresult = comparefunc(element, testelement); - if (compareresult > 0) // element > testelement - { - leftbound = testindex + 1; - } else { - rightbound = testindex; - } - } - array.splice(leftbound, 0, element); - } - - // Get the x coordinate of a point with a certain y coordinate, interpolated between two - // points (CSG.Vector2D). - // Interpolation is robust even if the points have the same y coordinate - CSG.interpolateBetween2DPointsForY = function(point1, point2, y) { - var f1 = y - point1.y; - var f2 = point2.y - point1.y; - if (f2 < 0) { - f1 = -f1; - f2 = -f2; - } - var t; - if (f1 <= 0) { - t = 0.0; - } else if (f1 >= f2) { - t = 1.0; - } else if (f2 < 1e-10) { - t = 0.5; - } else { - t = f1 / f2; - } - var result = point1.x + t * (point2.x - point1.x); - return result; - }; - - // Retesselation function for a set of coplanar polygons. See the introduction at the top of - // this file. - CSG.reTesselateCoplanarPolygons = function(sourcepolygons, destpolygons) { - var EPS = 1e-5; - - var numpolygons = sourcepolygons.length; - if (numpolygons > 0) { - var plane = sourcepolygons[0].plane; - var shared = sourcepolygons[0].shared; - var orthobasis = new CSG.OrthoNormalBasis(plane); - var polygonvertices2d = []; // array of array of CSG.Vector2D - var polygontopvertexindexes = []; // array of indexes of topmost vertex per polygon - var topy2polygonindexes = {}; - var ycoordinatetopolygonindexes = {}; - - var xcoordinatebins = {}; - var ycoordinatebins = {}; - - // convert all polygon vertices to 2D - // Make a list of all encountered y coordinates - // And build a map of all polygons that have a vertex at a certain y coordinate: - var ycoordinateBinningFactor = 1.0 / EPS * 10; - for (var polygonindex = 0; polygonindex < numpolygons; polygonindex++) { - var poly3d = sourcepolygons[polygonindex]; - var vertices2d = []; - var numvertices = poly3d.vertices.length; - var minindex = -1; - if (numvertices > 0) { - var miny, maxy, maxindex; - for (var i = 0; i < numvertices; i++) { - var pos2d = orthobasis.to2D(poly3d.vertices[i].pos); - // perform binning of y coordinates: If we have multiple vertices very - // close to each other, give them the same y coordinate: - var ycoordinatebin = Math.floor(pos2d.y * ycoordinateBinningFactor); - var newy; - if (ycoordinatebin in ycoordinatebins) { - newy = ycoordinatebins[ycoordinatebin]; - } else if (ycoordinatebin + 1 in ycoordinatebins) { - newy = ycoordinatebins[ycoordinatebin + 1]; - } else if (ycoordinatebin - 1 in ycoordinatebins) { - newy = ycoordinatebins[ycoordinatebin - 1]; - } else { - newy = pos2d.y; - ycoordinatebins[ycoordinatebin] = pos2d.y; - } - pos2d = CSG.Vector2D.Create(pos2d.x, newy); - vertices2d.push(pos2d); - var y = pos2d.y; - if ((i === 0) || (y < miny)) { - miny = y; - minindex = i; - } - if ((i === 0) || (y > maxy)) { - maxy = y; - maxindex = i; - } - if (!(y in ycoordinatetopolygonindexes)) { - ycoordinatetopolygonindexes[y] = {}; - } - ycoordinatetopolygonindexes[y][polygonindex] = true; - } - if (miny >= maxy) { - // degenerate polygon, all vertices have same y coordinate. Just ignore it from now: - vertices2d = []; - numvertices = 0; - minindex = -1; - } else { - if (!(miny in topy2polygonindexes)) { - topy2polygonindexes[miny] = []; - } - topy2polygonindexes[miny].push(polygonindex); - } - } // if(numvertices > 0) - // reverse the vertex order: - vertices2d.reverse(); - minindex = numvertices - minindex - 1; - polygonvertices2d.push(vertices2d); - polygontopvertexindexes.push(minindex); - } - var ycoordinates = []; - for (var ycoordinate in ycoordinatetopolygonindexes) ycoordinates.push(ycoordinate); - ycoordinates.sort(fnNumberSort); - - // Now we will iterate over all y coordinates, from lowest to highest y coordinate - // activepolygons: source polygons that are 'active', i.e. intersect with our y coordinate - // Is sorted so the polygons are in left to right order - // Each element in activepolygons has these properties: - // polygonindex: the index of the source polygon (i.e. an index into the sourcepolygons - // and polygonvertices2d arrays) - // leftvertexindex: the index of the vertex at the left side of the polygon (lowest x) - // that is at or just above the current y coordinate - // rightvertexindex: dito at right hand side of polygon - // topleft, bottomleft: coordinates of the left side of the polygon crossing the current y coordinate - // topright, bottomright: coordinates of the right hand side of the polygon crossing the current y coordinate - var activepolygons = []; - var prevoutpolygonrow = []; - for (var yindex = 0; yindex < ycoordinates.length; yindex++) { - var newoutpolygonrow = []; - var ycoordinate_as_string = ycoordinates[yindex]; - var ycoordinate = Number(ycoordinate_as_string); - - // update activepolygons for this y coordinate: - // - Remove any polygons that end at this y coordinate - // - update leftvertexindex and rightvertexindex (which point to the current vertex index - // at the the left and right side of the polygon - // Iterate over all polygons that have a corner at this y coordinate: - var polygonindexeswithcorner = ycoordinatetopolygonindexes[ycoordinate_as_string]; - for (var activepolygonindex = 0; activepolygonindex < activepolygons.length; ++activepolygonindex) { - var activepolygon = activepolygons[activepolygonindex]; - var polygonindex = activepolygon.polygonindex; - if (polygonindexeswithcorner[polygonindex]) { - // this active polygon has a corner at this y coordinate: - var vertices2d = polygonvertices2d[polygonindex]; - var numvertices = vertices2d.length; - var newleftvertexindex = activepolygon.leftvertexindex; - var newrightvertexindex = activepolygon.rightvertexindex; - // See if we need to increase leftvertexindex or decrease rightvertexindex: - while (true) { - var nextleftvertexindex = newleftvertexindex + 1; - if (nextleftvertexindex >= numvertices) nextleftvertexindex = 0; - if (vertices2d[nextleftvertexindex].y != ycoordinate) break; - newleftvertexindex = nextleftvertexindex; - } - var nextrightvertexindex = newrightvertexindex - 1; - if (nextrightvertexindex < 0) nextrightvertexindex = numvertices - 1; - if (vertices2d[nextrightvertexindex].y == ycoordinate) { - newrightvertexindex = nextrightvertexindex; - } - if ((newleftvertexindex != activepolygon.leftvertexindex) && (newleftvertexindex == newrightvertexindex)) { - // We have increased leftvertexindex or decreased rightvertexindex, and now they point to the same vertex - // This means that this is the bottom point of the polygon. We'll remove it: - activepolygons.splice(activepolygonindex, 1); - --activepolygonindex; - } else { - activepolygon.leftvertexindex = newleftvertexindex; - activepolygon.rightvertexindex = newrightvertexindex; - activepolygon.topleft = vertices2d[newleftvertexindex]; - activepolygon.topright = vertices2d[newrightvertexindex]; - var nextleftvertexindex = newleftvertexindex + 1; - if (nextleftvertexindex >= numvertices) nextleftvertexindex = 0; - activepolygon.bottomleft = vertices2d[nextleftvertexindex]; - var nextrightvertexindex = newrightvertexindex - 1; - if (nextrightvertexindex < 0) nextrightvertexindex = numvertices - 1; - activepolygon.bottomright = vertices2d[nextrightvertexindex]; - } - } // if polygon has corner here - } // for activepolygonindex - var nextycoordinate; - if (yindex >= ycoordinates.length - 1) { - // last row, all polygons must be finished here: - activepolygons = []; - nextycoordinate = null; - } else // yindex < ycoordinates.length-1 - { - nextycoordinate = Number(ycoordinates[yindex + 1]); - var middleycoordinate = 0.5 * (ycoordinate + nextycoordinate); - // update activepolygons by adding any polygons that start here: - var startingpolygonindexes = topy2polygonindexes[ycoordinate_as_string]; - for (var polygonindex_key in startingpolygonindexes) { - var polygonindex = startingpolygonindexes[polygonindex_key]; - var vertices2d = polygonvertices2d[polygonindex]; - var numvertices = vertices2d.length; - var topvertexindex = polygontopvertexindexes[polygonindex]; - // the top of the polygon may be a horizontal line. In that case topvertexindex can point to any point on this line. - // Find the left and right topmost vertices which have the current y coordinate: - var topleftvertexindex = topvertexindex; - while (true) { - var i = topleftvertexindex + 1; - if (i >= numvertices) i = 0; - if (vertices2d[i].y != ycoordinate) break; - if (i == topvertexindex) break; // should not happen, but just to prevent endless loops - topleftvertexindex = i; - } - var toprightvertexindex = topvertexindex; - while (true) { - var i = toprightvertexindex - 1; - if (i < 0) i = numvertices - 1; - if (vertices2d[i].y != ycoordinate) break; - if (i == topleftvertexindex) break; // should not happen, but just to prevent endless loops - toprightvertexindex = i; - } - var nextleftvertexindex = topleftvertexindex + 1; - if (nextleftvertexindex >= numvertices) nextleftvertexindex = 0; - var nextrightvertexindex = toprightvertexindex - 1; - if (nextrightvertexindex < 0) nextrightvertexindex = numvertices - 1; - var newactivepolygon = { - polygonindex: polygonindex, - leftvertexindex: topleftvertexindex, - rightvertexindex: toprightvertexindex, - topleft: vertices2d[topleftvertexindex], - topright: vertices2d[toprightvertexindex], - bottomleft: vertices2d[nextleftvertexindex], - bottomright: vertices2d[nextrightvertexindex], - }; - insertSorted(activepolygons, newactivepolygon, function(el1, el2) { - var x1 = CSG.interpolateBetween2DPointsForY( - el1.topleft, el1.bottomleft, middleycoordinate); - var x2 = CSG.interpolateBetween2DPointsForY( - el2.topleft, el2.bottomleft, middleycoordinate); - if (x1 > x2) return 1; - if (x1 < x2) return -1; - return 0; - }); - } // for(var polygonindex in startingpolygonindexes) - } // yindex < ycoordinates.length-1 - //if( (yindex == ycoordinates.length-1) || (nextycoordinate - ycoordinate > EPS) ) - if (true) { - // Now activepolygons is up to date - // Build the output polygons for the next row in newoutpolygonrow: - for (var activepolygon_key in activepolygons) { - var activepolygon = activepolygons[activepolygon_key]; - var polygonindex = activepolygon.polygonindex; - var vertices2d = polygonvertices2d[polygonindex]; - var numvertices = vertices2d.length; - - var x = CSG.interpolateBetween2DPointsForY(activepolygon.topleft, activepolygon.bottomleft, ycoordinate); - var topleft = CSG.Vector2D.Create(x, ycoordinate); - x = CSG.interpolateBetween2DPointsForY(activepolygon.topright, activepolygon.bottomright, ycoordinate); - var topright = CSG.Vector2D.Create(x, ycoordinate); - x = CSG.interpolateBetween2DPointsForY(activepolygon.topleft, activepolygon.bottomleft, nextycoordinate); - var bottomleft = CSG.Vector2D.Create(x, nextycoordinate); - x = CSG.interpolateBetween2DPointsForY(activepolygon.topright, activepolygon.bottomright, nextycoordinate); - var bottomright = CSG.Vector2D.Create(x, nextycoordinate); - var outpolygon = { - topleft: topleft, - topright: topright, - bottomleft: bottomleft, - bottomright: bottomright, - leftline: CSG.Line2D.fromPoints(topleft, bottomleft), - rightline: CSG.Line2D.fromPoints(bottomright, topright) - }; - if (newoutpolygonrow.length > 0) { - var prevoutpolygon = newoutpolygonrow[newoutpolygonrow.length - 1]; - var d1 = outpolygon.topleft.distanceTo(prevoutpolygon.topright); - var d2 = outpolygon.bottomleft.distanceTo(prevoutpolygon.bottomright); - if ((d1 < EPS) && (d2 < EPS)) { - // we can join this polygon with the one to the left: - outpolygon.topleft = prevoutpolygon.topleft; - outpolygon.leftline = prevoutpolygon.leftline; - outpolygon.bottomleft = prevoutpolygon.bottomleft; - newoutpolygonrow.splice(newoutpolygonrow.length - 1, 1); - } - } - newoutpolygonrow.push(outpolygon); - } // for(activepolygon in activepolygons) - if (yindex > 0) { - // try to match the new polygons against the previous row: - var prevcontinuedindexes = {}; - var matchedindexes = {}; - for (var i = 0; i < newoutpolygonrow.length; i++) { - var thispolygon = newoutpolygonrow[i]; - for (var ii = 0; ii < prevoutpolygonrow.length; ii++) { - if (!matchedindexes[ii]) // not already processed? - { - // We have a match if the sidelines are equal or if the top coordinates - // are on the sidelines of the previous polygon - var prevpolygon = prevoutpolygonrow[ii]; - if (prevpolygon.bottomleft.distanceTo(thispolygon.topleft) < EPS) { - if (prevpolygon.bottomright.distanceTo(thispolygon.topright) < EPS) { - // Yes, the top of this polygon matches the bottom of the previous: - matchedindexes[ii] = true; - // Now check if the joined polygon would remain convex: - var d1 = thispolygon.leftline.direction().x - prevpolygon.leftline.direction().x; - var d2 = thispolygon.rightline.direction().x - prevpolygon.rightline.direction().x; - var leftlinecontinues = Math.abs(d1) < EPS; - var rightlinecontinues = Math.abs(d2) < EPS; - var leftlineisconvex = leftlinecontinues || (d1 >= 0); - var rightlineisconvex = rightlinecontinues || (d2 >= 0); - if (leftlineisconvex && rightlineisconvex) { - // yes, both sides have convex corners: - // This polygon will continue the previous polygon - thispolygon.outpolygon = prevpolygon.outpolygon; - thispolygon.leftlinecontinues = leftlinecontinues; - thispolygon.rightlinecontinues = rightlinecontinues; - prevcontinuedindexes[ii] = true; - } - break; - } - } - } // if(!prevcontinuedindexes[ii]) - } // for ii - } // for i - for (var ii = 0; ii < prevoutpolygonrow.length; ii++) { - if (!prevcontinuedindexes[ii]) { - // polygon ends here - // Finish the polygon with the last point(s): - var prevpolygon = prevoutpolygonrow[ii]; - prevpolygon.outpolygon.rightpoints.push(prevpolygon.bottomright); - if (prevpolygon.bottomright.distanceTo(prevpolygon.bottomleft) > EPS) { - // polygon ends with a horizontal line: - prevpolygon.outpolygon.leftpoints.push(prevpolygon.bottomleft); - } - // reverse the left half so we get a counterclockwise circle: - prevpolygon.outpolygon.leftpoints.reverse(); - var points2d = prevpolygon.outpolygon.rightpoints.concat(prevpolygon.outpolygon.leftpoints); - var vertices3d = []; - points2d.map(function(point2d) { - var point3d = orthobasis.to3D(point2d); - var vertex3d = new CSG.Vertex(point3d); - vertices3d.push(vertex3d); - }); - var polygon = new CSG.Polygon(vertices3d, shared, plane); - destpolygons.push(polygon); - } - } - } // if(yindex > 0) - for (var i = 0; i < newoutpolygonrow.length; i++) { - var thispolygon = newoutpolygonrow[i]; - if (!thispolygon.outpolygon) { - // polygon starts here: - thispolygon.outpolygon = { - leftpoints: [], - rightpoints: [] - }; - thispolygon.outpolygon.leftpoints.push(thispolygon.topleft); - if (thispolygon.topleft.distanceTo(thispolygon.topright) > EPS) { - // we have a horizontal line at the top: - thispolygon.outpolygon.rightpoints.push(thispolygon.topright); - } - } else { - // continuation of a previous row - if (!thispolygon.leftlinecontinues) { - thispolygon.outpolygon.leftpoints.push(thispolygon.topleft); - } - if (!thispolygon.rightlinecontinues) { - thispolygon.outpolygon.rightpoints.push(thispolygon.topright); - } - } - } - prevoutpolygonrow = newoutpolygonrow; - } - } // for yindex - } // if(numpolygons > 0) - }; - - //////////////////////////////// - // ## class fuzzyFactory - // This class acts as a factory for objects. We can search for an object with approximately - // the desired properties (say a rectangle with width 2 and height 1) - // The lookupOrCreate() method looks for an existing object (for example it may find an existing rectangle - // with width 2.0001 and height 0.999. If no object is found, the user supplied callback is - // called, which should generate a new object. The new object is inserted into the database - // so it can be found by future lookupOrCreate() calls. - // Constructor: - // numdimensions: the number of parameters for each object - // for example for a 2D rectangle this would be 2 - // tolerance: The maximum difference for each parameter allowed to be considered a match - CSG.fuzzyFactory = function(numdimensions, tolerance) { - this.lookuptable = {}; - this.multiplier = 1.0 / tolerance; - }; - - CSG.fuzzyFactory.prototype = { - // var obj = f.lookupOrCreate([el1, el2, el3], function(elements) {/* create the new object */}); - // Performs a fuzzy lookup of the object with the specified elements. - // If found, returns the existing object - // If not found, calls the supplied callback function which should create a new object with - // the specified properties. This object is inserted in the lookup database. - lookupOrCreate: function(els, creatorCallback) { - var hash = ""; - var multiplier = this.multiplier; - els.forEach(function(el) { - var valueQuantized = Math.round(el * multiplier); - hash += valueQuantized + "/"; - }); - if (hash in this.lookuptable) { - return this.lookuptable[hash]; - } else { - var object = creatorCallback(els); - var hashparts = els.map(function(el) { - var q0 = Math.floor(el * multiplier); - var q1 = q0 + 1; - return ["" + q0 + "/", "" + q1 + "/"]; - }); - var numelements = els.length; - var numhashes = 1 << numelements; - for (var hashmask = 0; hashmask < numhashes; ++hashmask) { - var hashmask_shifted = hashmask; - hash = ""; - hashparts.forEach(function(hashpart) { - hash += hashpart[hashmask_shifted & 1]; - hashmask_shifted >>= 1; - }); - this.lookuptable[hash] = object; - } - return object; - } - }, - }; - - - ////////////////////////////////////// - CSG.fuzzyCSGFactory = function() { - this.vertexfactory = new CSG.fuzzyFactory(3, 1e-5); - this.planefactory = new CSG.fuzzyFactory(4, 1e-5); - this.polygonsharedfactory = {}; - }; - - CSG.fuzzyCSGFactory.prototype = { - getPolygonShared: function(sourceshared) { - var hash = sourceshared.getHash(); - if (hash in this.polygonsharedfactory) { - return this.polygonsharedfactory[hash]; - } else { - this.polygonsharedfactory[hash] = sourceshared; - return sourceshared; - } - }, - - getVertex: function(sourcevertex) { - var elements = [sourcevertex.pos._x, sourcevertex.pos._y, sourcevertex.pos._z]; - var result = this.vertexfactory.lookupOrCreate(elements, function(els) { - return sourcevertex; - }); - return result; - }, - - getPlane: function(sourceplane) { - var elements = [sourceplane.normal._x, sourceplane.normal._y, sourceplane.normal._z, sourceplane.w]; - var result = this.planefactory.lookupOrCreate(elements, function(els) { - return sourceplane; - }); - return result; - }, - - getPolygon: function(sourcepolygon) { - var newplane = this.getPlane(sourcepolygon.plane); - var newshared = this.getPolygonShared(sourcepolygon.shared); - var _this = this; - var newvertices = sourcepolygon.vertices.map(function(vertex) { - return _this.getVertex(vertex); - }); - // two vertices that were originally very close may now have become - // truly identical (referring to the same CSG.Vertex object). - // Remove duplicate vertices: - var newvertices_dedup = []; - if(newvertices.length > 0) { - var prevvertextag = newvertices[newvertices.length-1].getTag(); - newvertices.forEach(function(vertex) { - var vertextag = vertex.getTag(); - if(vertextag != prevvertextag) - { - newvertices_dedup.push(vertex); - } - prevvertextag = vertextag; - }); - } - // If it's degenerate, remove all vertices: - if(newvertices_dedup.length < 3) { - newvertices_dedup = []; - } - return new CSG.Polygon(newvertices_dedup, newshared, newplane); - }, - - getCSG: function(sourcecsg) { - var _this = this; - var newpolygons = []; - sourcecsg.polygons.forEach(function(polygon) { - var newpolygon = _this.getPolygon(polygon); - // see getPolygon above: we may get a polygon with no vertices, discard it: - if(newpolygon.vertices.length >= 3) - { - newpolygons.push(newpolygon); - } - }); - return CSG.fromPolygons(newpolygons); - } - }; - - ////////////////////////////////////// - // Tag factory: we can request a unique tag through CSG.getTag() - CSG.staticTag = 1; - - CSG.getTag = function() { - return CSG.staticTag++; - }; - - ////////////////////////////////////// - // # Class Properties - // This class is used to store properties of a solid - // A property can for example be a CSG.Vertex, a CSG.Plane or a CSG.Line3D - // Whenever an affine transform is applied to the CSG solid, all its properties are - // transformed as well. - // The properties can be stored in a complex nested structure (using arrays and objects) - CSG.Properties = function() {}; - - CSG.Properties.prototype = { - _transform: function(matrix4x4) { - var result = new CSG.Properties(); - CSG.Properties.transformObj(this, result, matrix4x4); - return result; - }, - _merge: function(otherproperties) { - var result = new CSG.Properties(); - CSG.Properties.cloneObj(this, result); - CSG.Properties.addFrom(result, otherproperties); - return result; - } - }; - - CSG.Properties.transformObj = function(source, result, matrix4x4) { - for (var propertyname in source) { - if (propertyname == "_transform") continue; - if (propertyname == "_merge") continue; - var propertyvalue = source[propertyname]; - var transformed = propertyvalue; - if (typeof(propertyvalue) == "object") { - if (('transform' in propertyvalue) && (typeof(propertyvalue.transform) == "function")) { - transformed = propertyvalue.transform(matrix4x4); - } else if (propertyvalue instanceof Array) { - transformed = []; - CSG.Properties.transformObj(propertyvalue, transformed, matrix4x4); - } else if (propertyvalue instanceof CSG.Properties) { - transformed = new CSG.Properties(); - CSG.Properties.transformObj(propertyvalue, transformed, matrix4x4); - } - } - result[propertyname] = transformed; - } - }; - - CSG.Properties.cloneObj = function(source, result) { - for (var propertyname in source) { - if (propertyname == "_transform") continue; - if (propertyname == "_merge") continue; - var propertyvalue = source[propertyname]; - var cloned = propertyvalue; - if (typeof(propertyvalue) == "object") { - if (propertyvalue instanceof Array) { - cloned = []; - for (var i = 0; i < propertyvalue.length; i++) { - cloned.push(propertyvalue[i]); - } - } else if (propertyvalue instanceof CSG.Properties) { - cloned = new CSG.Properties(); - CSG.Properties.cloneObj(propertyvalue, cloned); - } - } - result[propertyname] = cloned; - } - }; - - CSG.Properties.addFrom = function(result, otherproperties) { - for (var propertyname in otherproperties) { - if (propertyname == "_transform") continue; - if (propertyname == "_merge") continue; - if ((propertyname in result) && - (typeof(result[propertyname]) == "object") && - (result[propertyname] instanceof CSG.Properties) && - (typeof(otherproperties[propertyname]) == "object") && - (otherproperties[propertyname] instanceof CSG.Properties)) { - CSG.Properties.addFrom(result[propertyname], otherproperties[propertyname]); - } else if (!(propertyname in result)) { - result[propertyname] = otherproperties[propertyname]; - } - } - }; - - ////////////////////////////////////// - // # class Connector - // A connector allows to attach two objects at predefined positions - // For example a servo motor and a servo horn: - // Both can have a Connector called 'shaft' - // The horn can be moved and rotated such that the two connectors match - // and the horn is attached to the servo motor at the proper position. - // Connectors are stored in the properties of a CSG solid so they are - // ge the same transformations applied as the solid - CSG.Connector = function(point, axisvector, normalvector) { - this.point = new CSG.Vector3D(point); - this.axisvector = new CSG.Vector3D(axisvector).unit(); - this.normalvector = new CSG.Vector3D(normalvector).unit(); - }; - - CSG.Connector.prototype = { - normalized: function() { - var axisvector = this.axisvector.unit(); - // make the normal vector truly normal: - var n = this.normalvector.cross(axisvector).unit(); - var normalvector = axisvector.cross(n); - return new CSG.Connector(this.point, axisvector, normalvector); - }, - - transform: function(matrix4x4) { - var point = this.point.multiply4x4(matrix4x4); - var axisvector = this.point.plus(this.axisvector).multiply4x4(matrix4x4).minus(point); - var normalvector = this.point.plus(this.normalvector).multiply4x4(matrix4x4).minus(point); - return new CSG.Connector(point, axisvector, normalvector); - }, - - // Get the transformation matrix to connect this Connector to another connector - // other: a CSG.Connector to which this connector should be connected - // mirror: false: the 'axis' vectors of the connectors should point in the same direction - // true: the 'axis' vectors of the connectors should point in opposite direction - // normalrotation: degrees of rotation between the 'normal' vectors of the two - // connectors - getTransformationTo: function(other, mirror, normalrotation) { - mirror = mirror ? true : false; - normalrotation = normalrotation ? Number(normalrotation) : 0; - var us = this.normalized(); - other = other.normalized(); - // shift to the origin: - var transformation = CSG.Matrix4x4.translation(this.point.negated()); - // construct the plane crossing through the origin and the two axes: - var axesplane = CSG.Plane.anyPlaneFromVector3Ds( - new CSG.Vector3D(0, 0, 0), us.axisvector, other.axisvector); - var axesbasis = new CSG.OrthoNormalBasis(axesplane); - var angle1 = axesbasis.to2D(us.axisvector).angle(); - var angle2 = axesbasis.to2D(other.axisvector).angle(); - var rotation = 180.0 * (angle2 - angle1) / Math.PI; - if (mirror) rotation += 180.0; - transformation = transformation.multiply(axesbasis.getProjectionMatrix()); - transformation = transformation.multiply(CSG.Matrix4x4.rotationZ(rotation)); - transformation = transformation.multiply(axesbasis.getInverseProjectionMatrix()); - var usAxesAligned = us.transform(transformation); - // Now we have done the transformation for aligning the axes. - // We still need to align the normals: - var normalsplane = CSG.Plane.fromNormalAndPoint(other.axisvector, new CSG.Vector3D(0, 0, 0)); - var normalsbasis = new CSG.OrthoNormalBasis(normalsplane); - angle1 = normalsbasis.to2D(usAxesAligned.normalvector).angle(); - angle2 = normalsbasis.to2D(other.normalvector).angle(); - rotation = 180.0 * (angle2 - angle1) / Math.PI; - rotation += normalrotation; - transformation = transformation.multiply(normalsbasis.getProjectionMatrix()); - transformation = transformation.multiply(CSG.Matrix4x4.rotationZ(rotation)); - transformation = transformation.multiply(normalsbasis.getInverseProjectionMatrix()); - // and translate to the destination point: - transformation = transformation.multiply(CSG.Matrix4x4.translation(other.point)); - // var usAligned = us.transform(transformation); - return transformation; - }, - - axisLine: function() { - return new CSG.Line3D(this.point, this.axisvector); - }, - - // creates a new Connector, with the connection point moved in the direction of the axisvector - extend: function(distance) { - var newpoint = this.point.plus(this.axisvector.unit().times(distance)); - return new CSG.Connector(newpoint, this.axisvector, this.normalvector); - } - }; - - CSG.ConnectorList = function(connectors) { - this.connectors_ = connectors ? connectors.slice() : []; - }; - - CSG.ConnectorList.defaultNormal = [0, 0, 1]; - - CSG.ConnectorList.fromPath2D = function(path2D, arg1, arg2) { - if (arguments.length === 3) { - return CSG.ConnectorList._fromPath2DTangents(path2D, arg1, arg2); - } else if (arguments.length == 2) { - return CSG.ConnectorList._fromPath2DExplicit(path2D, arg1); - } else { - throw("call with path2D and either 2 direction vectors, or a function returning direction vectors"); - } - }; - - /* - * calculate the connector axisvectors by calculating the "tangent" for path2D. - * This is undefined for start and end points, so axis for these have to be manually - * provided. - */ - CSG.ConnectorList._fromPath2DTangents = function(path2D, start, end) { - // path2D - var axis; - var pathLen = path2D.points.length; - var result = new CSG.ConnectorList([new CSG.Connector(path2D.points[0], - start, CSG.ConnectorList.defaultNormal)]); - // middle points - path2D.points.slice(1, pathLen - 1).forEach(function(p2, i) { - axis = path2D.points[i + 2].minus(path2D.points[i]).toVector3D(0); - result.appendConnector(new CSG.Connector(p2.toVector3D(0), axis, - CSG.ConnectorList.defaultNormal)); - }, this); - result.appendConnector(new CSG.Connector(path2D.points[pathLen - 1], end, - CSG.ConnectorList.defaultNormal)); - result.closed = path2D.closed; - return result; - }; - - /* - * angleIsh: either a static angle, or a function(point) returning an angle - */ - CSG.ConnectorList._fromPath2DExplicit = function(path2D, angleIsh) { - function getAngle(angleIsh, pt, i) { - if (typeof angleIsh == 'function') { - angleIsh = angleIsh(pt, i); - } - return angleIsh; - } - var result = new CSG.ConnectorList( - path2D.points.map(function(p2, i) { - return new CSG.Connector(p2.toVector3D(0), - CSG.Vector3D.Create(1, 0, 0).rotateZ(getAngle(angleIsh, p2, i)), - CSG.ConnectorList.defaultNormal); - }, this) - ); - result.closed = path2D.closed; - return result; - }; - - - CSG.ConnectorList.prototype = { - setClosed: function(bool) { - this.closed = !!closed; - }, - appendConnector: function(conn) { - this.connectors_.push(conn); - }, - /* - * arguments: cagish: a cag or a function(connector) returning a cag - * closed: whether the 3d path defined by connectors location - * should be closed or stay open - * Note: don't duplicate connectors in the path - * TODO: consider an option "maySelfIntersect" to close & force union all single segments - */ - followWith: function(cagish) { - this.verify(); - function getCag(cagish, connector) { - if (typeof cagish == "function") { - cagish = cagish(connector.point, connector.axisvector, connector.normalvector); - } - return cagish; - } - - var polygons = [], currCag; - var prevConnector = this.connectors_[this.connectors_.length - 1]; - var prevCag = getCag(cagish, prevConnector); - // add walls - this.connectors_.forEach(function(connector, notFirst) { - currCag = getCag(cagish, connector); - if (notFirst || this.closed) { - polygons.push.apply(polygons, prevCag._toWallPolygons({ - toConnector1: prevConnector, toConnector2: connector, cag: currCag})); - } else { - // it is the first, and shape not closed -> build start wall - polygons.push.apply(polygons, - currCag._toPlanePolygons({toConnector: connector, flipped: true})); - } - if (notFirst == this.connectors_.length - 1 && !this.closed) { - // build end wall - polygons.push.apply(polygons, - currCag._toPlanePolygons({toConnector: connector})); - } - prevCag = currCag; - prevConnector = connector; - }, this); - return CSG.fromPolygons(polygons).reTesselated().canonicalized(); - }, - /* - * general idea behind these checks: connectors need to have smooth transition from one to another - * TODO: add a check that 2 follow-on CAGs are not intersecting - */ - verify: function() { - var connI, connI1, dPosToAxis, axisToNextAxis; - for (var i = 0; i < this.connectors_.length - 1; i++) { - connI = this.connectors_[i], connI1 = this.connectors_[i + 1]; - if (connI1.point.minus(connI.point).dot(connI.axisvector) <= 0) { - throw("Invalid ConnectorList. Each connectors position needs to be within a <90deg range of previous connectors axisvector"); - } - if (connI.axisvector.dot(connI1.axisvector) <= 0) { - throw("invalid ConnectorList. No neighboring connectors axisvectors may span a >=90deg angle"); - } - } - } - }; - - ////////////////////////////////////// - // # Class Path2D - CSG.Path2D = function(points, closed) { - closed = !!closed; - points = points || []; - // re-parse the points into CSG.Vector2D - // and remove any duplicate points - var prevpoint = null; - if (closed && (points.length > 0)) { - prevpoint = new CSG.Vector2D(points[points.length - 1]); - } - var newpoints = []; - points.map(function(point) { - point = new CSG.Vector2D(point); - var skip = false; - if (prevpoint !== null) { - var distance = point.distanceTo(prevpoint); - skip = distance < 1e-5; - } - if (!skip) newpoints.push(point); - prevpoint = point; - }); - this.points = newpoints; - this.closed = closed; - }; - - /* - Construct a (part of a) circle. Parameters: - options.center: the center point of the arc (CSG.Vector2D or array [x,y]) - options.radius: the circle radius (float) - options.startangle: the starting angle of the arc, in degrees - 0 degrees corresponds to [1,0] - 90 degrees to [0,1] - and so on - options.endangle: the ending angle of the arc, in degrees - options.resolution: number of points per 360 degree of rotation - options.maketangent: adds two extra tiny line segments at both ends of the circle - this ensures that the gradients at the edges are tangent to the circle - Returns a CSG.Path2D. The path is not closed (even if it is a 360 degree arc). - close() the resulting path if you want to create a true circle. - */ - CSG.Path2D.arc = function(options) { - var center = CSG.parseOptionAs2DVector(options, "center", 0); - var radius = CSG.parseOptionAsFloat(options, "radius", 1); - var startangle = CSG.parseOptionAsFloat(options, "startangle", 0); - var endangle = CSG.parseOptionAsFloat(options, "endangle", 360); - var resolution = CSG.parseOptionAsInt(options, "resolution", CSG.defaultResolution2D); - var maketangent = CSG.parseOptionAsBool(options, "maketangent", false); - // no need to make multiple turns: - while (endangle - startangle >= 720) { - endangle -= 360; - } - while (endangle - startangle <= -720) { - endangle += 360; - } - var points = [], - point; - var absangledif = Math.abs(endangle - startangle); - if (absangledif < 1e-5) { - point = CSG.Vector2D.fromAngle(startangle / 180.0 * Math.PI).times(radius); - points.push(point.plus(center)); - } else { - var numsteps = Math.floor(resolution * absangledif / 360) + 1; - var edgestepsize = numsteps * 0.5 / absangledif; // step size for half a degree - if (edgestepsize > 0.25) edgestepsize = 0.25; - var numsteps_mod = maketangent ? (numsteps + 2) : numsteps; - for (var i = 0; i <= numsteps_mod; i++) { - var step = i; - if (maketangent) { - step = (i - 1) * (numsteps - 2 * edgestepsize) / numsteps + edgestepsize; - if (step < 0) step = 0; - if (step > numsteps) step = numsteps; - } - var angle = startangle + step * (endangle - startangle) / numsteps; - point = CSG.Vector2D.fromAngle(angle / 180.0 * Math.PI).times(radius); - points.push(point.plus(center)); - } - } - return new CSG.Path2D(points, false); - }; - - CSG.Path2D.prototype = { - concat: function(otherpath) { - if (this.closed || otherpath.closed) { - throw new Error("Paths must not be closed"); - } - var newpoints = this.points.concat(otherpath.points); - return new CSG.Path2D(newpoints); - }, - - appendPoint: function(point) { - if (this.closed) { - throw new Error("Path must not be closed"); - } - point = new CSG.Vector2D(point); // cast to Vector2D - var newpoints = this.points.concat([point]); - return new CSG.Path2D(newpoints); - }, - - appendPoints: function(points) { - if (this.closed) { - throw new Error("Path must not be closed"); - } - var newpoints = this.points; - points.forEach(function(point) { - newpoints.push(new CSG.Vector2D(point)); // cast to Vector2D - }) - return new CSG.Path2D(newpoints); - }, - - close: function() { - return new CSG.Path2D(this.points, true); - }, - - // Extrude the path by following it with a rectangle (upright, perpendicular to the path direction) - // Returns a CSG solid - // width: width of the extrusion, in the z=0 plane - // height: height of the extrusion in the z direction - // resolution: number of segments per 360 degrees for the curve in a corner - rectangularExtrude: function(width, height, resolution) { - var cag = this.expandToCAG(width / 2, resolution); - var result = cag.extrude({ - offset: [0, 0, height] - }); - return result; - }, - - // Expand the path to a CAG - // This traces the path with a circle with radius pathradius - expandToCAG: function(pathradius, resolution) { - var sides = []; - var numpoints = this.points.length; - var startindex = 0; - if (this.closed && (numpoints > 2)) startindex = -1; - var prevvertex; - for (var i = startindex; i < numpoints; i++) { - var pointindex = i; - if (pointindex < 0) pointindex = numpoints - 1; - var point = this.points[pointindex]; - var vertex = new CAG.Vertex(point); - if (i > startindex) { - var side = new CAG.Side(prevvertex, vertex); - sides.push(side); - } - prevvertex = vertex; - } - var shellcag = CAG.fromSides(sides); - var expanded = shellcag.expandedShell(pathradius, resolution); - return expanded; - }, - - innerToCAG: function() { - if (!this.closed) throw new Error("The path should be closed!"); - return CAG.fromPoints(this.points); - }, - - transform: function(matrix4x4) { - var newpoints = this.points.map(function(point) { - return point.multiply4x4(matrix4x4); - }); - return new CSG.Path2D(newpoints, this.closed); - }, - - appendBezier: function(controlpoints, options) { - if (arguments.length < 2) { - options = {}; - } - if (this.closed) { - throw new Error("Path must not be closed"); - } - if (!(controlpoints instanceof Array)) { - throw new Error("appendBezier: should pass an array of control points") - } - if (controlpoints.length < 1) { - throw new Error("appendBezier: need at least 1 control point") - } - if (this.points.length < 1) { - throw new Error("appendBezier: path must already contain a point (the endpoint of the path is used as the starting point for the bezier curve)"); - } - var resolution = CSG.parseOptionAsInt(options, "resolution", CSG.defaultResolution2D); - if (resolution < 4) resolution = 4; - var factorials = []; - var controlpoints_parsed = []; - controlpoints_parsed.push(this.points[this.points.length - 1]); // start at the previous end point - for (var i = 0; i < controlpoints.length; ++i) { - var p = controlpoints[i]; - if (p === null) { - // we can pass null as the first control point. In that case a smooth gradient is ensured: - if (i != 0) { - throw new Error("appendBezier: null can only be passed as the first control point"); - } - if (controlpoints.length < 2) { - throw new Error("appendBezier: null can only be passed if there is at least one more control point"); - } - var lastBezierControlPoint; - if ('lastBezierControlPoint' in this) { - lastBezierControlPoint = this.lastBezierControlPoint; - } else { - if (this.points.length < 2) { - throw new Error("appendBezier: null is passed as a control point but this requires a previous bezier curve or at least two points in the existing path"); - } - lastBezierControlPoint = this.points[this.points.length - 2]; - } - // mirror the last bezier control point: - p = this.points[this.points.length - 1].times(2).minus(lastBezierControlPoint); - } else { - p = new CSG.Vector2D(p); // cast to Vector2D - } - controlpoints_parsed.push(p); - } - var bezier_order = controlpoints_parsed.length - 1; - var fact = 1; - for (var i = 0; i <= bezier_order; ++i) { - if (i > 0) fact *= i; - factorials.push(fact); - } - var binomials = []; - for (var i = 0; i <= bezier_order; ++i) { - var binomial = factorials[bezier_order] / (factorials[i] * factorials[bezier_order - i]); - binomials.push(binomial); - } - var getPointForT = function(t) { - var t_k = 1; // = pow(t,k) - var one_minus_t_n_minus_k = Math.pow(1 - t, bezier_order); // = pow( 1-t, bezier_order - k) - var inv_1_minus_t = (t != 1) ? (1 / (1 - t)) : 1; - var point = new CSG.Vector2D(0, 0); - for (var k = 0; k <= bezier_order; ++k) { - if (k == bezier_order) one_minus_t_n_minus_k = 1; - var bernstein_coefficient = binomials[k] * t_k * one_minus_t_n_minus_k; - point = point.plus(controlpoints_parsed[k].times(bernstein_coefficient)); - t_k *= t; - one_minus_t_n_minus_k *= inv_1_minus_t; - } - return point; - }; - var newpoints = []; - var newpoints_t = []; - var numsteps = bezier_order + 1; - for (var i = 0; i < numsteps; ++i) { - var t = i / (numsteps - 1); - var point = getPointForT(t); - newpoints.push(point); - newpoints_t.push(t); - } - // subdivide each segment until the angle at each vertex becomes small enough: - var subdivide_base = 1; - var maxangle = Math.PI * 2 / resolution; // segments may have differ no more in angle than this - var maxsinangle = Math.sin(maxangle); - while (subdivide_base < newpoints.length - 1) { - var dir1 = newpoints[subdivide_base].minus(newpoints[subdivide_base - 1]).unit(); - var dir2 = newpoints[subdivide_base + 1].minus(newpoints[subdivide_base]).unit(); - var sinangle = dir1.cross(dir2); // this is the sine of the angle - if (Math.abs(sinangle) > maxsinangle) { - // angle is too big, we need to subdivide - var t0 = newpoints_t[subdivide_base - 1]; - var t1 = newpoints_t[subdivide_base + 1]; - var t0_new = t0 + (t1 - t0) * 1 / 3; - var t1_new = t0 + (t1 - t0) * 2 / 3; - var point0_new = getPointForT(t0_new); - var point1_new = getPointForT(t1_new); - // remove the point at subdivide_base and replace with 2 new points: - newpoints.splice(subdivide_base, 1, point0_new, point1_new); - newpoints_t.splice(subdivide_base, 1, t0_new, t1_new); - // re - evaluate the angles, starting at the previous junction since it has changed: - subdivide_base--; - if (subdivide_base < 1) subdivide_base = 1; - } else { - ++subdivide_base; - } - } - // append to the previous points, but skip the first new point because it is identical to the last point: - newpoints = this.points.concat(newpoints.slice(1)); - var result = new CSG.Path2D(newpoints); - result.lastBezierControlPoint = controlpoints_parsed[controlpoints_parsed.length - 2]; - return result; - }, - - /* - options: - .resolution // smoothness of the arc (number of segments per 360 degree of rotation) - // to create a circular arc: - .radius - // to create an elliptical arc: - .xradius - .yradius - .xaxisrotation // the rotation (in degrees) of the x axis of the ellipse with respect to the x axis of our coordinate system - // this still leaves 4 possible arcs between the two given points. The following two flags select which one we draw: - .clockwise // = true | false (default is false). Two of the 4 solutions draw clockwise with respect to the center point, the other 2 counterclockwise - .large // = true | false (default is false). Two of the 4 solutions are an arc longer than 180 degrees, the other two are <= 180 degrees - This implementation follows the SVG arc specs. For the details see - http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands - */ - appendArc: function(endpoint, options) { - if (arguments.length < 2) { - options = {}; - } - if (this.closed) { - throw new Error("Path must not be closed"); - } - if (this.points.length < 1) { - throw new Error("appendArc: path must already contain a point (the endpoint of the path is used as the starting point for the arc)"); - } - var resolution = CSG.parseOptionAsInt(options, "resolution", CSG.defaultResolution2D); - if (resolution < 4) resolution = 4; - var xradius, yradius; - if (('xradius' in options) || ('yradius' in options)) { - if ('radius' in options) { - throw new Error("Should either give an xradius and yradius parameter, or a radius parameter"); - } - xradius = CSG.parseOptionAsFloat(options, "xradius", 0); - yradius = CSG.parseOptionAsFloat(options, "yradius", 0); - } else { - xradius = CSG.parseOptionAsFloat(options, "radius", 0); - yradius = xradius; - } - var xaxisrotation = CSG.parseOptionAsFloat(options, "xaxisrotation", 0); - var clockwise = CSG.parseOptionAsBool(options, "clockwise", false); - var largearc = CSG.parseOptionAsBool(options, "large", false); - var startpoint = this.points[this.points.length - 1]; - endpoint = new CSG.Vector2D(endpoint); - - var sweep_flag = !clockwise; - var newpoints = []; - if ((xradius == 0) || (yradius == 0)) { - // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes: - // If rx = 0 or ry = 0, then treat this as a straight line from (x1, y1) to (x2, y2) and stop - newpoints.push(endpoint); - } else { - xradius = Math.abs(xradius); - yradius = Math.abs(yradius); - - // see http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes : - var phi = xaxisrotation * Math.PI / 180.0; - var cosphi = Math.cos(phi); - var sinphi = Math.sin(phi); - var minushalfdistance = startpoint.minus(endpoint).times(0.5); - // F.6.5.1: - var start_translated = new CSG.Vector2D(cosphi * minushalfdistance.x + sinphi * minushalfdistance.y, -sinphi * minushalfdistance.x + cosphi * minushalfdistance.y); - // F.6.6.2: - var biglambda = start_translated.x * start_translated.x / (xradius * xradius) + start_translated.y * start_translated.y / (yradius * yradius); - if (biglambda > 1) { - // F.6.6.3: - var sqrtbiglambda = Math.sqrt(biglambda); - xradius *= sqrtbiglambda; - yradius *= sqrtbiglambda; - } - // F.6.5.2: - var multiplier1 = Math.sqrt((xradius * xradius * yradius * yradius - xradius * xradius * start_translated.y * start_translated.y - yradius * yradius * start_translated.x * start_translated.x) / (xradius * xradius * start_translated.y * start_translated.y + yradius * yradius * start_translated.x * start_translated.x)); - if (sweep_flag == largearc) multiplier1 = -multiplier1; - var center_translated = new CSG.Vector2D(xradius * start_translated.y / yradius, -yradius * start_translated.x / xradius).times(multiplier1); - // F.6.5.3: - var center = new CSG.Vector2D(cosphi * center_translated.x - sinphi * center_translated.y, sinphi * center_translated.x + cosphi * center_translated.y).plus((startpoint.plus(endpoint)).times(0.5)); - // F.6.5.5: - var vec1 = new CSG.Vector2D((start_translated.x - center_translated.x) / xradius, (start_translated.y - center_translated.y) / yradius); - var vec2 = new CSG.Vector2D((-start_translated.x - center_translated.x) / xradius, (-start_translated.y - center_translated.y) / yradius); - var theta1 = vec1.angleRadians(); - var theta2 = vec2.angleRadians(); - var deltatheta = theta2 - theta1; - deltatheta = deltatheta % (2 * Math.PI); - if ((!sweep_flag) && (deltatheta > 0)) { - deltatheta -= 2 * Math.PI; - } else if ((sweep_flag) && (deltatheta < 0)) { - deltatheta += 2 * Math.PI; - } - - // Ok, we have the center point and angle range (from theta1, deltatheta radians) so we can create the ellipse - var numsteps = Math.ceil(Math.abs(deltatheta) / (2 * Math.PI) * resolution) + 1; - if (numsteps < 1) numsteps = 1; - for (var step = 1; step <= numsteps; step++) { - var theta = theta1 + step / numsteps * deltatheta; - var costheta = Math.cos(theta); - var sintheta = Math.sin(theta); - // F.6.3.1: - var point = new CSG.Vector2D(cosphi * xradius * costheta - sinphi * yradius * sintheta, sinphi * xradius * costheta + cosphi * yradius * sintheta).plus(center); - newpoints.push(point); - } - } - newpoints = this.points.concat(newpoints); - var result = new CSG.Path2D(newpoints); - return result; - }, - }; - - // Add several convenience methods to the classes that support a transform() method: - CSG.addTransformationMethodsToPrototype = function(prot) { - prot.mirrored = function(plane) { - return this.transform(CSG.Matrix4x4.mirroring(plane)); - }; - - prot.mirroredX = function() { - var plane = new CSG.Plane(CSG.Vector3D.Create(1, 0, 0), 0); - return this.mirrored(plane); - }; - - prot.mirroredY = function() { - var plane = new CSG.Plane(CSG.Vector3D.Create(0, 1, 0), 0); - return this.mirrored(plane); - }; - - prot.mirroredZ = function() { - var plane = new CSG.Plane(CSG.Vector3D.Create(0, 0, 1), 0); - return this.mirrored(plane); - }; - - prot.translate = function(v) { - return this.transform(CSG.Matrix4x4.translation(v)); - }; - - prot.scale = function(f) { - return this.transform(CSG.Matrix4x4.scaling(f)); - }; - - prot.rotateX = function(deg) { - return this.transform(CSG.Matrix4x4.rotationX(deg)); - }; - - prot.rotateY = function(deg) { - return this.transform(CSG.Matrix4x4.rotationY(deg)); - }; - - prot.rotateZ = function(deg) { - return this.transform(CSG.Matrix4x4.rotationZ(deg)); - }; - - prot.rotate = function(rotationCenter, rotationAxis, degrees) { - return this.transform(CSG.Matrix4x4.rotation(rotationCenter, rotationAxis, degrees)); - }; - - prot.rotateEulerAngles = function(alpha, beta, gamma, position) { - position = position || [0,0,0]; - - var Rz1 = CSG.Matrix4x4.rotationZ(alpha); - var Rx = CSG.Matrix4x4.rotationX(beta); - var Rz2 = CSG.Matrix4x4.rotationZ(gamma); - var T = CSG.Matrix4x4.translation(new CSG.Vector3D(position)); - - return this.transform(Rz2.multiply(Rx).multiply(Rz1).multiply(T)); - }; - }; - - // TODO: consider generalization and adding to addTransformationMethodsToPrototype - CSG.addCenteringToPrototype = function(prot, axes) { - prot.center = function(cAxes) { - cAxes = Array.prototype.map.call(arguments, function(a) { - return a; //.toLowerCase(); - }); - // no args: center on all axes - if (!cAxes.length) { - cAxes = axes.slice(); - } - var b = this.getBounds(); - return this.translate(axes.map(function(a) { - return cAxes.indexOf(a) > -1 ? - -(b[0][a] + b[1][a])/2 : 0; - })); - }; - }; - - ////////////////// - // CAG: solid area geometry: like CSG but 2D - // Each area consists of a number of sides - // Each side is a line between 2 points - var CAG = function() { - this.sides = []; - }; - - // Construct a CAG from a list of `CAG.Side` instances. - CAG.fromSides = function(sides) { - var cag = new CAG(); - cag.sides = sides; - return cag; - }; - - // Construct a CAG from a list of points (a polygon) - // Rotation direction of the points is not relevant. Points can be a convex or concave polygon. - // Polygon must not self intersect - CAG.fromPoints = function(points) { - var numpoints = points.length; - if (numpoints < 3) throw new Error("CAG shape needs at least 3 points"); - var sides = []; - var prevpoint = new CSG.Vector2D(points[numpoints - 1]); - var prevvertex = new CAG.Vertex(prevpoint); - points.map(function(p) { - var point = new CSG.Vector2D(p); - var vertex = new CAG.Vertex(point); - var side = new CAG.Side(prevvertex, vertex); - sides.push(side); - prevvertex = vertex; - }); - var result = CAG.fromSides(sides); - if (result.isSelfIntersecting()) { - throw new Error("Polygon is self intersecting!"); - } - var area = result.area(); - if (Math.abs(area) < 1e-5) { - throw new Error("Degenerate polygon!"); - } - if (area < 0) { - result = result.flipped(); - } - result = result.canonicalized(); - return result; - }; - - // Like CAG.fromPoints but does not check if it's a valid polygon. - // Points should rotate counter clockwise - CAG.fromPointsNoCheck = function(points) { - var sides = []; - var prevpoint = new CSG.Vector2D(points[points.length - 1]); - var prevvertex = new CAG.Vertex(prevpoint); - points.map(function(p) { - var point = new CSG.Vector2D(p); - var vertex = new CAG.Vertex(point); - var side = new CAG.Side(prevvertex, vertex); - sides.push(side); - prevvertex = vertex; - }); - return CAG.fromSides(sides); - }; - - // Converts a CSG to a CAG. The CSG must consist of polygons with only z coordinates +1 and -1 - // as constructed by CAG._toCSGWall(-1, 1). This is so we can use the 3D union(), intersect() etc - CAG.fromFakeCSG = function(csg) { - var sides = csg.polygons.map(function(p) { - return CAG.Side._fromFakePolygon(p); - }) - .filter(function(s) { - return s !== null; - }); - return CAG.fromSides(sides); - }; - - // see if the line between p0start and p0end intersects with the line between p1start and p1end - // returns true if the lines strictly intersect, the end points are not counted! - CAG.linesIntersect = function(p0start, p0end, p1start, p1end) { - if (p0end.equals(p1start) || p1end.equals(p0start)) { - var d = p1end.minus(p1start).unit().plus(p0end.minus(p0start).unit()).length(); - if (d < 1e-5) { - return true; - } - } else { - var d0 = p0end.minus(p0start); - var d1 = p1end.minus(p1start); - if (Math.abs(d0.cross(d1)) < 1e-9) return false; // lines are parallel - var alphas = CSG.solve2Linear(-d0.x, d1.x, -d0.y, d1.y, p0start.x - p1start.x, p0start.y - p1start.y); - if ((alphas[0] > 1e-6) && (alphas[0] < 0.999999) && (alphas[1] > 1e-5) && (alphas[1] < 0.999999)) return true; - // if( (alphas[0] >= 0) && (alphas[0] <= 1) && (alphas[1] >= 0) && (alphas[1] <= 1) ) return true; - } - return false; - }; - - /* Construct a circle - options: - center: a 2D center point - radius: a scalar - resolution: number of sides per 360 degree rotation - returns a CAG object - */ - CAG.circle = function(options) { - options = options || {}; - var center = CSG.parseOptionAs2DVector(options, "center", [0, 0]); - var radius = CSG.parseOptionAsFloat(options, "radius", 1); - var resolution = CSG.parseOptionAsInt(options, "resolution", CSG.defaultResolution2D); - var sides = []; - var prevvertex; - for (var i = 0; i <= resolution; i++) { - var radians = 2 * Math.PI * i / resolution; - var point = CSG.Vector2D.fromAngleRadians(radians).times(radius).plus(center); - var vertex = new CAG.Vertex(point); - if (i > 0) { - sides.push(new CAG.Side(prevvertex, vertex)); - } - prevvertex = vertex; - } - return CAG.fromSides(sides); - }; - - /* Construct a rectangle - options: - center: a 2D center point - radius: a 2D vector with width and height - returns a CAG object - */ - CAG.rectangle = function(options) { - options = options || {}; - var c, r; - if (('corner1' in options) || ('corner2' in options)) { - if (('center' in options) || ('radius' in options)) { - throw new Error("rectangle: should either give a radius and center parameter, or a corner1 and corner2 parameter") - } - corner1 = CSG.parseOptionAs2DVector(options, "corner1", [0, 0]); - corner2 = CSG.parseOptionAs2DVector(options, "corner2", [1, 1]); - c = corner1.plus(corner2).times(0.5); - r = corner2.minus(corner1).times(0.5); - } else { - c = CSG.parseOptionAs2DVector(options, "center", [0, 0]); - r = CSG.parseOptionAs2DVector(options, "radius", [1, 1]); - } - r = r.abs(); // negative radii make no sense - var rswap = new CSG.Vector2D(r.x, -r.y); - var points = [ - c.plus(r), c.plus(rswap), c.minus(r), c.minus(rswap) - ]; - return CAG.fromPoints(points); - }; - - // var r = CSG.roundedRectangle({ - // center: [0, 0], - // radius: [2, 1], - // roundradius: 0.2, - // resolution: 8, - // }); - CAG.roundedRectangle = function(options) { - options = options || {}; - var center, radius; - if (('corner1' in options) || ('corner2' in options)) { - if (('center' in options) || ('radius' in options)) { - throw new Error("roundedRectangle: should either give a radius and center parameter, or a corner1 and corner2 parameter") - } - corner1 = CSG.parseOptionAs2DVector(options, "corner1", [0, 0]); - corner2 = CSG.parseOptionAs2DVector(options, "corner2", [1, 1]); - center = corner1.plus(corner2).times(0.5); - radius = corner2.minus(corner1).times(0.5); - } else { - center = CSG.parseOptionAs2DVector(options, "center", [0, 0]); - radius = CSG.parseOptionAs2DVector(options, "radius", [1, 1]); - } - radius = radius.abs(); // negative radii make no sense - var roundradius = CSG.parseOptionAsFloat(options, "roundradius", 0.2); - var resolution = CSG.parseOptionAsInt(options, "resolution", CSG.defaultResolution2D); - var maxroundradius = Math.min(radius.x, radius.y); - maxroundradius -= 0.1; - roundradius = Math.min(roundradius, maxroundradius); - roundradius = Math.max(0, roundradius); - radius = new CSG.Vector2D(radius.x - roundradius, radius.y - roundradius); - var rect = CAG.rectangle({ - center: center, - radius: radius - }); - if (roundradius > 0) { - rect = rect.expand(roundradius, resolution); - } - return rect; - }; - - // Reconstruct a CAG from the output of toCompactBinary() - CAG.fromCompactBinary = function(bin) { - if (bin['class'] != "CAG") throw new Error("Not a CAG"); - var vertices = []; - var vertexData = bin.vertexData; - var numvertices = vertexData.length / 2; - var arrayindex = 0; - for (var vertexindex = 0; vertexindex < numvertices; vertexindex++) { - var x = vertexData[arrayindex++]; - var y = vertexData[arrayindex++]; - var pos = new CSG.Vector2D(x, y); - var vertex = new CAG.Vertex(pos); - vertices.push(vertex); - } - - var sides = []; - var numsides = bin.sideVertexIndices.length / 2; - arrayindex = 0; - for (var sideindex = 0; sideindex < numsides; sideindex++) { - var vertexindex0 = bin.sideVertexIndices[arrayindex++]; - var vertexindex1 = bin.sideVertexIndices[arrayindex++]; - var side = new CAG.Side(vertices[vertexindex0], vertices[vertexindex1]); - sides.push(side); - } - var cag = CAG.fromSides(sides); - cag.isCanonicalized = true; - return cag; - }; - - function fnSortByIndex(a, b) { - return a.index - b.index; - } - - CAG.prototype = { - toString: function() { - var result = "CAG (" + this.sides.length + " sides):\n"; - this.sides.map(function(side) { - result += " " + side.toString() + "\n"; - }); - return result; - }, - - _toCSGWall: function(z0, z1) { - var polygons = this.sides.map(function(side) { - return side.toPolygon3D(z0, z1); - }); - return CSG.fromPolygons(polygons); - }, - - _toVector3DPairs: function(m) { - // transform m - var pairs = this.sides.map(function(side) { - var p0 = side.vertex0.pos, p1 = side.vertex1.pos; - return [CSG.Vector3D.Create(p0.x, p0.y, 0), - CSG.Vector3D.Create(p1.x, p1.y, 0)]; - }); - if (typeof m != 'undefined') { - pairs = pairs.map(function(pair) { - return pair.map(function(v) { - return v.transform(m); - }); - }); - } - return pairs; - }, - - /* - * transform a cag into the polygons of a corresponding 3d plane, positioned per options - * Accepts a connector for plane positioning, or optionally - * single translation, axisVector, normalVector arguments - * (toConnector has precedence over single arguments if provided) - */ - _toPlanePolygons: function(options) { - var flipped = options.flipped || false; - // reference connector for transformation - var origin = [0, 0, 0], defaultAxis = [0, 0, 1], defaultNormal = [0, 1, 0]; - var thisConnector = new CSG.Connector(origin, defaultAxis, defaultNormal); - // translated connector per options - var translation = options.translation || origin; - var axisVector = options.axisVector || defaultAxis; - var normalVector = options.normalVector || defaultNormal; - // will override above if options has toConnector - var toConnector = options.toConnector || - new CSG.Connector(translation, axisVector, normalVector); - // resulting transform - var m = thisConnector.getTransformationTo(toConnector, false, 0); - // create plane as a (partial non-closed) CSG in XY plane - var bounds = this.getBounds(); - bounds[0] = bounds[0].minus(new CSG.Vector2D(1, 1)); - bounds[1] = bounds[1].plus(new CSG.Vector2D(1, 1)); - var csgshell = this._toCSGWall(-1, 1); - var csgplane = CSG.fromPolygons([new CSG.Polygon([ - new CSG.Vertex(new CSG.Vector3D(bounds[0].x, bounds[0].y, 0)), - new CSG.Vertex(new CSG.Vector3D(bounds[1].x, bounds[0].y, 0)), - new CSG.Vertex(new CSG.Vector3D(bounds[1].x, bounds[1].y, 0)), - new CSG.Vertex(new CSG.Vector3D(bounds[0].x, bounds[1].y, 0)) - ])]); - if (flipped) { - csgplane = csgplane.invert(); - } - // intersectSub -> prevent premature retesselate/canonicalize - csgplane = csgplane.intersectSub(csgshell); - // only keep the polygons in the z plane: - var polys = csgplane.polygons.filter(function(polygon) { - return Math.abs(polygon.plane.normal.z) > 0.99; - }); - // finally, position the plane per passed transformations - return polys.map(function(poly) { - return poly.transform(m); - }); - }, - - - /* - * given 2 connectors, this returns all polygons of a "wall" between 2 - * copies of this cag, positioned in 3d space as "bottom" and - * "top" plane per connectors toConnector1, and toConnector2, respectively - */ - _toWallPolygons: function(options) { - // normals are going to be correct as long as toConn2.point - toConn1.point - // points into cag normal direction (check in caller) - // arguments: options.toConnector1, options.toConnector2, options.cag - // walls go from toConnector1 to toConnector2 - // optionally, target cag to point to - cag needs to have same number of sides as this! - var origin = [0, 0, 0], defaultAxis = [0, 0, 1], defaultNormal = [0, 1, 0]; - var thisConnector = new CSG.Connector(origin, defaultAxis, defaultNormal); - // arguments: - var toConnector1 = options.toConnector1; - // var toConnector2 = new CSG.Connector([0, 0, -30], defaultAxis, defaultNormal); - var toConnector2 = options.toConnector2; - if (!(toConnector1 instanceof CSG.Connector && toConnector2 instanceof CSG.Connector)) { - throw('could not parse CSG.Connector arguments toConnector1 or toConnector2'); - } - if (options.cag) { - if (options.cag.sides.length != this.sides.length) { - throw('target cag needs same sides count as start cag'); - } - } - // target cag is same as this unless specified - var toCag = options.cag || this; - var m1 = thisConnector.getTransformationTo(toConnector1, false, 0); - var m2 = thisConnector.getTransformationTo(toConnector2, false, 0); - var vps1 = this._toVector3DPairs(m1); - var vps2 = toCag._toVector3DPairs(m2); - - var polygons = []; - vps1.forEach(function(vp1, i) { - polygons.push(new CSG.Polygon([ - new CSG.Vertex(vps2[i][1]), new CSG.Vertex(vps2[i][0]), new CSG.Vertex(vp1[0])])); - polygons.push(new CSG.Polygon([ - new CSG.Vertex(vps2[i][1]), new CSG.Vertex(vp1[0]), new CSG.Vertex(vp1[1])])); - }); - return polygons; - }, - - union: function(cag) { - var cags; - if (cag instanceof Array) { - cags = cag; - } else { - cags = [cag]; - } - var r = this._toCSGWall(-1, 1); - var r = r.union( - cags.map(function(cag) { - return cag._toCSGWall(-1, 1).reTesselated(); - }), false, false) - return CAG.fromFakeCSG(r).canonicalized(); - }, - - subtract: function(cag) { - var cags; - if (cag instanceof Array) { - cags = cag; - } else { - cags = [cag]; - } - var r = this._toCSGWall(-1, 1); - cags.map(function(cag) { - r = r.subtractSub(cag._toCSGWall(-1, 1), false, false); - }); - r = r.reTesselated(); - r = r.canonicalized(); - r = CAG.fromFakeCSG(r); - r = r.canonicalized(); - return r; - }, - - intersect: function(cag) { - var cags; - if (cag instanceof Array) { - cags = cag; - } else { - cags = [cag]; - } - var r = this._toCSGWall(-1, 1); - cags.map(function(cag) { - r = r.intersectSub(cag._toCSGWall(-1, 1), false, false); - }); - r = r.reTesselated(); - r = r.canonicalized(); - r = CAG.fromFakeCSG(r); - r = r.canonicalized(); - return r; - }, - - transform: function(matrix4x4) { - var ismirror = matrix4x4.isMirroring(); - var newsides = this.sides.map(function(side) { - return side.transform(matrix4x4); - }); - var result = CAG.fromSides(newsides); - if (ismirror) { - result = result.flipped(); - } - return result; - }, - - // see http://local.wasp.uwa.edu.au/~pbourke/geometry/polyarea/ : - // Area of the polygon. For a counter clockwise rotating polygon the area is positive, otherwise negative - // Note(bebbi): this looks wrong. See polygon getArea() - area: function() { - var polygonArea = 0; - this.sides.map(function(side) { - polygonArea += side.vertex0.pos.cross(side.vertex1.pos); - }); - polygonArea *= 0.5; - return polygonArea; - }, - - flipped: function() { - var newsides = this.sides.map(function(side) { - return side.flipped(); - }); - newsides.reverse(); - return CAG.fromSides(newsides); - }, - - getBounds: function() { - var minpoint; - if (this.sides.length === 0) { - minpoint = new CSG.Vector2D(0, 0); - } else { - minpoint = this.sides[0].vertex0.pos; - } - var maxpoint = minpoint; - this.sides.map(function(side) { - minpoint = minpoint.min(side.vertex0.pos); - minpoint = minpoint.min(side.vertex1.pos); - maxpoint = maxpoint.max(side.vertex0.pos); - maxpoint = maxpoint.max(side.vertex1.pos); - }); - return [minpoint, maxpoint]; - }, - - isSelfIntersecting: function(debug) { - var numsides = this.sides.length; - for (var i = 0; i < numsides; i++) { - var side0 = this.sides[i]; - for (var ii = i + 1; ii < numsides; ii++) { - var side1 = this.sides[ii]; - if (CAG.linesIntersect(side0.vertex0.pos, side0.vertex1.pos, side1.vertex0.pos, side1.vertex1.pos)) { - if (debug) { OpenJsCad.log(side0); OpenJsCad.log(side1);} - return true; - } - } - } - return false; - }, - - expandedShell: function(radius, resolution) { - resolution = resolution || 8; - if (resolution < 4) resolution = 4; - var cags = []; - var pointmap = {}; - var cag = this.canonicalized(); - cag.sides.map(function(side) { - var d = side.vertex1.pos.minus(side.vertex0.pos); - var dl = d.length(); - if (dl > 1e-5) { - d = d.times(1.0 / dl); - var normal = d.normal().times(radius); - var shellpoints = [ - side.vertex1.pos.plus(normal), - side.vertex1.pos.minus(normal), - side.vertex0.pos.minus(normal), - side.vertex0.pos.plus(normal) - ]; - // var newcag = CAG.fromPointsNoCheck(shellpoints); - var newcag = CAG.fromPoints(shellpoints); - cags.push(newcag); - for (var step = 0; step < 2; step++) { - var p1 = (step === 0) ? side.vertex0.pos : side.vertex1.pos; - var p2 = (step === 0) ? side.vertex1.pos : side.vertex0.pos; - var tag = p1.x + " " + p1.y; - if (!(tag in pointmap)) { - pointmap[tag] = []; - } - pointmap[tag].push({ - "p1": p1, - "p2": p2 - }); - } - } - }); - for (var tag in pointmap) { - var m = pointmap[tag]; - var angle1, angle2; - var pcenter = m[0].p1; - if (m.length == 2) { - var end1 = m[0].p2; - var end2 = m[1].p2; - angle1 = end1.minus(pcenter).angleDegrees(); - angle2 = end2.minus(pcenter).angleDegrees(); - if (angle2 < angle1) angle2 += 360; - if (angle2 >= (angle1 + 360)) angle2 -= 360; - if (angle2 < angle1 + 180) { - var t = angle2; - angle2 = angle1 + 360; - angle1 = t; - } - angle1 += 90; - angle2 -= 90; - } else { - angle1 = 0; - angle2 = 360; - } - var fullcircle = (angle2 > angle1 + 359.999); - if (fullcircle) { - angle1 = 0; - angle2 = 360; - } - if (angle2 > (angle1 + 1e-5)) { - var points = []; - if (!fullcircle) { - points.push(pcenter); - } - var numsteps = Math.round(resolution * (angle2 - angle1) / 360); - if (numsteps < 1) numsteps = 1; - for (var step = 0; step <= numsteps; step++) { - var angle = angle1 + step / numsteps * (angle2 - angle1); - if (step == numsteps) angle = angle2; // prevent rounding errors - var point = pcenter.plus(CSG.Vector2D.fromAngleDegrees(angle).times(radius)); - if ((!fullcircle) || (step > 0)) { - points.push(point); - } - } - var newcag = CAG.fromPointsNoCheck(points); - cags.push(newcag); - } - } - var result = new CAG(); - result = result.union(cags); - return result; - }, - - expand: function(radius, resolution) { - var result = this.union(this.expandedShell(radius, resolution)); - return result; - }, - - contract: function(radius, resolution) { - var result = this.subtract(this.expandedShell(radius, resolution)); - return result; - }, - - // extrude the CAG in a certain plane. - // Giving just a plane is not enough, multiple different extrusions in the same plane would be possible - // by rotating around the plane's origin. An additional right-hand vector should be specified as well, - // and this is exactly a CSG.OrthoNormalBasis. - // orthonormalbasis: characterizes the plane in which to extrude - // depth: thickness of the extruded shape. Extrusion is done symmetrically above and below the plane. - extrudeInOrthonormalBasis: function(orthonormalbasis, depth) { - // first extrude in the regular Z plane: - if (!(orthonormalbasis instanceof CSG.OrthoNormalBasis)) { - throw new Error("extrudeInPlane: the first parameter should be a CSG.OrthoNormalBasis"); - } - var extruded = this.extrude({ - offset: [0, 0, depth] - }); - var matrix = orthonormalbasis.getInverseProjectionMatrix(); - extruded = extruded.transform(matrix); - return extruded; - }, - - // Extrude in a standard cartesian plane, specified by two axis identifiers. Each identifier can be - // one of ["X","Y","Z","-X","-Y","-Z"] - // The 2d x axis will map to the first given 3D axis, the 2d y axis will map to the second. - // See CSG.OrthoNormalBasis.GetCartesian for details. - extrudeInPlane: function(axis1, axis2, depth) { - return this.extrudeInOrthonormalBasis(CSG.OrthoNormalBasis.GetCartesian(axis1, axis2), depth); - }, - - // extruded=cag.extrude({offset: [0,0,10], twistangle: 360, twiststeps: 100}); - // linear extrusion of 2D shape, with optional twist - // The 2d shape is placed in in z=0 plane and extruded into direction (a CSG.Vector3D) - // The final face is rotated degrees. Rotation is done around the origin of the 2d shape (i.e. x=0, y=0) - // twiststeps determines the resolution of the twist (should be >= 1) - // returns a CSG object - extrude: function(options) { - if (this.sides.length == 0) { - // empty! - return new CSG(); - } - var offsetVector = CSG.parseOptionAs3DVector(options, "offset", [0, 0, 1]); - var twistangle = CSG.parseOptionAsFloat(options, "twistangle", 0); - var twiststeps = CSG.parseOptionAsInt(options, "twiststeps", CSG.defaultResolution3D); - if (offsetVector.z == 0) { - throw('offset cannot be orthogonal to Z axis'); - } - if (twistangle == 0 || twiststeps < 1) { - twiststeps = 1; - } - var normalVector = CSG.Vector3D.Create(0, 1, 0); - - var polygons = []; - // bottom and top - polygons = polygons.concat(this._toPlanePolygons({translation: [0, 0, 0], - normalVector: normalVector, flipped: !(offsetVector.z < 0)})); - polygons = polygons.concat(this._toPlanePolygons({translation: offsetVector, - normalVector: normalVector.rotateZ(twistangle), flipped: offsetVector.z < 0})); - // walls - for (var i = 0; i < twiststeps; i++) { - var c1 = new CSG.Connector(offsetVector.times(i / twiststeps), [0, 0, offsetVector.z], - normalVector.rotateZ(i * twistangle/twiststeps)); - var c2 = new CSG.Connector(offsetVector.times((i + 1) / twiststeps), [0, 0, offsetVector.z], - normalVector.rotateZ((i + 1) * twistangle/twiststeps)); - polygons = polygons.concat(this._toWallPolygons({toConnector1: c1, toConnector2: c2})); - } - - return CSG.fromPolygons(polygons); - }, - - /* - * extrude CAG to 3d object by rotating the origin around the y axis - * (and turning everything into XY plane) - * arguments: options dict with angle and resolution, both optional - */ - rotateExtrude: function(options) { - var alpha = CSG.parseOptionAsFloat(options, "angle", 360); - var resolution = CSG.parseOptionAsInt(options, "resolution", CSG.defaultResolution3D); - - var EPS = 1e-5; - - alpha = alpha > 360 ? alpha % 360 : alpha; - var origin = [0, 0, 0]; - var axisV = CSG.Vector3D.Create(0, 1, 0); - var normalV = [0, 0, 1]; - var polygons = []; - // planes only needed if alpha > 0 - var connS = new CSG.Connector(origin, axisV, normalV); - if (alpha > 0 && alpha < 360) { - // we need to rotate negative to satisfy wall function condition of - // building in the direction of axis vector - var connE = new CSG.Connector(origin, axisV.rotateZ(-alpha), normalV); - polygons = polygons.concat( - this._toPlanePolygons({toConnector: connS, flipped: true})); - polygons = polygons.concat( - this._toPlanePolygons({toConnector: connE})); - } - var connT1 = connS, connT2; - var step = alpha/resolution; - for (var a = step; a <= alpha + EPS; a += step) { - connT2 = new CSG.Connector(origin, axisV.rotateZ(-a), normalV); - polygons = polygons.concat(this._toWallPolygons( - {toConnector1: connT1, toConnector2: connT2})); - connT1 = connT2; - } - return CSG.fromPolygons(polygons).reTesselated(); - }, - - // check if we are a valid CAG (for debugging) - // NOTE(bebbi) uneven side count doesn't work because rounding with EPS isn't taken into account - check: function() { - var EPS = 1e-5; - var errors = []; - if (this.isSelfIntersecting(true)) { - errors.push("Self intersects"); - } - var pointcount = {}; - this.sides.map(function(side) { - function mappoint(p) { - var tag = p.x + " " + p.y; - if (!(tag in pointcount)) pointcount[tag] = 0; - pointcount[tag] ++; - } - mappoint(side.vertex0.pos); - mappoint(side.vertex1.pos); - }); - for (var tag in pointcount) { - var count = pointcount[tag]; - if (count & 1) { - errors.push("Uneven number of sides (" + count + ") for point " + tag); - } - } - var area = this.area(); - if (area < EPS*EPS) { - errors.push("Area is " + area); - } - if (errors.length > 0) { - var ertxt = ""; - errors.map(function(err) { - ertxt += err + "\n"; - }); - throw new Error(ertxt); - } - }, - - canonicalized: function() { - if (this.isCanonicalized) { - return this; - } else { - var factory = new CAG.fuzzyCAGFactory(); - var result = factory.getCAG(this); - result.isCanonicalized = true; - return result; - } - }, - - toCompactBinary: function() { - var cag = this.canonicalized(); - var numsides = cag.sides.length; - var vertexmap = {}; - var vertices = []; - var numvertices = 0; - var sideVertexIndices = new Uint32Array(2 * numsides); - var sidevertexindicesindex = 0; - cag.sides.map(function(side) { - [side.vertex0, side.vertex1].map(function(v) { - var vertextag = v.getTag(); - var vertexindex; - if (!(vertextag in vertexmap)) { - vertexindex = numvertices++; - vertexmap[vertextag] = vertexindex; - vertices.push(v); - } else { - vertexindex = vertexmap[vertextag]; - } - sideVertexIndices[sidevertexindicesindex++] = vertexindex; - }); - }); - var vertexData = new Float64Array(numvertices * 2); - var verticesArrayIndex = 0; - vertices.map(function(v) { - var pos = v.pos; - vertexData[verticesArrayIndex++] = pos._x; - vertexData[verticesArrayIndex++] = pos._y; - }); - var result = { - 'class': "CAG", - sideVertexIndices: sideVertexIndices, - vertexData: vertexData - }; - return result; - }, - - getOutlinePaths: function() { - var cag = this.canonicalized(); - var sideTagToSideMap = {}; - var startVertexTagToSideTagMap = {}; - cag.sides.map(function(side) { - var sidetag = side.getTag(); - sideTagToSideMap[sidetag] = side; - var startvertextag = side.vertex0.getTag(); - if (!(startvertextag in startVertexTagToSideTagMap)) { - startVertexTagToSideTagMap[startvertextag] = []; - } - startVertexTagToSideTagMap[startvertextag].push(sidetag); - }); - var paths = []; - while (true) { - var startsidetag = null; - for (var aVertexTag in startVertexTagToSideTagMap) { - var sidesForThisVertex = startVertexTagToSideTagMap[aVertexTag]; - startsidetag = sidesForThisVertex[0]; - sidesForThisVertex.splice(0, 1); - if (sidesForThisVertex.length === 0) { - delete startVertexTagToSideTagMap[aVertexTag]; - } - break; - } - if (startsidetag === null) break; // we've had all sides - var connectedVertexPoints = []; - var sidetag = startsidetag; - var thisside = sideTagToSideMap[sidetag]; - var startvertextag = thisside.vertex0.getTag(); - while (true) { - connectedVertexPoints.push(thisside.vertex0.pos); - var nextvertextag = thisside.vertex1.getTag(); - if (nextvertextag == startvertextag) break; // we've closed the polygon - if (!(nextvertextag in startVertexTagToSideTagMap)) { - throw new Error("Area is not closed!"); - } - var nextpossiblesidetags = startVertexTagToSideTagMap[nextvertextag]; - var nextsideindex = -1; - if (nextpossiblesidetags.length == 1) { - nextsideindex = 0; - } else { - // more than one side starting at the same vertex. This means we have - // two shapes touching at the same corner - var bestangle = null; - var thisangle = thisside.direction().angleDegrees(); - for (var sideindex = 0; sideindex < nextpossiblesidetags.length; sideindex++) { - var nextpossiblesidetag = nextpossiblesidetags[sideindex]; - var possibleside = sideTagToSideMap[nextpossiblesidetag]; - var angle = possibleside.direction().angleDegrees(); - var angledif = angle - thisangle; - if (angledif < -180) angledif += 360; - if (angledif >= 180) angledif -= 360; - if ((nextsideindex < 0) || (angledif > bestangle)) { - nextsideindex = sideindex; - bestangle = angledif; - } - } - } - var nextsidetag = nextpossiblesidetags[nextsideindex]; - nextpossiblesidetags.splice(nextsideindex, 1); - if (nextpossiblesidetags.length === 0) { - delete startVertexTagToSideTagMap[nextvertextag]; - } - thisside = sideTagToSideMap[nextsidetag]; - } // inner loop - var path = new CSG.Path2D(connectedVertexPoints, true); - paths.push(path); - } // outer loop - return paths; - }, - - /* - cag = cag.overCutInsideCorners(cutterradius); - - Using a CNC router it's impossible to cut out a true sharp inside corner. The inside corner - will be rounded due to the radius of the cutter. This function compensates for this by creating - an extra cutout at each inner corner so that the actual cut out shape will be at least as large - as needed. - */ - overCutInsideCorners: function(cutterradius) { - var cag = this.canonicalized(); - // for each vertex determine the 'incoming' side and 'outgoing' side: - var pointmap = {}; // tag => {pos: coord, from: [], to: []} - cag.sides.map(function(side) { - if (!(side.vertex0.getTag() in pointmap)) { - pointmap[side.vertex0.getTag()] = { - pos: side.vertex0.pos, - from: [], - to: [] - }; - } - pointmap[side.vertex0.getTag()].to.push(side.vertex1.pos); - if (!(side.vertex1.getTag() in pointmap)) { - pointmap[side.vertex1.getTag()] = { - pos: side.vertex1.pos, - from: [], - to: [] - }; - } - pointmap[side.vertex1.getTag()].from.push(side.vertex0.pos); - }); - // overcut all sharp corners: - var cutouts = []; - for (var pointtag in pointmap) { - var pointobj = pointmap[pointtag]; - if ((pointobj.from.length == 1) && (pointobj.to.length == 1)) { - // ok, 1 incoming side and 1 outgoing side: - var fromcoord = pointobj.from[0]; - var pointcoord = pointobj.pos; - var tocoord = pointobj.to[0]; - var v1 = pointcoord.minus(fromcoord).unit(); - var v2 = tocoord.minus(pointcoord).unit(); - var crossproduct = v1.cross(v2); - var isInnerCorner = (crossproduct < 0.001); - if (isInnerCorner) { - // yes it's a sharp corner: - var alpha = v2.angleRadians() - v1.angleRadians() + Math.PI; - if (alpha < 0) { - alpha += 2 * Math.PI; - } else if (alpha >= 2 * Math.PI) { - alpha -= 2 * Math.PI; - } - var midvector = v2.minus(v1).unit(); - var circlesegmentangle = 30 / 180 * Math.PI; // resolution of the circle: segments of 30 degrees - // we need to increase the radius slightly so that our imperfect circle will contain a perfect circle of cutterradius - var radiuscorrected = cutterradius / Math.cos(circlesegmentangle / 2); - var circlecenter = pointcoord.plus(midvector.times(radiuscorrected)); - // we don't need to create a full circle; a pie is enough. Find the angles for the pie: - var startangle = alpha + midvector.angleRadians(); - var deltaangle = 2 * (Math.PI - alpha); - var numsteps = 2 * Math.ceil(deltaangle / circlesegmentangle / 2); // should be even - // build the pie: - var points = [circlecenter]; - for (var i = 0; i <= numsteps; i++) { - var angle = startangle + i / numsteps * deltaangle; - var p = CSG.Vector2D.fromAngleRadians(angle).times(radiuscorrected).plus(circlecenter); - points.push(p); - } - cutouts.push(CAG.fromPoints(points)); - } - } - } - var result = cag.subtract(cutouts); - return result; - } - }; - - CAG.Vertex = function(pos) { - this.pos = pos; - }; - - CAG.Vertex.prototype = { - toString: function() { - return "(" + this.pos.x.toFixed(2) + "," + this.pos.y.toFixed(2) + ")"; - }, - getTag: function() { - var result = this.tag; - if (!result) { - result = CSG.getTag(); - this.tag = result; - } - return result; - } - }; - - CAG.Side = function(vertex0, vertex1) { - if (!(vertex0 instanceof CAG.Vertex)) throw new Error("Assertion failed"); - if (!(vertex1 instanceof CAG.Vertex)) throw new Error("Assertion failed"); - this.vertex0 = vertex0; - this.vertex1 = vertex1; - }; - - CAG.Side._fromFakePolygon = function(polygon) { - polygon.vertices.forEach(function(v) { - if (!((v.pos.z >= -1.001) && (v.pos.z < -0.999)) && !((v.pos.z >= 0.999) && (v.pos.z < 1.001))) { - throw("Assertion failed: _fromFakePolygon expects abs z values of 1"); - } - }) - // this can happen based on union, seems to be residuals - - // return null and handle in caller - if (polygon.vertices.length < 4) { - return null; - } - var reverse = false; - var vert1Indices = []; - var pts2d = polygon.vertices.filter(function(v, i) { - if (v.pos.z > 0) { - vert1Indices.push(i); - return true; - } - }) - .map(function(v) { - return new CSG.Vector2D(v.pos.x, v.pos.y); - }); - if (pts2d.length != 2) { - throw('Assertion failed: _fromFakePolygon: not enough points found') - } - var d = vert1Indices[1] - vert1Indices[0]; - if (d == 1 || d == 3) { - if (d == 1) { - pts2d.reverse(); - } - } else { - throw('Assertion failed: _fromFakePolygon: unknown index ordering'); - } - var result = new CAG.Side(new CAG.Vertex(pts2d[0]), new CAG.Vertex(pts2d[1])); - return result; - }; - - CAG.Side.prototype = { - toString: function() { - return this.vertex0 + " -> " + this.vertex1; - }, - - toPolygon3D: function(z0, z1) { - var vertices = [ - new CSG.Vertex(this.vertex0.pos.toVector3D(z0)), - new CSG.Vertex(this.vertex1.pos.toVector3D(z0)), - new CSG.Vertex(this.vertex1.pos.toVector3D(z1)), - new CSG.Vertex(this.vertex0.pos.toVector3D(z1)) - ]; - return new CSG.Polygon(vertices); - }, - - transform: function(matrix4x4) { - var newp1 = this.vertex0.pos.transform(matrix4x4); - var newp2 = this.vertex1.pos.transform(matrix4x4); - return new CAG.Side(new CAG.Vertex(newp1), new CAG.Vertex(newp2)); - }, - - flipped: function() { - return new CAG.Side(this.vertex1, this.vertex0); - }, - - direction: function() { - return this.vertex1.pos.minus(this.vertex0.pos); - }, - - getTag: function() { - var result = this.tag; - if (!result) { - result = CSG.getTag(); - this.tag = result; - } - return result; - }, - - lengthSquared: function() { - var x = this.vertex1.pos.x - this.vertex0.pos.x, - y = this.vertex1.pos.y - this.vertex0.pos.y; - return x * x + y * y; - }, - - length: function() { - return Math.sqrt(this.lengthSquared()); - } - }; - - ////////////////////////////////////// - CAG.fuzzyCAGFactory = function() { - this.vertexfactory = new CSG.fuzzyFactory(2, 1e-5); - }; - - CAG.fuzzyCAGFactory.prototype = { - getVertex: function(sourcevertex) { - var elements = [sourcevertex.pos._x, sourcevertex.pos._y]; - var result = this.vertexfactory.lookupOrCreate(elements, function(els) { - return sourcevertex; - }); - return result; - }, - - getSide: function(sourceside) { - var vertex0 = this.getVertex(sourceside.vertex0); - var vertex1 = this.getVertex(sourceside.vertex1); - return new CAG.Side(vertex0, vertex1); - }, - - getCAG: function(sourcecag) { - var _this = this; - var newsides = sourcecag.sides.map(function(side) { - return _this.getSide(side); - }) - // remove bad sides (mostly a user input issue) - .filter(function(side) { - return side.length() > 1e-5; - }); - return CAG.fromSides(newsides); - } - }; - - ////////////////////////////////////// - CSG.addTransformationMethodsToPrototype(CSG.prototype); - CSG.addTransformationMethodsToPrototype(CSG.Vector2D.prototype); - CSG.addTransformationMethodsToPrototype(CSG.Vector3D.prototype); - CSG.addTransformationMethodsToPrototype(CSG.Vertex.prototype); - CSG.addTransformationMethodsToPrototype(CSG.Plane.prototype); - CSG.addTransformationMethodsToPrototype(CSG.Polygon.prototype); - CSG.addTransformationMethodsToPrototype(CSG.Line3D.prototype); - CSG.addTransformationMethodsToPrototype(CSG.Connector.prototype); - CSG.addTransformationMethodsToPrototype(CSG.Path2D.prototype); - CSG.addTransformationMethodsToPrototype(CSG.Line2D.prototype); - CSG.addTransformationMethodsToPrototype(CAG.prototype); - CSG.addTransformationMethodsToPrototype(CAG.Side.prototype); - CSG.addTransformationMethodsToPrototype(CSG.OrthoNormalBasis.prototype); - - CSG.addCenteringToPrototype(CSG.prototype, ['x', 'y', 'z']); - CSG.addCenteringToPrototype(CAG.prototype, ['x', 'y']); - - /* - 2D polygons are now supported through the CAG class. - With many improvements (see documentation): - - shapes do no longer have to be convex - - union/intersect/subtract is supported - - expand / contract are supported - - But we'll keep CSG.Polygon2D as a stub for backwards compatibility - */ - CSG.Polygon2D = function(points) { - var cag = CAG.fromPoints(points); - this.sides = cag.sides; - }; - CSG.Polygon2D.prototype = CAG.prototype; - - - - module.CSG = CSG; - module.CAG = CAG; -})(this); //module to export to \ No newline at end of file