From 4516cd57e77c8819620424c4ccb916cb12877cfc Mon Sep 17 00:00:00 2001 From: Joost Nieuwenhuijse Date: Fri, 20 Jan 2012 15:52:48 +0100 Subject: [PATCH] First release! --- csg.js | 2957 ++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 290 +++++- lightgl.js | 61 ++ viewer.js | 192 ++++ 4 files changed, 3499 insertions(+), 1 deletion(-) create mode 100644 csg.js create mode 100644 lightgl.js create mode 100644 viewer.js diff --git a/csg.js b/csg.js new file mode 100644 index 0000000..83f0edf --- /dev/null +++ b/csg.js @@ -0,0 +1,2957 @@ +var _CSGDEBUG=false; + +/* +## License + +Copyright (c) 2012 Joost Nieuwenhuijse (joost@newhouse.nl) under MIT license + +Based on original CSG.js: http://evanw.github.com/csg.js/ +Copyright (c) 2011 Evan Wallace 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() + +Extrusion of 2D polygons (CSG.Polygon2D.extrude()) + +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. + +*/ + +// # class CSG + +// Holds a binary space partition tree representing a 3D solid. Two solids can +// be combined using the `union()`, `subtract()`, and `intersect()` methods. + +CSG = function() { + this.polygons = []; +}; + +// Construct a CSG solid from a list of `CSG.Polygon` instances. +CSG.fromPolygons = function(polygons) { + var csg = new CSG(); + csg.polygons = polygons; + 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) { + return this.unionSub(csg, true, true); + }, + + unionSub: function(csg, retesselate, canonicalize) { + var a = new CSG.Tree(this.polygons); + var b = new CSG.Tree(csg.polygons); + a.clipTo(b, false); + b.clipTo(a, true); + var newpolygons = a.allPolygons().concat(b.allPolygons()); + var csg = CSG.fromPolygons(newpolygons); + if(canonicalize) csg = csg.canonicalized(); + if(retesselate) csg = csg.reTesselated(); + return csg; + }, + + // 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) { + return this.subtractSub(csg, true, true); + }, + + 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 csg = CSG.fromPolygons(a.allPolygons()); + if(canonicalize) csg = csg.canonicalized(); + if(retesselate) csg = csg.reTesselated(); + return csg; + }, + + // 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) { + return this.intersectSub(csg, true, true); + }, + + 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 csg = CSG.fromPolygons(a.allPolygons()); + if(canonicalize) csg = csg.canonicalized(); + if(retesselate) csg = csg.reTesselated(); + return csg; + }, + + // Return a new CSG solid with solid and empty space switched. This solid is + // not modified. + inverse: function() { + var flippedpolygons = this.polygons.map(function(p) { p.flipped(); }); + return CSG.fromPolygons(flippedpolygons); + }, + + // Affine transformation of CSG object. Returns a new CSG object + transform: function(matrix4x4) { + var newpolygons = this.polygons.map(function(p) { return p.transform(matrix4x4); } ); + return CSG.fromPolygons(newpolygons); + }, + + translate: function(v) { + return this.transform(CSG.Matrix4x4.translation(v)); + }, + + scale: function(f) { + return this.transform(CSG.Matrix4x4.scaling(f)); + }, + + rotateX: function(deg) { + return this.transform(CSG.Matrix4x4.rotationX(deg)); + }, + + rotateY: function(deg) { + return this.transform(CSG.Matrix4x4.rotationY(deg)); + }, + + rotateZ: function(deg) { + return this.transform(CSG.Matrix4x4.rotationZ(deg)); + }, + + toStlString: function() { + var result="solid csg.js\n"; + this.polygons.map(function(p){ result += p.toStlString(); }); + result += "endsolid csg.js\n"; + return result; + }, + + toString: function() { + var result = ""; + 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; + this.polygons.map(function(p) { + var expanded=p.expand(radius, resolution); + result=result.unionSub(expanded, true, false); + }); + result = result.canonicalized(); + return result; + }, + + // Contract the solid + // resolution: number of points per 360 degree for the rounded corners + contract: function(radius, resolution) { + var result=this; + this.polygons.map(function(p) { + var expanded=p.expand(radius, resolution); + result=result.subtract(expanded); + }); + return result; + }, + + canonicalized: function () { + if(this.isCanonicalized) + { + return this; + } + else + { + var factory = new CSG.fuzzyCSGFactory(); + var result = factory.getCSG(this); + result.isCanonicalized = true; + return result; + } + }, + + reTesselated: function () { + if(this.isRetesselated) + { + return this; + } + else + { + var csg=this; //.canonicalized(); + var polygonsPerPlane = {}; + csg.polygons.map(function(polygon) { + var planetag = polygon.plane.getTag(); + if(! (planetag in polygonsPerPlane) ) + { + polygonsPerPlane[planetag] = []; + } + polygonsPerPlane[planetag].push(polygon); + }); + var destpolygons = []; + for(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; + return 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.parseOptionAsFloat = function(options, optionname, defaultvalue) { + var result = CSG.parseOption(options, optionname, defaultvalue); + if(typeof(result) == "string") + { + result = Number(result); + } + else if(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); + return Number(Math.floor(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 = CSG.parseOptionAs3DVector(options, "center", [0,0,0]); + var r = CSG.parseOptionAs3DVector(options, "radius", [1,1,1]); + return 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 */); + })); +}; + +// 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) +// +// 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", 12); + if(resolution < 4) resolution = 4; + var qresolution = Math.round(resolution / 4); + var xvector = new CSG.Vector3D([1,0,0]).times(radius); + var yvector = new CSG.Vector3D([0,-1,0]).times(radius); + var zvector = new CSG.Vector3D([0,0,1]).times(radius); + 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; + } + return CSG.fromPolygons(polygons); +}; + +// 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 slices = CSG.parseOptionAsFloat(options, "resolution", 12); + var ray = e.minus(s); + var axisZ = ray.unit(), isY = (Math.abs(axisZ.y) > 0.5); + 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, normalBlend) { + var angle = slice * Math.PI * 2; + var out = axisX.times(Math.cos(angle)).plus(axisY.times(Math.sin(angle))); + var pos = s.plus(ray.times(stack)).plus(out.times(r)); + var normal = out.times(1 - Math.abs(normalBlend)).plus(axisZ.times(normalBlend)); + return new CSG.Vertex(pos); + } + for (var i = 0; i < slices; i++) { + var t0 = i / slices, t1 = (i + 1) / slices; + polygons.push(new CSG.Polygon([start, point(0, t0, -1), point(0, t1, -1)])); + polygons.push(new CSG.Polygon([point(0, t1, 0), point(0, t0, 0), point(1, t0, 0), point(1, t1, 0)])); + polygons.push(new CSG.Polygon([end, point(1, t1, 1), point(1, t0, 1)])); + } + return CSG.fromPolygons(polygons); +}; + +// 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.parseOptionAsFloat(options, "resolution", 12); + 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 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; + } + return CSG.fromPolygons(polygons); +}; + +// 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 center = CSG.parseOptionAs3DVector(options, "center", [0,0,0]); + var cuberadius = CSG.parseOptionAs3DVector(options, "radius", [1,1,1]); + var resolution = CSG.parseOptionAsFloat(options, "resolution", 8); + if(resolution < 4) resolution = 4; + var roundradius = CSG.parseOptionAsFloat(options, "roundradius", 0.2); + var innercuberadius=cuberadius.clone(); + innercuberadius.x -= roundradius; + innercuberadius.y -= roundradius; + innercuberadius.z -= roundradius; + var result = CSG.cube({center: center, radius: [cuberadius.x, innercuberadius.y, innercuberadius.z]}); + result = result.unionSub( CSG.cube({center: center, radius: [innercuberadius.x, cuberadius.y, innercuberadius.z]}),false,false); + result = result.unionSub( CSG.cube({center: center, radius: [innercuberadius.x, innercuberadius.y, cuberadius.z]}),false,false); + for(var level=0; level < 2; level++) + { + var z = innercuberadius.z; + if(level == 1) z = -z; + var p1 = new CSG.Vector3D(innercuberadius.x, innercuberadius.y, z).plus(center); + var p2 = new CSG.Vector3D(innercuberadius.x, -innercuberadius.y, z).plus(center); + var p3 = new CSG.Vector3D(-innercuberadius.x, -innercuberadius.y, z).plus(center); + var p4 = new CSG.Vector3D(-innercuberadius.x, innercuberadius.y, z).plus(center); + var sphere = CSG.sphere({center: p1, radius: roundradius, resolution: resolution}); + result = result.unionSub(sphere,false,false); + sphere = CSG.sphere({center: p2, radius: roundradius, resolution: resolution}); + result = result.unionSub(sphere,false,false); + sphere = CSG.sphere({center: p3, radius: roundradius, resolution: resolution}); + result = result.unionSub(sphere,false,false); + sphere = CSG.sphere({center: p4, radius: roundradius, resolution: resolution}); + result = result.unionSub(sphere,true,true); + var cylinder = CSG.cylinder({start:p1, end: p2, radius: roundradius, resolution: resolution}); + result = result.unionSub(cylinder,false,false); + cylinder = CSG.cylinder({start:p2, end: p3, radius: roundradius, resolution: resolution}); + result = result.unionSub(cylinder,false,false); + cylinder = CSG.cylinder({start:p3, end: p4, radius: roundradius, resolution: resolution}); + result = result.unionSub(cylinder,false,false); + cylinder = CSG.cylinder({start:p4, end: p1, radius: roundradius, resolution: resolution}); + result = result.unionSub(cylinder,false,false); + if(level == 0) { + var d = new CSG.Vector3D(0, 0, -2*z); + cylinder = CSG.cylinder({start:p1, end: p1.plus(d), radius: roundradius, resolution: resolution}); + result = result.unionSub(cylinder); + cylinder = CSG.cylinder({start:p2, end: p2.plus(d), radius: roundradius, resolution: resolution}); + result = result.unionSub(cylinder); + cylinder = CSG.cylinder({start:p3, end: p3.plus(d), radius: roundradius, resolution: resolution}); + result = result.unionSub(cylinder); + cylinder = CSG.cylinder({start:p4, end: p4.plus(d), radius: roundradius, resolution: resolution}); + result = result.unionSub(cylinder,true,true); + } + } + return result; +} + + + +// # 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 }); + +CSG.Vector3D = function(x, y, z) { + var ok = true; + if (arguments.length == 1) + { + if(typeof(x) == "object") + { + if(x instanceof Array) + { + this.x = x[0]; + this.y = x[1]; + this.z = x[2]; + } + else if( ('x' in x) && ('y' in x) && ('z' in x) ) + { + this.x = x.x; + this.y = x.y; + this.z = x.z; + } + else ok = false; + } + else + { + var v = Number(x); + this.x = v; + this.y = v; + this.z = v; + } + } + else if (arguments.length == 3) + { + this.x = Number(x); + this.y = Number(y); + this.z = Number(z); + } + else ok = false; + if(!ok) + { + throw new Error("wrong arguments"); + } +}; + +CSG.Vector3D.prototype = { + clone: function() { + return new CSG.Vector3D(this.x, this.y, this.z); + }, + + negated: function() { + return new CSG.Vector3D(-this.x, -this.y, -this.z); + }, + + plus: function(a) { + return new CSG.Vector3D(this.x + a.x, this.y + a.y, this.z + a.z); + }, + + minus: function(a) { + return new CSG.Vector3D(this.x - a.x, this.y - a.y, this.z - a.z); + }, + + times: function(a) { + return new CSG.Vector3D(this.x * a, this.y * a, this.z * a); + }, + + dividedBy: function(a) { + return new CSG.Vector3D(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 new CSG.Vector3D( + 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.rightMultiply1x3Vector(this); + }, + + toStlString: function() { + return this.x+" "+this.y+" "+this.z; + }, + + toString: function() { + return "("+this.x+", "+this.y+", "+this.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; +}; + +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); + }, + + toStlString: function() { + return "vertex "+this.pos.toStlString()+"\n"; + }, + + 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; +}; + +// `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.fromPoints = function(a, b, c) { + var n = b.minus(a).cross(c.minus(a)).unit(); + return new CSG.Plane(n, n.dot(a)); +}; + +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 origin = new CSG.Vector3D(0,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.Plane(newnormal, neww); + }, + + // 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; + // first check if the polygon's bounding sphere is completely in front or in back: + var bound = polygon.boundingSphere(); + var spherecenter = bound[0]; + var sphereradius = bound[1]; + sphereradius += EPS; + //var d = this.signedDistanceToPoint(spherecenter); + var d = planenormal.dot(spherecenter) - thisw; + + if(d > sphereradius) + { + result.type = 2; + } + else if(d < -sphereradius) + { + result.type = 3; + } + else + { + // no, we really have to check each vertex separately: + 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 line = CSG.Line3D.fromPoints(point, nextpoint); + var intersectionpoint = this.intersectWithLine(line); + 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; + }, + + // returns CSG.Point3D + 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+"]"; + }, +}; + + +// # 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; + this.shared = shared; + var numvertices = vertices.length; + + if(arguments.length >= 3) + { + this.plane = plane; + } + else + { + this.plane = CSG.Plane.fromPoints(vertices[0].pos, vertices[1].pos, vertices[2].pos); + } + + if(_CSGDEBUG) + { + this.checkIfConvex(); + } +}; + +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)) + { + throw new Error("Not convex!"); + } + }, + + // 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); + newpolygons.push(sidefacepolygon); + } + polygon2 = polygon2.flipped(); + newpolygons.push(polygon2); + return CSG.fromPolygons(newpolygons); + }, + + translate: function(offset) { + return this.transform(CSG.Matrix4x4.translation(offset)); + }, + + // Expand the polygon with a certain radius + // This extrudes the face of the polygon and adds rounded corners + // Returns a CSG object (not a polygon anymore!) + // resolution: number of points per 360 degree for the rounded corners + expand: function(radius, resolution) { + if( (!resolution) || (resolution < 4) ) resolution = 4; + resolution = 4 * Math.floor(resolution / 4); + + var result=new CSG(); + + // expand each side of the polygon. The expansion of a line is a roundedCylinder: + var numvertices=this.vertices.length; + for(var i=0; i < numvertices; i++) + { + var previ = (i == 0) ? (numvertices-1):i-1; + var p1 = this.vertices[previ].pos; + var p2 = this.vertices[i].pos; + + var roundedCylinder = CSG.roundedCylinder({start: p1, end: p2, normal: this.plane.normal, radius: radius, resolution: resolution}); + result = result.unionSub(roundedCylinder, false, false); + } + var extrudevector=this.plane.normal.unit().times(2*radius); + var translatedpolygon = this.translate(extrudevector.times(-0.5)); + var extrudedface = translatedpolygon.extrude(extrudevector); + result=result.unionSub(extrudedface, true, false); + return result; + }, + + // 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); + maxpoint=new CSG.Vector3D(0,0,0); + } + else + { + minpoint=vertices[0].pos.clone(); + maxpoint=vertices[0].pos.clone(); + } + for(var i=1; i < numvertices; i++) + { + var point = vertices[i].pos; + minpoint.x = Math.min(minpoint.x, point.x); + minpoint.y = Math.min(minpoint.y, point.y); + minpoint.z = Math.min(minpoint.z, point.z); + maxpoint.x = Math.max(maxpoint.x, point.x); + maxpoint.y = Math.max(maxpoint.y, point.y); + maxpoint.z = Math.max(maxpoint.z, point.z); + } + 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); + return new CSG.Polygon(newvertices, this.shared, newplane); + }, + + toStlString: function() { + var result=""; + if(this.vertices.length >= 3) // should be! + { + // STL requires triangular polygons. If our polygon has more vertices, create + // multiple triangles: + var firstVertexStl = this.vertices[0].toStlString(); + for(var i=0; i < this.vertices.length-2; i++) + { + result += "facet normal "+this.plane.normal.toStlString()+"\nouter loop\n"; + result += firstVertexStl; + result += this.vertices[i+1].toStlString(); + result += this.vertices[i+2].toStlString(); + result += "endloop\nendfacet\n"; + } + } + return result; + }, + + toString: function() { + var result = "Polygon plane: "+this.plane.toString()+"\n"; + this.vertices.map(function(vertex) { + result += " "+vertex.toString()+"\n"; + }); + return result; + }, +}; + +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 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()) throw new Error("Assertion failed"); // new polygons can only be added to root node; children can only be splitted polygons + 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) { + if(this.polygon) + { + // the polygon hasn't been broken yet. We can ignore the children and return our polygon: + result.push(this.polygon); + } + else + { + // our polygon has been split up and broken, so gather all subpolygons from the children: + var childpolygons = []; + this.children.map(function(child) { + child.getPolygons(childpolygons); + }); + childpolygons.map(function(p) { + result.push(p); + }); + } + }, + + // 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) { + var children = this.children; + var numchildren = children.length; + if(numchildren > 0) + { + // if we have children, split the children + for(var i = 0; i < numchildren; i++) + { + children[i].splitByPlane(plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes); + } + } + else + { + // no children. Split the polygon: + if(this.polygon) + { + var splitresult = plane.splitPolygon(this.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() { + if(this.polygon) + { + this.polygon = this.polygon.flipped(); + } + this.children.map(function(child) { + child.invertSub(); + }); + }, + + recursivelyInvalidatePolygon: function() { + if(this.polygon) + { + this.polygon = null; + if(this.parent) + { + this.parent.recursivelyInvalidatePolygon(); + } + } + }, + +}; + + + +// # 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(); + 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; + polygons.map(function(p) { + _this.addPolygon(p); + }); + }, + + addPolygon: function(polygon) { + var polygontreenode=this.polygonTree.addChild(polygon); + this.rootnode.addPolygonTreeNode(polygontreenode); + }, +}; + +// # 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() { + this.plane = null; + this.front = null; + this.back = null; + this.polygontreenodes = []; +}; + +CSG.Node.prototype = { + // Convert solid space to empty space and empty space to solid space. + invert: function() { + this.plane = this.plane.flipped(); + if (this.front) this.front.invert(); + if (this.back) this.back.invert(); + var temp = this.front; + this.front = this.back; + this.back = temp; + }, + + // clip polygontreenodes to our plane + // calls remove() for all clipped PolygonTreeNodes + clipPolygons: function(polygontreenodes, alsoRemovecoplanarFront) { + if(this.plane) + { + var backnodes = []; + var frontnodes = []; + var coplanarfrontnodes = alsoRemovecoplanarFront? backnodes:frontnodes; + var plane = this.plane; + var numpolygontreenodes = polygontreenodes.length; + for(i=0; i < numpolygontreenodes; i++) + { + var node = polygontreenodes[i]; + if(!node.isRemoved() ) + { + node.splitByPlane(plane, coplanarfrontnodes, backnodes, frontnodes, backnodes); + } + } + if(this.front && (frontnodes.length > 0) ) + { + this.front.clipPolygons(frontnodes, alsoRemovecoplanarFront); + } + var numbacknodes = backnodes.length; + if(this.back && (numbacknodes > 0) ) + { + this.back.clipPolygons(backnodes, alsoRemovecoplanarFront); + } + else + { + // there's nothing behind this plane. Delete the nodes behind this plane: + for(i=0; i < numbacknodes; i++) + { + backnodes[i].remove(); + } + } + } + }, + + // Remove all polygons in this BSP tree that are inside the other BSP tree + // `tree`. + clipTo: function(tree, alsoRemovecoplanarFront) { + if(this.polygontreenodes.length > 0) + { + tree.rootnode.clipPolygons(this.polygontreenodes, alsoRemovecoplanarFront); + } + if (this.front) this.front.clipTo(tree, alsoRemovecoplanarFront); + if (this.back) this.back.clipTo(tree, alsoRemovecoplanarFront); + }, + + addPolygonTreeNode: function(polygontreenode) { + if(!this.plane) + { + this.plane = polygontreenode.getPolygon().plane; + } + var frontnodes = []; + var backnodes = []; + polygontreenode.splitByPlane(this.plane, this.polygontreenodes, this.polygontreenodes, frontnodes, backnodes); + if(frontnodes.length > 0) + { + if (!this.front) this.front = new CSG.Node(); + this.front.addPolygonTreeNode(frontnodes[0]); + } + if(backnodes.length > 0) + { + if (!this.back) this.back = new CSG.Node(); + this.back.addPolygonTreeNode(backnodes[0]); + } + }, +}; + +////////// + +// # 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); + }, + + // Multiply a CSG.Vector3D (interpreted as 1 row, 3 column) by this matrix + // 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.Vector2D (interpreted as 1 row, 2 column) by this matrix + // 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); + }, +}; + +// 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); +}; + +// 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, vec.x, + 0, 1, 0, vec.y, + 0, 0, 1, vec.z, + 0, 0, 0, 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) { + var ok = true; + if (arguments.length == 1) + { + if(typeof(x) == "object") + { + if(x instanceof Array) + { + this.x = x[0]; + this.y = x[1]; + } + else if( ('x' in x) && ('y' in x) ) + { + this.x = x.x; + this.y = x.y; + } + else ok = false; + } + else + { + var v = Number(x); + this.x = v; + this.y = v; + } + } + else if (arguments.length == 2) + { + this.x = Number(x); + this.y = Number(y); + } + else ok = false; + if(!ok) + { + throw new Error("wrong arguments"); + } +}; + +CSG.Vector2D.prototype = { + // 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 new CSG.Vector2D(this.x, this.y); + }, + + negated: function() { + return new CSG.Vector2D(-this.x, -this.y); + }, + + plus: function(a) { + return new CSG.Vector2D(this.x + a.x, this.y + a.y); + }, + + minus: function(a) { + return new CSG.Vector2D(this.x - a.x, this.y - a.y); + }, + + times: function(a) { + return new CSG.Vector2D(this.x * a, this.y * a); + }, + + dividedBy: function(a) { + return new CSG.Vector2D(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(); + }, + + unit: function() { + return this.dividedBy(this.length()); + }, + + // returns the vector rotated by 90 degrees clockwise + normal: function() { + return new CSG.Vector2D(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.rightMultiply1x2Vector(this); + }, +}; + +// A polygon in 2D space: +CSG.Polygon2D = function(points, shared) { + var vectors = []; + if(arguments.length >= 1) { + points.map( function(p) { + vectors.push(new CSG.Vector2D(p) ); + }); + } + this.points = vectors; + this.shared = shared; +}; + +CSG.Polygon2D.prototype = { + // Matrix transformation of polygon. Returns a new CSG.Polygon2D + transform: function(matrix4x4) { + var newpoints = this.points.map(function(p) { return p.multiply4x4(matrix4x4); } ); + return new CSG.Polygon2D(newpoints, this.shared); + }, + + translate: function(v) { + v=new CSG.Vector2D(v); + return this.transform(CSG.Matrix4x4.translation(v.toVector3D(0))); + }, + + scale: function(f) { + f=new CSG.Vector2D(f); + return this.transform(CSG.Matrix4x4.scaling(f.toVector3D(1))); + }, + + rotate: function(deg) { + return this.transform(CSG.Matrix4x4.rotationZ(deg)); + }, + + // convert into a CSG.Polygon; set z coordinate to the given value + toPolygon3D: function(z) { + var points3d=[]; + this.points.map( function(p) { + var vec3d = p.toVector3D(z); + points3d.push(vec3d); + }); + var polygon = CSG.Polygon.createFromPoints(points3d, this.shared); + polygon.checkIfConvex(); + return polygon; + }, + + // extruded=shape2d.extrude({offset: [0,0,10], twistangle: 360, twiststeps: 100}); + // linear extrusion of 2D polygon, with optional twist + // The 2d polygon 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) { + var offsetvector = CSG.parseOptionAs3DVector(options, "offset", [0,0,1]); + var twistangle = CSG.parseOptionAsFloat(options, "twistangle", 0); + var twiststeps = CSG.parseOptionAsInt(options, "twiststeps", 10); + + // create the polygons: + var newpolygons = []; + + // bottom face polygon: + var bottomfacepolygon = this.toPolygon3D(0); + var direction = bottomfacepolygon.plane.normal.dot(offsetvector); + if(direction > 0) + { + bottomfacepolygon = bottomfacepolygon.flipped(); + } + newpolygons.push(bottomfacepolygon); + + var getTwistedPolygon = function(twiststep) { + var fraction = (twiststep + 1) / twiststeps; + var rotation = twistangle * fraction; + var offset = offsetvector.times(fraction); + var transformmatrix = CSG.Matrix4x4.rotationZ(rotation).multiply( CSG.Matrix4x4.translation(offset) ); + var polygon = bottomfacepolygon.transform(transformmatrix); + return polygon; + }; + + // create the side face polygons: + var numvertices = bottomfacepolygon.vertices.length; + var prevlevelpolygon = bottomfacepolygon; + for(var twiststep=0; twiststep < twiststeps; ++twiststep) + { + var levelpolygon = getTwistedPolygon(twiststep); + for(var i=0; i < numvertices; i++) + { + var sidefacepoints = []; + var nexti = (i < (numvertices-1))? i+1:0; + sidefacepoints.push(prevlevelpolygon.vertices[i].pos); + sidefacepoints.push(levelpolygon.vertices[i].pos); + sidefacepoints.push(levelpolygon.vertices[nexti].pos); + sidefacepoints.push(prevlevelpolygon.vertices[nexti].pos); + var sidefacepolygon=CSG.Polygon.createFromPoints(sidefacepoints, this.shared); + newpolygons.push(sidefacepolygon); + } + if(twiststep == (twiststeps -1) ) + { + // last level; add the top face polygon: + levelpolygon = levelpolygon.flipped(); // flip so that the normal points outwards + newpolygons.push(levelpolygon); + } + prevlevelpolygon = levelpolygon; + } + + return CSG.fromPolygons(newpolygons); + } +}; + + + +// # 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) { + this.normal = normal; + this.w = w; +}; + +CSG.Line2D.fromPoints = function(p1, 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: + inverse: 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) { + var point_projected = point.dot(this.normal); + var distance = Math.abs(point_projected - this.w); + return distance; + }, + + closestPoint: function(point) { + var vector = point.dot(this.direction()); + return origin.plus(vector); + }, +}; + +// # 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) { + this.point = point; + this.direction = direction; +}; + +CSG.Line3D.fromPoints = function(p1, p2) { + var direction = p2.minus(p1).unit(); + 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.Line3D.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.Line3D.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.Line3D.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); +}; + +// solve +// [ab][x] = [u] +// [cd][y] [v] +CSG.Line3D.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]; +}; + +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) { + 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) { + 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) { + // choose an arbitrary right hand vector, making sure it is somewhat orthogonal to the plane normal: + var rightvector; + if(Math.abs(plane.normal.x) > Math.abs(plane.normal.y)) + { + rightvector = new CSG.Vector3D(0, 1, 0); + } + else + { + rightvector = new CSG.Vector3D(1, 0, 0); + } + this.v = rightvector.cross(plane.normal).unit(); + this.u = plane.normal.cross(this.v); + this.planeorigin = plane.normal.times(plane.w); +}; + +CSG.OrthoNormalBasis.prototype = { + 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); + }, +}; + +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 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); + if(ycoordinatebin in ycoordinatebins) + { + pos2d.y = ycoordinatebins[ycoordinatebin]; + } + else if(ycoordinatebin+1 in ycoordinatebins) + { + pos2d.y = ycoordinatebins[ycoordinatebin+1]; + } + else if(ycoordinatebin-1 in ycoordinatebins) + { + pos2d.y = ycoordinatebins[ycoordinatebin-1]; + } + else + { + ycoordinatebins[ycoordinatebin] = pos2d.y; + } + 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 = []; + } + else + { + if(! (miny in topy2polygonindexes)) + { + topy2polygonindexes[miny] = []; + } + topy2polygonindexes[miny].push(polygonindex); + } + } // if(numvertices > 0) + polygonvertices2d.push(vertices2d); + polygontopvertexindexes.push(minindex); + } + var ycoordinates = []; + for(var ycoordinate in ycoordinatetopolygonindexes) ycoordinates.push(ycoordinate); + ycoordinates.sort(function(a,b) {return a-b}); + + // 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=new CSG.Vector2D(x, ycoordinate); + x = CSG.interpolateBetween2DPointsForY(activepolygon.topright, activepolygon.bottomright, ycoordinate); + var topright=new CSG.Vector2D(x, ycoordinate); + x = CSG.interpolateBetween2DPointsForY(activepolygon.topleft, activepolygon.bottomleft, nextycoordinate); + var bottomleft=new CSG.Vector2D(x, nextycoordinate); + x = CSG.interpolateBetween2DPointsForY(activepolygon.topright, activepolygon.bottomright, nextycoordinate); + var bottomright=new CSG.Vector2D(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 right half so we get a counterclockwise circle: + prevpolygon.outpolygon.rightpoints.reverse(); + var points2d = prevpolygon.outpolygon.leftpoints.concat(prevpolygon.outpolygon.rightpoints); + var vertices3d = []; + points2d.map(function(point2d) { + var point3d = orthobasis.to3D(point2d); + var vertex3d = new CSG.Vertex(point3d); + vertices3d.push(vertex3d); + }); + var shared = null; + 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) { + var lookuptable = []; + for(var i=0; i < numdimensions; i++) + { + lookuptable.push({}); + } + this.lookuptable = lookuptable; + this.nextElementId = 1; + this.multiplier = 1.0 / tolerance; + this.objectTable = {}; +}; + +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 object; + var key = this.lookupKey(els); + if(key === null) + { + object = creatorCallback(els); + key = this.nextElementId++; + this.objectTable[key] = object; + for(var dimension = 0; dimension < els.length; dimension++) + { + var elementLookupTable = this.lookuptable[dimension]; + var value = els[dimension]; + var valueMultiplied = value * this.multiplier; + var valueQuantized1 = Math.floor(valueMultiplied); + var valueQuantized2 = Math.ceil(valueMultiplied); + CSG.fuzzyFactory.insertKey(key, elementLookupTable, valueQuantized1); + CSG.fuzzyFactory.insertKey(key, elementLookupTable, valueQuantized2); + } + } + else + { + object = this.objectTable[key]; + } + return object; + }, + + // ----------- PRIVATE METHODS: + lookupKey: function(els) { + var keyset = {}; + for(var dimension=0; dimension < els.length; dimension++) + { + var elementLookupTable = this.lookuptable[dimension]; + var value = els[dimension]; + var valueQuantized = Math.round(value * this.multiplier); + valueQuantized += ""; + if(valueQuantized in elementLookupTable) + { + if(dimension == 0) + { + keyset = elementLookupTable[valueQuantized]; + } + else + { + keyset = CSG.fuzzyFactory.intersectSets(keyset, elementLookupTable[valueQuantized]); + } + } + else + { + return null; + } + if(CSG.fuzzyFactory.isEmptySet(keyset)) return null; + } + // return first matching key: + for(var key in keyset) return key; + return null; + }, + + lookupKeySetForDimension: function(dimension, value) { + var result; + var elementLookupTable = this.lookuptable[dimension]; + var valueMultiplied = value * this.multiplier; + var valueQuantized = Math.floor(value * this.multiplier); + if(valueQuantized in elementLookupTable) + { + result = elementLookupTable[valueQuantized]; + } + else + { + result = {}; + } + return result; + }, +}; + +CSG.fuzzyFactory.insertKey = function(key, lookuptable, quantizedvalue) { + if(quantizedvalue in lookuptable) + { + lookuptable[quantizedvalue][key] = true; + } + else + { + var newset = {}; + newset[key] = true; + lookuptable[quantizedvalue] = newset; + } +}; + +CSG.fuzzyFactory.isEmptySet = function(obj) { + for(var key in obj) return false; + return true; +}; + +CSG.fuzzyFactory.intersectSets = function(set1, set2) { + var result = {}; + for(var key in set1) + { + if(key in set2) + { + result[key] = true; + } + } + return result; +}; + +CSG.fuzzyFactory.joinSets = function(set1, set2) { + var result = {}; + for(var key in set1) + { + result[key] = true; + } + for(var key in set2) + { + result[key] = true; + } + return result; +}; + +////////////////////////////////////// + +CSG.fuzzyCSGFactory = function() { + this.vertexfactory = new CSG.fuzzyFactory(3, 1e-5); + this.planefactory = new CSG.fuzzyFactory(4, 1e-5); +}; + +CSG.fuzzyCSGFactory.prototype = { + 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 _this = this; + var newvertices = sourcepolygon.vertices.map(function(vertex) { + return _this.getVertex(vertex); + }); + return new CSG.Polygon(newvertices, sourcepolygon.shared, newplane); + }, + + getCSG: function(sourcecsg) { + var _this = this; + var newpolygons = sourcecsg.polygons.map(function(polygon) { + return _this.getPolygon(polygon); + }); + return CSG.fromPolygons(newpolygons); + }, +}; + +////////////////////////////////////// + +// Tag factory: we can request a unique tag through CSG.getTag() +CSG.staticTag = 1; + +CSG.getTag = function () { + return CSG.staticTag++; +}; \ No newline at end of file diff --git a/index.html b/index.html index 3ee6711..a2a646a 100644 --- a/index.html +++ b/index.html @@ -1 +1,289 @@ -Test! + + + + + + + + +OpenJsCad + + +

OpenJsCad

+ Please note: currently only works reliably in Google Chrome!
+ Create an STL file using constructive solid modeling. Click Update Preview to parse the source code from the textarea. + Click Get STL to generate the stl data, ready for 3d printing. See below for documentation. + + + + +
+ + +

+
+
+
+ +
+

About

+This is intended to become a Javascript based alternative to OpenSCAD, +for 3D solid modeling. +CSG model is contructed using Javascript. For example:
+var cube = CSG.cube(); return cube; creates a cube with a radius of 1 and centered at the origin. +Enter javascript code in the textbox above. The code should end in a return statement, returning a CSG solid. +Click on Update Preview to generate the mesh and update the viewer. +

+Click Get STL to generate the STL file. Create a file with .stl extension in a text editor and paste the contents +of the box into this file; this is ready to be printed on your 3d printer. +

License

+Copyright (c) 2012 Joost Nieuwenhuijse. +Uses CSG.js, original copyright (c) 2011 Evan Wallace, +extensively modified, copyright (c) 2012 Joost Nieuwenhuijse. +Uses lightgl.js by Evan Wallace for WebGL rendering. +All code released under MIT license. +

+Contributions are welcome! To contribute go to CSG.js at GitHub, +create your own fork and +send me a pull request. +

Viewer navigation

+Click and drag to rotate the model around the origin.
+Shift+Drag moves the model around.
+Alt+drag zooms (by changing the distance between camera and model). + +

Primitive solids

+Currently the following solids are supported. The parameters are passed in an object; most +parameters are optional. 3D vectors can be passed in an array. If a scalar is passed +for a parameter which expects a 3D vector, it is used for the x, y and z value. +In other words: radius: 1 will give radius: [1,1,1]. +

+All rounded solids have a 'resolution' parameter which controls tesselation. If resolution +is set to 8, then 8 polygons per 360 degree of revolution are used. Beware that rendering +time will increase dramatically when increasing the resolution. For a sphere the number of polygons +increases quadratically with the resolution used. +

+
+// a cube:
+var cube = CSG.cube({
+  center: [0, 0, 0],
+  radius: [1, 1, 1]
+});
+
+// a sphere:
+var sphere = CSG.sphere({
+  center: [0, 0, 0],
+  radius: 2,            // must be scalar
+  resolution: 32
+});
+
+// a cylinder:
+var cylinder = CSG.cylinder({
+  start: [0, -1, 0],
+  end: [0, 1, 0],
+  radius: 1,
+  resolution: 16
+});
+
+// like a cylinder, but with spherical endpoints:
+var roundedCylinder = CSG.roundedCylinder({
+  start: [0, -1, 0],
+  end: [0, 1, 0],
+  radius: 1,
+  resolution: 16
+});
+
+// a rounded cube:
+var cube = CSG.roundedCube({
+  center: [0, 0, 0],
+  radius: 1,
+  roundradius: 0.2,
+  resolution: 8,
+});
+
+ +

CSG operations

+The 3 standard CSG operations are supported. All CSG operations return a new solid; the source solids +are not modified: +

+
+var csg1 = cube.union(sphere);
+var csg2 = cube.intersect(sphere);
+var csg3 = cube.subtract(sphere);
+
+ +

Transformations

+Solids can be translated, scaled and rotated. Multiple transforms can be combined into a single matrix transform: +

+
+var cube = CSG.cube();
+
+// translation:
+var cube2 = cube.translate([1, 2, 3]);
+
+// scaling:
+var largecube = cube.scale(2.0);
+var stretchedcube = cube.scale([1.5, 1, 0.5]);
+
+// rotation:
+var rotated1 = cube.rotateX(-45); // rotate around the X axis
+var rotated2 = cube.rotateY(90);  // rotate around the Y axis
+var rotated3 = cube.rotateZ(20);  // rotate around the Z axis
+
+// combine multiple transforms into a single matrix transform:
+var m = new CSG.Matrix4x4();
+m = m.multiply(CSG.Matrix4x4.rotationX(40));
+m = m.multiply(CSG.Matrix4x4.rotationZ(40));
+m = m.multiply(CSG.Matrix4x4.translation([-.5, 0, 0]));
+m = m.multiply(CSG.Matrix4x4.scaling([1.1, 1.2, 1.3]));
+
+// and apply the transform:
+var cube3 = cube.transform(m);
+
+ +

Expansion and contraction

+Expansion can be seen +as the 3D convolution of an object with a sphere. Contraction is the reverse: the area outside the solid +is expanded, and this is then subtracted from the solid. +

+Expansion and contraction are very powerful ways to get an object with nice smooth corners. For example +a rounded cube can be created by expanding a normal cube. +

+Note that these are expensive operations: spheroids are created around every corner and edge in the original +object, so the number of polygons quickly increases. Expansion and contraction therefore are only practical for simple +non-curved objects. +

+expand() and contract() take two parameters: the first is the radius of expansion or contraction; the second +parameter is optional and specififies the resolution (number of polygons on spherical surfaces, per 360 degree revolution). +
+var cube1 = CSG.cube({radius: 1.0});
+var cube2 = CSG.cube({radius: 1.0}).translate([-0.3, -0.3, -0.3]);
+var csg = cube1.subtract(cube2);
+var rounded = csg.expand(0.2, 8); 
+
+ +

2d shapes

+Two dimensional shapes can be defined through the Polygon2D class. Currently this requires the polygon to be convex +(i.e. all corners less than 180 degrees). Shapes can be transformed (rotation, translation, scaling). +To actually use the shape it needs to be extruded into a 3D CSG object through the extrude() function. extrude() +places the 2D solid onto the z=0 plane, and extrudes in the specified direction. Extrusion can be done with an optional +twist. This rotates the solid around the z axis (and not necessariy around the extrusion axis) during extrusion. +The total degrees of rotation is specified in the twistangle parameter, and twiststeps determine the number of steps +between the bottom and top surface. +
+// Create a shape; argument is an array of 2D coordinates
+// The shape must be convex, can be specified in clockwise or in counterclockwise direction 
+var shape2d=new CSG.Polygon2D([[0,0], [5,0], [3,5], [0,5]]);
+
+// Do some transformations:
+shape2d=shape2d.translate([-2, -2]);
+shape2d=shape2d.rotate(20);
+shape2d=shape2d.scale([0.2, 0.2]);
+
+// And extrude. This creates a CSG solid:
+var extruded=shape2d.extrude({
+  offset: [0.5, 0, 2],   // direction for extrusion
+  twistangle: 180,       // top surface is rotated 180 degrees 
+  twiststeps: 100        // create 100 slices
+});
+
+ + + \ No newline at end of file diff --git a/lightgl.js b/lightgl.js new file mode 100644 index 0000000..f02987f --- /dev/null +++ b/lightgl.js @@ -0,0 +1,61 @@ +/* + * lightgl.js + * http://github.com/evanw/lightgl.js/ + * + * Copyright 2011 Evan Wallace + * Released under the MIT license + */ +var GL=function(){function I(){d.MODELVIEW=C|1;d.PROJECTION=C|2;var b=new n,c=new n;d.modelviewMatrix=new n;d.projectionMatrix=new n;var a=[],f=[],g,j;d.matrixMode=function(h){switch(h){case d.MODELVIEW:g="modelviewMatrix";j=a;break;case d.PROJECTION:g="projectionMatrix";j=f;break;default:throw"invalid matrix mode "+h;}};d.loadIdentity=function(){n.identity(d[g])};d.loadMatrix=function(h){h=h.m;for(var i=d[g].m,m=0;m<16;m++)i[m]=h[m]};d.multMatrix=function(h){d.loadMatrix(n.multiply(d[g],h,c))};d.perspective= +function(h,i,m,k){d.multMatrix(n.perspective(h,i,m,k,b))};d.frustum=function(h,i,m,k,o,p){d.multMatrix(n.frustum(h,i,m,k,o,p,b))};d.ortho=function(h,i,m,k,o,p){d.multMatrix(n.ortho(h,i,m,k,o,p,b))};d.scale=function(h,i,m){d.multMatrix(n.scale(h,i,m,b))};d.translate=function(h,i,m){d.multMatrix(n.translate(h,i,m,b))};d.rotate=function(h,i,m,k){d.multMatrix(n.rotate(h,i,m,k,b))};d.lookAt=function(h,i,m,k,o,p,J,K,L){d.multMatrix(n.lookAt(h,i,m,k,o,p,J,K,L,b))};d.pushMatrix=function(){j.push(Array.prototype.slice.call(d[g].m))}; +d.popMatrix=function(){var h=j.pop();d[g].m=D?new Float32Array(h):h};d.project=function(h,i,m,k,o,p){k=k||d.modelviewMatrix;o=o||d.projectionMatrix;p=p||d.getParameter(d.VIEWPORT);h=o.transformPoint(k.transformPoint(new l(h,i,m)));return new l(p[0]+p[2]*(h.x*0.5+0.5),p[1]+p[3]*(h.y*0.5+0.5),h.z*0.5+0.5)};d.unProject=function(h,i,m,k,o,p){k=k||d.modelviewMatrix;o=o||d.projectionMatrix;p=p||d.getParameter(d.VIEWPORT);h=new l((h-p[0])/p[2]*2-1,(i-p[1])/p[3]*2-1,m*2-1);return n.inverse(n.multiply(o,k, +b),c).transformPoint(h)};d.matrixMode(d.MODELVIEW)}function M(){var b={mesh:new q({coords:true,colors:true,triangles:false}),mode:-1,coord:[0,0,0,0],color:[1,1,1,1],pointSize:1,shader:new y("uniform float pointSize;varying vec4 color;varying vec4 coord;varying vec2 pixel;void main(){color=gl_Color;coord=gl_TexCoord;gl_Position=gl_ModelViewProjectionMatrix*gl_Vertex;pixel=gl_Position.xy/gl_Position.w*0.5+0.5;gl_PointSize=pointSize;}", +"uniform sampler2D texture;uniform float pointSize;uniform bool useTexture;uniform vec2 windowSize;varying vec4 color;varying vec4 coord;varying vec2 pixel;void main(){gl_FragColor=color;if(useTexture)gl_FragColor*=texture2D(texture,coord.xy);}")};d.pointSize=function(c){b.shader.uniforms({pointSize:c})};d.begin=function(c){if(b.mode!=-1)throw"mismatched gl.begin() and gl.end() calls";b.mode=c;b.mesh.colors=[];b.mesh.coords= +[];b.mesh.vertices=[]};d.color=function(c,a,f,g){b.color=arguments.length==1?c.toArray().concat(1):[c,a,f,g||1]};d.texCoord=function(c,a){b.coord=arguments.length==1?c.toArray(2):[c,a]};d.vertex=function(c,a,f){b.mesh.colors.push(b.color);b.mesh.coords.push(b.coord);b.mesh.vertices.push(arguments.length==1?c.toArray():[c,a,f])};d.end=function(){if(b.mode==-1)throw"mismatched gl.begin() and gl.end() calls";b.mesh.compile();b.shader.uniforms({windowSize:[d.canvas.width,d.canvas.height],useTexture:!!d.getParameter(d.TEXTURE_BINDING_2D)}).draw(b.mesh, +b.mode);b.mode=-1}}function N(){function b(){for(var k in i)if(i[k])return true;return false}function c(k){e=Object.create(k);e.original=k;e.x=e.pageX;e.y=e.pageY;for(k=d.canvas;k;k=k.offsetParent){e.x-=k.offsetLeft;e.y-=k.offsetTop}if(m){e.deltaX=e.x-j;e.deltaY=e.y-h}else{e.deltaX=0;e.deltaY=0;m=true}j=e.x;h=e.y;e.dragging=b();e.preventDefault=function(){e.original.preventDefault()};e.stopPropagation=function(){e.original.stopPropagation()};return e}function a(k){k=c(k);d.onmousemove&&d.onmousemove(k); +k.preventDefault()}function f(k){i[k.which]=false;if(!b()){document.removeEventListener("mousemove",a);document.removeEventListener("mouseup",f);d.canvas.addEventListener("mousemove",a);d.canvas.addEventListener("mouseup",f)}k=c(k);d.onmouseup&&d.onmouseup(k);k.preventDefault()}function g(){m=false}var j=0,h=0,i={},m=false;z(d.canvas,"mousedown",function(k){if(!b()){document.addEventListener("mousemove",a);document.addEventListener("mouseup",f);d.canvas.removeEventListener("mousemove",a);d.canvas.removeEventListener("mouseup", +f)}i[k.which]=true;k=c(k);d.onmousedown&&d.onmousedown(k);k.preventDefault()});d.canvas.addEventListener("mousemove",a);d.canvas.addEventListener("mouseup",f);d.canvas.addEventListener("mouseover",g);d.canvas.addEventListener("mouseout",g)}function E(b){return{8:"BACKSPACE",9:"TAB",13:"ENTER",16:"SHIFT",27:"ESCAPE",32:"SPACE",37:"LEFT",38:"UP",39:"RIGHT",40:"DOWN"}[b]||(b>=65&&b<=90?String.fromCharCode(b):null)}function z(b,c,a){b.addEventListener(c,a)}function O(){(function(b){d.makeCurrent=function(){d= +b}})(d);d.animate=function(){function b(){d=f;var g=new Date;d.onupdate&&d.onupdate((g-a)/1E3);d.ondraw&&d.ondraw();c(b);a=g}var c=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||function(g){setTimeout(g,1E3/60)},a=new Date,f=d;b()};d.fullscreen=function(b){function c(){d.canvas.width=window.innerWidth-f-g;d.canvas.height=window.innerHeight-a-j;d.viewport(0,0,d.canvas.width,d.canvas.height);if(b.camera||!("camera"in b)){d.matrixMode(d.PROJECTION); +d.loadIdentity();d.perspective(b.fov||45,d.canvas.width/d.canvas.height,b.near||0.1,b.far||1E3);d.matrixMode(d.MODELVIEW)}d.ondraw&&d.ondraw()}b=b||{};var a=b.paddingTop||0,f=b.paddingLeft||0,g=b.paddingRight||0,j=b.paddingBottom||0;if(!document.body)throw"document.body doesn't exist yet (call gl.fullscreen() from window.onload() or from inside the tag)";document.body.appendChild(d.canvas);document.body.style.overflow="hidden";d.canvas.style.position="absolute";d.canvas.style.left=f+"px";d.canvas.style.top= +a+"px";window.addEventListener("resize",c);c()}}function n(){var b=Array.prototype.concat.apply([],arguments);b.length||(b=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]);this.m=D?new Float32Array(b):b}function w(){this.unique=[];this.indices=[];this.map={}}function x(b,c){this.buffer=null;this.target=b;this.type=c;this.data=[]}function q(b){b=b||{};this.vertexBuffers={};this.indexBuffers={};this.addVertexBuffer("vertices","gl_Vertex");b.coords&&this.addVertexBuffer("coords","gl_TexCoord");b.normals&&this.addVertexBuffer("normals", +"gl_Normal");b.colors&&this.addVertexBuffer("colors","gl_Color");if(!("triangles"in b)||b.triangles)this.addIndexBuffer("triangles");b.lines&&this.addIndexBuffer("lines")}function F(b){return new l((b&1)*2-1,(b&2)-1,(b&4)/2-1)}function t(b,c,a){this.t=arguments.length?b:Number.MAX_VALUE;this.hit=c;this.normal=a}function u(){var b=d.getParameter(d.VIEWPORT),c=d.modelviewMatrix.m,a=new l(c[0],c[4],c[8]),f=new l(c[1],c[5],c[9]),g=new l(c[2],c[6],c[10]);c=new l(c[3],c[7],c[11]);this.eye=new l(-c.dot(a), +-c.dot(f),-c.dot(g));a=b[0];f=a+b[2];g=b[1];c=g+b[3];this.ray00=d.unProject(a,g,1).subtract(this.eye);this.ray10=d.unProject(f,g,1).subtract(this.eye);this.ray01=d.unProject(a,c,1).subtract(this.eye);this.ray11=d.unProject(f,c,1).subtract(this.eye);this.viewport=b}function y(b,c){function a(i,m,k){for(;(result=i.exec(m))!=null;)k(result)}function f(i,m){var k={},o=/^((\s*\/\/.*\n|\s*#extension.*\n)+)[^]*$/.exec(m);m=o?o[1]+i+m.substr(o[1].length):i+m;a(/\bgl_\w+\b/g,i,function(p){if(!(p in k)){m= +m.replace(RegExp("\\b"+p+"\\b","g"),"_"+p);k[p]=true}});return m}function g(i,m){var k=d.createShader(i);d.shaderSource(k,m);d.compileShader(k);if(!d.getShaderParameter(k,d.COMPILE_STATUS))throw"compile error: "+d.getShaderInfoLog(k);return k}var j=b+c;this.needsMVPM=/(gl_ModelViewProjectionMatrix|ftransform)/.test(j);this.needsNM=/gl_NormalMatrix/.test(j);b=f("uniform mat3 gl_NormalMatrix;uniform mat4 gl_ModelViewMatrix;uniform mat4 gl_ProjectionMatrix;uniform mat4 gl_ModelViewProjectionMatrix;attribute vec4 gl_Vertex;attribute vec4 gl_TexCoord;attribute vec3 gl_Normal;attribute vec4 gl_Color;vec4 ftransform(){return gl_ModelViewProjectionMatrix*gl_Vertex;}", +b);c=f("precision highp float;uniform mat3 gl_NormalMatrix;uniform mat4 gl_ModelViewMatrix;uniform mat4 gl_ProjectionMatrix;uniform mat4 gl_ModelViewProjectionMatrix;",c);this.program=d.createProgram();d.attachShader(this.program,g(d.VERTEX_SHADER,b));d.attachShader(this.program,g(d.FRAGMENT_SHADER,c));d.linkProgram(this.program);if(!d.getProgramParameter(this.program,d.LINK_STATUS))throw"link error: "+d.getProgramInfoLog(this.program);this.attributes={};var h={};a(/uniform\s+sampler(1D|2D|3D|Cube)\s+(\w+)\s*;/g, +b+c,function(i){h[i[2]]=1});this.isSampler=h}function s(b,c,a){a=a||{};this.id=d.createTexture();this.width=b;this.height=c;this.format=a.format||d.RGBA;this.type=a.type||d.UNSIGNED_BYTE;d.bindTexture(d.TEXTURE_2D,this.id);d.pixelStorei(d.UNPACK_FLIP_Y_WEBGL,1);d.texParameteri(d.TEXTURE_2D,d.TEXTURE_MAG_FILTER,a.filter||a.magFilter||d.LINEAR);d.texParameteri(d.TEXTURE_2D,d.TEXTURE_MIN_FILTER,a.filter||a.minFilter||d.LINEAR);d.texParameteri(d.TEXTURE_2D,d.TEXTURE_WRAP_S,a.wrap||a.wrapS||d.CLAMP_TO_EDGE); +d.texParameteri(d.TEXTURE_2D,d.TEXTURE_WRAP_T,a.wrap||a.wrapT||d.CLAMP_TO_EDGE);d.texImage2D(d.TEXTURE_2D,0,this.format,b,c,0,this.format,this.type,null)}function l(b,c,a){this.x=b||0;this.y=c||0;this.z=a||0}var d,v={create:function(b){b=b||{};var c=document.createElement("canvas");c.width=800;c.height=600;if(!("alpha"in b))b.alpha=false;try{d=c.getContext("webgl",b)}catch(a){}try{d=d||c.getContext("experimental-webgl",b)}catch(f){}if(!d)throw"WebGL not supported";I();M();N();O();return d},keys:{}, +Matrix:n,Indexer:w,Buffer:x,Mesh:q,HitTest:t,Raytracer:u,Shader:y,Texture:s,Vector:l};z(document,"keydown",function(b){if(!b.altKey&&!b.ctrlKey&&!b.metaKey){var c=E(b.keyCode);if(c)v.keys[c]=true;v.keys[b.keyCode]=true}});z(document,"keyup",function(b){if(!b.altKey&&!b.ctrlKey&&!b.metaKey){var c=E(b.keyCode);if(c)v.keys[c]=false;v.keys[b.keyCode]=false}});var C=305397760,D=typeof Float32Array!="undefined";n.prototype={inverse:function(){return n.inverse(this,new n)},transpose:function(){return n.transpose(this, +new n)},multiply:function(b){return n.multiply(this,b,new n)},transformPoint:function(b){var c=this.m;return(new l(c[0]*b.x+c[1]*b.y+c[2]*b.z+c[3],c[4]*b.x+c[5]*b.y+c[6]*b.z+c[7],c[8]*b.x+c[9]*b.y+c[10]*b.z+c[11])).divide(c[12]*b.x+c[13]*b.y+c[14]*b.z+c[15])},transformVector:function(b){var c=this.m;return new l(c[0]*b.x+c[1]*b.y+c[2]*b.z,c[4]*b.x+c[5]*b.y+c[6]*b.z,c[8]*b.x+c[9]*b.y+c[10]*b.z)}};n.inverse=function(b,c){c=c||new n;var a=b.m,f=c.m;f[0]=a[5]*a[10]*a[15]-a[5]*a[14]*a[11]-a[6]*a[9]*a[15]+ +a[6]*a[13]*a[11]+a[7]*a[9]*a[14]-a[7]*a[13]*a[10];f[1]=-a[1]*a[10]*a[15]+a[1]*a[14]*a[11]+a[2]*a[9]*a[15]-a[2]*a[13]*a[11]-a[3]*a[9]*a[14]+a[3]*a[13]*a[10];f[2]=a[1]*a[6]*a[15]-a[1]*a[14]*a[7]-a[2]*a[5]*a[15]+a[2]*a[13]*a[7]+a[3]*a[5]*a[14]-a[3]*a[13]*a[6];f[3]=-a[1]*a[6]*a[11]+a[1]*a[10]*a[7]+a[2]*a[5]*a[11]-a[2]*a[9]*a[7]-a[3]*a[5]*a[10]+a[3]*a[9]*a[6];f[4]=-a[4]*a[10]*a[15]+a[4]*a[14]*a[11]+a[6]*a[8]*a[15]-a[6]*a[12]*a[11]-a[7]*a[8]*a[14]+a[7]*a[12]*a[10];f[5]=a[0]*a[10]*a[15]-a[0]*a[14]*a[11]- +a[2]*a[8]*a[15]+a[2]*a[12]*a[11]+a[3]*a[8]*a[14]-a[3]*a[12]*a[10];f[6]=-a[0]*a[6]*a[15]+a[0]*a[14]*a[7]+a[2]*a[4]*a[15]-a[2]*a[12]*a[7]-a[3]*a[4]*a[14]+a[3]*a[12]*a[6];f[7]=a[0]*a[6]*a[11]-a[0]*a[10]*a[7]-a[2]*a[4]*a[11]+a[2]*a[8]*a[7]+a[3]*a[4]*a[10]-a[3]*a[8]*a[6];f[8]=a[4]*a[9]*a[15]-a[4]*a[13]*a[11]-a[5]*a[8]*a[15]+a[5]*a[12]*a[11]+a[7]*a[8]*a[13]-a[7]*a[12]*a[9];f[9]=-a[0]*a[9]*a[15]+a[0]*a[13]*a[11]+a[1]*a[8]*a[15]-a[1]*a[12]*a[11]-a[3]*a[8]*a[13]+a[3]*a[12]*a[9];f[10]=a[0]*a[5]*a[15]-a[0]* +a[13]*a[7]-a[1]*a[4]*a[15]+a[1]*a[12]*a[7]+a[3]*a[4]*a[13]-a[3]*a[12]*a[5];f[11]=-a[0]*a[5]*a[11]+a[0]*a[9]*a[7]+a[1]*a[4]*a[11]-a[1]*a[8]*a[7]-a[3]*a[4]*a[9]+a[3]*a[8]*a[5];f[12]=-a[4]*a[9]*a[14]+a[4]*a[13]*a[10]+a[5]*a[8]*a[14]-a[5]*a[12]*a[10]-a[6]*a[8]*a[13]+a[6]*a[12]*a[9];f[13]=a[0]*a[9]*a[14]-a[0]*a[13]*a[10]-a[1]*a[8]*a[14]+a[1]*a[12]*a[10]+a[2]*a[8]*a[13]-a[2]*a[12]*a[9];f[14]=-a[0]*a[5]*a[14]+a[0]*a[13]*a[6]+a[1]*a[4]*a[14]-a[1]*a[12]*a[6]-a[2]*a[4]*a[13]+a[2]*a[12]*a[5];f[15]=a[0]*a[5]* +a[10]-a[0]*a[9]*a[6]-a[1]*a[4]*a[10]+a[1]*a[8]*a[6]+a[2]*a[4]*a[9]-a[2]*a[8]*a[5];a=a[0]*f[0]+a[1]*f[4]+a[2]*f[8]+a[3]*f[12];for(var g=0;g<16;g++)f[g]/=a;return c};n.transpose=function(b,c){c=c||new n;var a=b.m,f=c.m;f[0]=a[0];f[1]=a[4];f[2]=a[8];f[3]=a[12];f[4]=a[1];f[5]=a[5];f[6]=a[9];f[7]=a[13];f[8]=a[2];f[9]=a[6];f[10]=a[10];f[11]=a[14];f[12]=a[3];f[13]=a[7];f[14]=a[11];f[15]=a[15];return c};n.multiply=function(b,c,a){a=a||new n;b=b.m;c=c.m;var f=a.m;f[0]=b[0]*c[0]+b[1]*c[4]+b[2]*c[8]+b[3]*c[12]; +f[1]=b[0]*c[1]+b[1]*c[5]+b[2]*c[9]+b[3]*c[13];f[2]=b[0]*c[2]+b[1]*c[6]+b[2]*c[10]+b[3]*c[14];f[3]=b[0]*c[3]+b[1]*c[7]+b[2]*c[11]+b[3]*c[15];f[4]=b[4]*c[0]+b[5]*c[4]+b[6]*c[8]+b[7]*c[12];f[5]=b[4]*c[1]+b[5]*c[5]+b[6]*c[9]+b[7]*c[13];f[6]=b[4]*c[2]+b[5]*c[6]+b[6]*c[10]+b[7]*c[14];f[7]=b[4]*c[3]+b[5]*c[7]+b[6]*c[11]+b[7]*c[15];f[8]=b[8]*c[0]+b[9]*c[4]+b[10]*c[8]+b[11]*c[12];f[9]=b[8]*c[1]+b[9]*c[5]+b[10]*c[9]+b[11]*c[13];f[10]=b[8]*c[2]+b[9]*c[6]+b[10]*c[10]+b[11]*c[14];f[11]=b[8]*c[3]+b[9]*c[7]+b[10]* +c[11]+b[11]*c[15];f[12]=b[12]*c[0]+b[13]*c[4]+b[14]*c[8]+b[15]*c[12];f[13]=b[12]*c[1]+b[13]*c[5]+b[14]*c[9]+b[15]*c[13];f[14]=b[12]*c[2]+b[13]*c[6]+b[14]*c[10]+b[15]*c[14];f[15]=b[12]*c[3]+b[13]*c[7]+b[14]*c[11]+b[15]*c[15];return a};n.identity=function(b){b=b||new n;var c=b.m;c[0]=c[5]=c[10]=c[15]=1;c[1]=c[2]=c[3]=c[4]=c[6]=c[7]=c[8]=c[9]=c[11]=c[12]=c[13]=c[14]=0;return b};n.perspective=function(b,c,a,f,g){b=Math.tan(b*Math.PI/360)*a;c=b*c;return n.frustum(-c,c,-b,b,a,f,g)};n.frustum=function(b, +c,a,f,g,j,h){h=h||new n;var i=h.m;i[0]=2*g/(c-b);i[1]=0;i[2]=(c+b)/(c-b);i[3]=0;i[4]=0;i[5]=2*g/(f-a);i[6]=(f+a)/(f-a);i[7]=0;i[8]=0;i[9]=0;i[10]=-(j+g)/(j-g);i[11]=-2*j*g/(j-g);i[12]=0;i[13]=0;i[14]=-1;i[15]=0;return h};n.ortho=function(b,c,a,f,g,j,h){h=h||new n;var i=h.m;i[0]=2/(c-b);i[1]=0;i[2]=0;i[3]=-(c+b)/(c-b);i[4]=0;i[5]=2/(f-a);i[6]=0;i[7]=-(f+a)/(f-a);i[8]=0;i[9]=0;i[10]=-2/(j-g);i[11]=-(j+g)/(j-g);i[12]=0;i[13]=0;i[14]=0;i[15]=1;return h};n.scale=function(b,c,a,f){f=f||new n;var g=f.m; +g[0]=b;g[1]=0;g[2]=0;g[3]=0;g[4]=0;g[5]=c;g[6]=0;g[7]=0;g[8]=0;g[9]=0;g[10]=a;g[11]=0;g[12]=0;g[13]=0;g[14]=0;g[15]=1;return f};n.translate=function(b,c,a,f){f=f||new n;var g=f.m;g[0]=1;g[1]=0;g[2]=0;g[3]=b;g[4]=0;g[5]=1;g[6]=0;g[7]=c;g[8]=0;g[9]=0;g[10]=1;g[11]=a;g[12]=0;g[13]=0;g[14]=0;g[15]=1;return f};n.rotate=function(b,c,a,f,g){if(!b||!c&&!a&&!f)return n.identity(g);g=g||new n;var j=g.m,h=Math.sqrt(c*c+a*a+f*f);b*=Math.PI/180;c/=h;a/=h;f/=h;h=Math.cos(b);b=Math.sin(b);var i=1-h;j[0]=c*c*i+h; +j[1]=c*a*i-f*b;j[2]=c*f*i+a*b;j[3]=0;j[4]=a*c*i+f*b;j[5]=a*a*i+h;j[6]=a*f*i-c*b;j[7]=0;j[8]=f*c*i-a*b;j[9]=f*a*i+c*b;j[10]=f*f*i+h;j[11]=0;j[12]=0;j[13]=0;j[14]=0;j[15]=1;return g};n.lookAt=function(b,c,a,f,g,j,h,i,m,k){k=k||new n;var o=k.m;b=new l(b,c,a);f=new l(f,g,j);i=new l(h,i,m);h=b.subtract(f).unit();i=i.cross(h).unit();m=h.cross(i).unit();o[0]=i.x;o[1]=i.y;o[2]=i.z;o[3]=-i.dot(b);o[4]=m.x;o[5]=m.y;o[6]=m.z;o[7]=-m.dot(b);o[8]=h.x;o[9]=h.y;o[10]=h.z;o[11]=-h.dot(b);o[12]=0;o[13]=0;o[14]=0; +o[15]=1;return k};w.prototype={add:function(b){var c=JSON.stringify(b);if(!(c in this.map)){this.map[c]=this.unique.length;this.unique.push(b)}return this.map[c]}};x.prototype={compile:function(b){for(var c=[],a=0;a0,j=[],h=0;h<=detail;h++){for(var i=0;h+i<=detail;i++){var m=h/detail,k=i/detail,o=(detail-h-i)/detail;k={vertex:(new l(m+(m-m*m)/2,k+(k-k*k)/2,o+(o-o*o)/2)).unit().multiply(f).toArray()};if(c.coords)k.coord=f.y>0?[1-m,o]:[o,1-m];j.push(a.add(k))}if(h>0)for(i=0;h+i<=detail;i++){m=(h-1)*(detail+1)+(h-1-(h-1)*(h-1))/2+i;k=h*(detail+1)+(h-h*h)/2+i;c.triangles.push(g?[j[m],j[k],j[m+1]]:[j[m],j[m+1],j[k]]);h+i0&&b.t0&&hf.x)-(b.xf.y)-(b.yf.z)-(b.z0){j=(-h-Math.sqrt(g))/(2*j);b=b.add(c.multiply(j));return new t(j,b,b.subtract(a).divide(f))}return null};u.hitTestTriangle=function(b,c,a,f,g){var j=f.subtract(a),h=g.subtract(a); +g=j.cross(h).unit();f=g.dot(a.subtract(b)).divide(g.dot(c));if(f>0){b=b.add(c.multiply(f));var i=b.subtract(a);a=h.dot(h);c=h.dot(j);h=h.dot(i);var m=j.dot(j);j=j.dot(i);i=a*m-c*c;m=(m*h-c*j)/i;j=(a*j-c*h)/i;if(m>=0&&j>=0&&m+j<=1)return new t(f,b,g)}return null};var P=new n,H=new n;y.prototype={uniforms:function(b){d.useProgram(this.program);for(var c in b){var a=d.getUniformLocation(this.program,c);if(a){var f=b[c];if(f instanceof l)f=[f.x,f.y,f.z];else if(f instanceof n)f=f.m;var g=Object.prototype.toString.call(f); +if(g=="[object Array]"||g=="[object Float32Array]")switch(f.length){case 1:d.uniform1fv(a,new Float32Array(f));break;case 2:d.uniform2fv(a,new Float32Array(f));break;case 3:d.uniform3fv(a,new Float32Array(f));break;case 4:d.uniform4fv(a,new Float32Array(f));break;case 9:d.uniformMatrix3fv(a,false,new Float32Array([f[0],f[3],f[6],f[1],f[4],f[7],f[2],f[5],f[8]]));break;case 16:d.uniformMatrix4fv(a,false,new Float32Array([f[0],f[4],f[8],f[12],f[1],f[5],f[9],f[13],f[2],f[6],f[10],f[14],f[3],f[7],f[11], +f[15]]));break;default:throw"don't know how to load uniform \""+c+'" of length '+f.length;}else{g=Object.prototype.toString.call(f);if(g=="[object Number]"||g=="[object Boolean]")(this.isSampler[c]?d.uniform1i:d.uniform1f).call(d,a,f);else throw'attempted to set uniform "'+c+'" to invalid value '+f;}}}return this},draw:function(b,c){this.drawBuffers(b.vertexBuffers,b.indexBuffers[c==d.LINES?"lines":"triangles"],arguments.length<2?d.TRIANGLES:c)},drawBuffers:function(b,c,a){this.uniforms({_gl_ModelViewMatrix:d.modelviewMatrix, +_gl_ProjectionMatrix:d.projectionMatrix});this.needsMVPM&&this.uniforms({_gl_ModelViewProjectionMatrix:n.multiply(d.projectionMatrix,d.modelviewMatrix,H)});if(this.needsNM){var f=n.transpose(n.inverse(d.modelviewMatrix,P),H).m;this.uniforms({_gl_NormalMatrix:[f[0],f[1],f[2],f[4],f[5],f[6],f[8],f[9],f[10]]})}f=0;for(var g in b){var j=b[g],h=this.attributes[g]||d.getAttribLocation(this.program,g.replace(/^gl_/,"_gl_"));if(!(h==-1||!j.buffer)){this.attributes[g]=h;d.bindBuffer(d.ARRAY_BUFFER,j.buffer); +d.enableVertexAttribArray(h);d.vertexAttribPointer(h,j.buffer.spacing,d.FLOAT,false,0,0);f=j.buffer.length/j.buffer.spacing}}for(g in this.attributes)g in b||d.disableVertexAttribArray(this.attributes[g]);if(f&&(!c||c.buffer))if(c){d.bindBuffer(d.ELEMENT_ARRAY_BUFFER,c.buffer);d.drawElements(a,c.buffer.length,d.UNSIGNED_SHORT,0)}else d.drawArrays(a,0,f);return this}};var A,r,B;s.prototype={bind:function(b){d.activeTexture(d.TEXTURE0+(b||0));d.bindTexture(d.TEXTURE_2D,this.id)},unbind:function(b){d.activeTexture(d.TEXTURE0+ +(b||0));d.bindTexture(d.TEXTURE_2D,null)},drawTo:function(b){var c=d.getParameter(d.VIEWPORT);A=A||d.createFramebuffer();r=r||d.createRenderbuffer();d.bindFramebuffer(d.FRAMEBUFFER,A);d.bindRenderbuffer(d.RENDERBUFFER,r);if(this.width!=r.width||this.height!=r.height){r.width=this.width;r.height=this.height;d.renderbufferStorage(d.RENDERBUFFER,d.DEPTH_COMPONENT16,this.width,this.height)}d.framebufferTexture2D(d.FRAMEBUFFER,d.COLOR_ATTACHMENT0,d.TEXTURE_2D,this.id,0);d.framebufferRenderbuffer(d.FRAMEBUFFER, +d.DEPTH_ATTACHMENT,d.RENDERBUFFER,r);d.viewport(0,0,this.width,this.height);b();d.bindFramebuffer(d.FRAMEBUFFER,null);d.bindRenderbuffer(d.RENDERBUFFER,null);d.viewport(c[0],c[1],c[2],c[3])},swapWith:function(b){var c;c=b.id;b.id=this.id;this.id=c;c=b.width;b.width=this.width;this.width=c;c=b.height;b.height=this.height;this.height=c}};s.fromImage=function(b,c){c=c||{};var a=new s(b.width,b.height,c);try{d.texImage2D(d.TEXTURE_2D,0,a.format,a.format,a.type,b)}catch(f){if(location.protocol=="file:")throw'image not loaded for security reasons (serve this page over "http://" instead)'; +else throw"image not loaded for security reasons (image must originate from the same domain as this page or use Cross-Origin Resource Sharing)";}c.minFilter&&c.minFilter!=d.NEAREST&&c.minFilter!=d.LINEAR&&d.generateMipmap(d.TEXTURE_2D);return a};s.fromURL=function(b,c){B=B||function(){var g=document.createElement("canvas").getContext("2d");g.canvas.width=g.canvas.height=128;for(var j=0;j