diff --git a/web/app/main.js b/web/app/main.js index 4f6ee6e2..bd3afb13 100644 --- a/web/app/main.js +++ b/web/app/main.js @@ -88,10 +88,23 @@ TCAD.App.prototype.extrude = function() { var app = this; this.craft.modify(polyFace.solid, function() { - return TCAD.craft.extrude(app, polyFace, height); + return TCAD.craft.extrude(app, polyFace, polyFace.solid.polyFaces, height); }); }; TCAD.App.prototype.cut = function(face, depth) { }; + + + +TCAD.App.prototype.save = function() { + + var polyFace = this.viewer.selectionMgr.selection[0]; + var height = prompt("Height", "50"); + + var app = this; + this.craft.modify(polyFace.solid, function() { + return TCAD.craft.extrude(app, polyFace, height); + }); +}; \ No newline at end of file diff --git a/web/app/workbench.js b/web/app/workbench.js index 58f41b82..d769a495 100644 --- a/web/app/workbench.js +++ b/web/app/workbench.js @@ -20,23 +20,44 @@ TCAD.workbench.readSketchGeom = function(sketch) { } return out; } -}; +}; + +TCAD.workbench.serializeSolid = function(solid) { + data = {}; + data.faceCounter = TCAD.geom.FACE_COUNTER; + for (var fi = 0; fi < solid.faces.length; ++fi) { + var face = solid.faces[fi]; + var faceData = {}; + faceData.id = face.id; + } + return data; +} + +TCAD.workbench.applyHistory = function(history) { + + for (var hi = 0; hi < history.length; ++hi) { + var mod = history[hi]; + switch (mod.operation) { + } + + } + +}; + TCAD.craft = {}; -TCAD.craft.extrude = function(app, face, height) { +TCAD.craft.getSketchedPolygons3D = function(app, face) { var savedFace = localStorage.getItem(app.faceStorageKey(face.id)); if (savedFace == null) return null; - + var geom = TCAD.workbench.readSketchGeom(JSON.parse(savedFace)); var polygons2D = TCAD.utils.sketchToPolygons(geom); - var solid = face.solid; var normal = face.polygon.normal; var depth = null; var sketchedPolygons = []; - var newSolidFaces = []; for (var i = 0; i < polygons2D.length; i++) { var poly2D = polygons2D[i]; if (poly2D.shell.length < 3) continue; @@ -57,7 +78,16 @@ TCAD.craft.extrude = function(app, face, height) { } sketchedPolygons.push(new TCAD.Polygon(shell)); } + return sketchedPolygons; +}; +TCAD.craft.extrude = function(app, face, faces, height) { + + var sketchedPolygons = TCAD.craft.getSketchedPolygons3D(app, face); + if (sketchedPolygons == null) return null; + + var newSolidFaces = []; + var normal = face.polygon.normal; for (var i = 0; i < sketchedPolygons.length; i++) { var extruded = TCAD.geom.extrude(sketchedPolygons[i], normal.multiply(height)); newSolidFaces = newSolidFaces.concat(newSolidFaces, extruded); @@ -65,8 +95,29 @@ TCAD.craft.extrude = function(app, face, height) { face.polygon.__face = undefined; - for (var i = 0; i < solid.polyFaces.length; i++) { - newSolidFaces.push(solid.polyFaces[i].polygon); + for (var i = 0; i < faces.length; i++) { + newSolidFaces.push(faces[i].polygon); + } + return newSolidFaces; +}; + + +TCAD.craft.cut = function(app, face, faces, height) { + + var sketchedPolygons = TCAD.craft.getSketchedPolygons3D(app, face); + if (sketchedPolygons == null) return null; + + var newSolidFaces = []; + var normal = face.polygon.normal; + for (var i = 0; i < sketchedPolygons.length; i++) { + var extruded = TCAD.geom.extrude(sketchedPolygons[i], normal.multiply(height)); + newSolidFaces = newSolidFaces.concat(newSolidFaces, extruded); + } + + face.polygon.__face = undefined; + + for (var i = 0; i < faces.length; i++) { + newSolidFaces.push(faces[i].polygon); } return newSolidFaces; }; diff --git a/web/lib/csg.js b/web/lib/csg.js new file mode 100644 index 00000000..2c2abd2e --- /dev/null +++ b/web/lib/csg.js @@ -0,0 +1,595 @@ +// Constructive Solid Geometry (CSG) is a modeling technique that uses Boolean +// operations like union and intersection to combine 3D solids. This library +// implements CSG operations on meshes elegantly and concisely using BSP trees, +// and is meant to serve as an easily understandable implementation of the +// algorithm. All edge cases involving overlapping coplanar polygons in both +// solids are correctly handled. +// +// Example usage: +// +// var cube = CSG.cube(); +// var sphere = CSG.sphere({ radius: 1.3 }); +// var polygons = cube.subtract(sphere).toPolygons(); +// +// ## Implementation Details +// +// All CSG operations are implemented in terms of two functions, `clipTo()` and +// `invert()`, which remove parts of a BSP tree inside another BSP tree and swap +// solid and empty space, respectively. To find the union of `a` and `b`, we +// want to remove everything in `a` inside `b` and everything in `b` inside `a`, +// then combine polygons from `a` and `b` into one solid: +// +// a.clipTo(b); +// b.clipTo(a); +// a.build(b.allPolygons()); +// +// The only tricky part is handling overlapping coplanar polygons in both trees. +// The code above keeps both copies, but we need to keep them in one tree and +// remove them in the other tree. To remove them from `b` we can clip the +// inverse of `b` against `a`. The code for union now looks like this: +// +// a.clipTo(b); +// b.clipTo(a); +// b.invert(); +// b.clipTo(a); +// b.invert(); +// a.build(b.allPolygons()); +// +// Subtraction and intersection naturally follow from set operations. If +// union is `A | B`, subtraction is `A - B = ~(~A | B)` and intersection is +// `A & B = ~(~A | ~B)` where `~` is the complement operator. +// +// ## License +// +// Copyright (c) 2011 Evan Wallace (http://madebyevan.com/), under the MIT license. + +// # 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 = { + clone: function() { + var csg = new CSG(); + csg.polygons = this.polygons.map(function(p) { return p.clone(); }); + return csg; + }, + + toPolygons: function() { + return this.polygons; + }, + + // Return a new CSG solid representing space in either this solid or in the + // solid `csg`. Neither this solid nor the solid `csg` are modified. + // + // A.union(B) + // + // +-------+ +-------+ + // | | | | + // | A | | | + // | +--+----+ = | +----+ + // +----+--+ | +----+ | + // | B | | | + // | | | | + // +-------+ +-------+ + // + union: function(csg) { + var a = new CSG.Node(this.clone().polygons); + var b = new CSG.Node(csg.clone().polygons); + a.clipTo(b); + b.clipTo(a); + b.invert(); + b.clipTo(a); + b.invert(); + a.build(b.allPolygons()); + return CSG.fromPolygons(a.allPolygons()); + }, + + // Return a new CSG solid representing space in this solid but not in the + // solid `csg`. Neither this solid nor the solid `csg` are modified. + // + // A.subtract(B) + // + // +-------+ +-------+ + // | | | | + // | A | | | + // | +--+----+ = | +--+ + // +----+--+ | +----+ + // | B | + // | | + // +-------+ + // + subtract: function(csg) { + var a = new CSG.Node(this.clone().polygons); + var b = new CSG.Node(csg.clone().polygons); + a.invert(); + a.clipTo(b); + b.clipTo(a); + b.invert(); + b.clipTo(a); + b.invert(); + a.build(b.allPolygons()); + a.invert(); + return CSG.fromPolygons(a.allPolygons()); + }, + + // Return a new CSG solid representing space both this solid and in the + // solid `csg`. Neither this solid nor the solid `csg` are modified. + // + // A.intersect(B) + // + // +-------+ + // | | + // | A | + // | +--+----+ = +--+ + // +----+--+ | +--+ + // | B | + // | | + // +-------+ + // + intersect: function(csg) { + var a = new CSG.Node(this.clone().polygons); + var b = new CSG.Node(csg.clone().polygons); + a.invert(); + b.clipTo(a); + b.invert(); + a.clipTo(b); + b.clipTo(a); + a.build(b.allPolygons()); + a.invert(); + return CSG.fromPolygons(a.allPolygons()); + }, + + // Return a new CSG solid with solid and empty space switched. This solid is + // not modified. + inverse: function() { + var csg = this.clone(); + csg.polygons.map(function(p) { p.flip(); }); + return csg; + } +}; + +// Construct an axis-aligned solid cuboid. Optional parameters are `center` and +// `radius`, which default to `[0, 0, 0]` and `[1, 1, 1]`. The radius can be +// specified using a single number or a list of three numbers, one for each axis. +// +// Example code: +// +// var cube = CSG.cube({ +// center: [0, 0, 0], +// radius: 1 +// }); +CSG.cube = function(options) { + options = options || {}; + var c = new CSG.Vector(options.center || [0, 0, 0]); + var r = !options.radius ? [1, 1, 1] : options.radius.length ? + options.radius : [options.radius, options.radius, options.radius]; + 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) { + return new CSG.Polygon(info[0].map(function(i) { + var pos = new CSG.Vector( + c.x + r[0] * (2 * !!(i & 1) - 1), + c.y + r[1] * (2 * !!(i & 2) - 1), + c.z + r[2] * (2 * !!(i & 4) - 1) + ); + return new CSG.Vertex(pos, new CSG.Vector(info[1])); + })); + })); +}; + +// Construct a solid sphere. Optional parameters are `center`, `radius`, +// `slices`, and `stacks`, which default to `[0, 0, 0]`, `1`, `16`, and `8`. +// The `slices` and `stacks` parameters control the tessellation along the +// longitude and latitude directions. +// +// Example usage: +// +// var sphere = CSG.sphere({ +// center: [0, 0, 0], +// radius: 1, +// slices: 16, +// stacks: 8 +// }); +CSG.sphere = function(options) { + options = options || {}; + var c = new CSG.Vector(options.center || [0, 0, 0]); + var r = options.radius || 1; + var slices = options.slices || 16; + var stacks = options.stacks || 8; + var polygons = [], vertices; + function vertex(theta, phi) { + theta *= Math.PI * 2; + phi *= Math.PI; + var dir = new CSG.Vector( + Math.cos(theta) * Math.sin(phi), + Math.cos(phi), + Math.sin(theta) * Math.sin(phi) + ); + vertices.push(new CSG.Vertex(c.plus(dir.times(r)), dir)); + } + for (var i = 0; i < slices; i++) { + for (var j = 0; j < stacks; j++) { + vertices = []; + vertex(i / slices, j / stacks); + if (j > 0) vertex((i + 1) / slices, j / stacks); + if (j < stacks - 1) vertex((i + 1) / slices, (j + 1) / stacks); + vertex(i / slices, (j + 1) / stacks); + polygons.push(new CSG.Polygon(vertices)); + } + } + return CSG.fromPolygons(polygons); +}; + +// Construct a solid cylinder. Optional parameters are `start`, `end`, +// `radius`, and `slices`, which default to `[0, -1, 0]`, `[0, 1, 0]`, `1`, and +// `16`. The `slices` parameter controls the tessellation. +// +// Example usage: +// +// var cylinder = CSG.cylinder({ +// start: [0, -1, 0], +// end: [0, 1, 0], +// radius: 1, +// slices: 16 +// }); +CSG.cylinder = function(options) { + options = options || {}; + var s = new CSG.Vector(options.start || [0, -1, 0]); + var e = new CSG.Vector(options.end || [0, 1, 0]); + var ray = e.minus(s); + var r = options.radius || 1; + var slices = options.slices || 16; + var axisZ = ray.unit(), isY = (Math.abs(axisZ.y) > 0.5); + var axisX = new CSG.Vector(isY, !isY, 0).cross(axisZ).unit(); + var axisY = axisX.cross(axisZ).unit(); + var start = new CSG.Vertex(s, axisZ.negated()); + var end = new CSG.Vertex(e, axisZ.unit()); + 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, normal); + } + 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); +}; + +// # class Vector + +// Represents a 3D vector. +// +// Example usage: +// +// new CSG.Vector(1, 2, 3); +// new CSG.Vector([1, 2, 3]); +// new CSG.Vector({ x: 1, y: 2, z: 3 }); + +CSG.Vector = function(x, y, z) { + if (arguments.length == 3) { + this.x = x; + this.y = y; + this.z = z; + } else if ('x' in x) { + this.x = x.x; + this.y = x.y; + this.z = x.z; + } else { + this.x = x[0]; + this.y = x[1]; + this.z = x[2]; + } +}; + +CSG.Vector.prototype = { + clone: function() { + return new CSG.Vector(this.x, this.y, this.z); + }, + + negated: function() { + return new CSG.Vector(-this.x, -this.y, -this.z); + }, + + plus: function(a) { + return new CSG.Vector(this.x + a.x, this.y + a.y, this.z + a.z); + }, + + minus: function(a) { + return new CSG.Vector(this.x - a.x, this.y - a.y, this.z - a.z); + }, + + times: function(a) { + return new CSG.Vector(this.x * a, this.y * a, this.z * a); + }, + + dividedBy: function(a) { + return new CSG.Vector(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)); + }, + + length: function() { + return Math.sqrt(this.dot(this)); + }, + + unit: function() { + return this.dividedBy(this.length()); + }, + + cross: function(a) { + return new CSG.Vector( + this.y * a.z - this.z * a.y, + this.z * a.x - this.x * a.z, + this.x * a.y - this.y * a.x + ); + } +}; + +// # 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 and `clone()`, +// `flip()`, and `interpolate()` methods that behave analogous to the ones +// defined by `CSG.Vertex`. This class provides `normal` so convenience +// functions like `CSG.sphere()` can return a smooth vertex normal, but `normal` +// is not used anywhere else. + +CSG.Vertex = function(pos, normal) { + this.pos = new CSG.Vector(pos); + this.normal = new CSG.Vector(normal); +}; + +CSG.Vertex.prototype = { + clone: function() { + return new CSG.Vertex(this.pos.clone(), this.normal.clone()); + }, + + // Invert all orientation-specific data (e.g. vertex normal). Called when the + // orientation of a polygon is flipped. + flip: function() { + this.normal = this.normal.negated(); + }, + + // 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) { + return new CSG.Vertex( + this.pos.lerp(other.pos, t), + this.normal.lerp(other.normal, t) + ); + } +}; + +// # 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 = { + clone: function() { + return new CSG.Plane(this.normal.clone(), this.w); + }, + + flip: function() { + this.normal = this.normal.negated(); + this.w = -this.w; + }, + + // Split `polygon` by this plane if needed, then put the polygon or polygon + // fragments in the appropriate lists. Coplanar polygons go into either + // `coplanarFront` or `coplanarBack` depending on their orientation with + // respect to this plane. Polygons in front or in back of this plane go into + // either `front` or `back`. + splitPolygon: function(polygon, coplanarFront, coplanarBack, front, back) { + var COPLANAR = 0; + var FRONT = 1; + var BACK = 2; + var SPANNING = 3; + + // Classify each point as well as the entire polygon into one of the above + // four classes. + var polygonType = 0; + var types = []; + for (var i = 0; i < polygon.vertices.length; i++) { + var t = this.normal.dot(polygon.vertices[i].pos) - this.w; + var type = (t < -CSG.Plane.EPSILON) ? BACK : (t > CSG.Plane.EPSILON) ? FRONT : COPLANAR; + polygonType |= type; + types.push(type); + } + + // Put the polygon in the correct list, splitting it when necessary. + switch (polygonType) { + case COPLANAR: + (this.normal.dot(polygon.plane.normal) > 0 ? coplanarFront : coplanarBack).push(polygon); + break; + case FRONT: + front.push(polygon); + break; + case BACK: + back.push(polygon); + break; + case SPANNING: + var f = [], b = []; + for (var i = 0; i < polygon.vertices.length; i++) { + var j = (i + 1) % polygon.vertices.length; + var ti = types[i], tj = types[j]; + var vi = polygon.vertices[i], vj = polygon.vertices[j]; + if (ti != BACK) f.push(vi); + if (ti != FRONT) b.push(ti != BACK ? vi.clone() : vi); + if ((ti | tj) == SPANNING) { + var t = (this.w - this.normal.dot(vi.pos)) / this.normal.dot(vj.pos.minus(vi.pos)); + var v = vi.interpolate(vj, t); + f.push(v); + b.push(v.clone()); + } + } + if (f.length >= 3) front.push(new CSG.Polygon(f, polygon.shared)); + if (b.length >= 3) back.push(new CSG.Polygon(b, polygon.shared)); + break; + } + } +}; + +// # 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). + +CSG.Polygon = function(vertices, shared) { + this.vertices = vertices; + this.shared = shared; + this.plane = CSG.Plane.fromPoints(vertices[0].pos, vertices[1].pos, vertices[2].pos); +}; + +CSG.Polygon.prototype = { + clone: function() { + var vertices = this.vertices.map(function(v) { return v.clone(); }); + return new CSG.Polygon(vertices, this.shared); + }, + + flip: function() { + this.vertices.reverse().map(function(v) { v.flip(); }); + this.plane.flip(); + } +}; + +// # 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. That polygon (and all other coplanar +// polygons) are added directly to that node and the other polygons are added to +// the front and/or back subtrees. This is not a leafy BSP tree since there is +// no distinction between internal and leaf nodes. + +CSG.Node = function(polygons) { + this.plane = null; + this.front = null; + this.back = null; + this.polygons = []; + if (polygons) this.build(polygons); +}; + +CSG.Node.prototype = { + clone: function() { + var node = new CSG.Node(); + node.plane = this.plane && this.plane.clone(); + node.front = this.front && this.front.clone(); + node.back = this.back && this.back.clone(); + node.polygons = this.polygons.map(function(p) { return p.clone(); }); + return node; + }, + + // Convert solid space to empty space and empty space to solid space. + invert: function() { + for (var i = 0; i < this.polygons.length; i++) { + this.polygons[i].flip(); + } + this.plane.flip(); + if (this.front) this.front.invert(); + if (this.back) this.back.invert(); + var temp = this.front; + this.front = this.back; + this.back = temp; + }, + + // Recursively remove all polygons in `polygons` that are inside this BSP + // tree. + clipPolygons: function(polygons) { + if (!this.plane) return polygons.slice(); + var front = [], back = []; + for (var i = 0; i < polygons.length; i++) { + this.plane.splitPolygon(polygons[i], front, back, front, back); + } + if (this.front) front = this.front.clipPolygons(front); + if (this.back) back = this.back.clipPolygons(back); + else back = []; + return front.concat(back); + }, + + // Remove all polygons in this BSP tree that are inside the other BSP tree + // `bsp`. + clipTo: function(bsp) { + this.polygons = bsp.clipPolygons(this.polygons); + if (this.front) this.front.clipTo(bsp); + if (this.back) this.back.clipTo(bsp); + }, + + // Return a list of all polygons in this BSP tree. + allPolygons: function() { + var polygons = this.polygons.slice(); + if (this.front) polygons = polygons.concat(this.front.allPolygons()); + if (this.back) polygons = polygons.concat(this.back.allPolygons()); + return polygons; + }, + + // Build a BSP tree out of `polygons`. When called on an existing tree, the + // new polygons are filtered down to the bottom of the tree and become new + // nodes there. Each set of polygons is partitioned using the first polygon + // (no heuristic is used to pick a good split). + build: function(polygons) { + if (!polygons.length) return; + if (!this.plane) this.plane = polygons[0].plane.clone(); + var front = [], back = []; + for (var i = 0; i < polygons.length; i++) { + this.plane.splitPolygon(polygons[i], this.polygons, this.polygons, front, back); + } + if (front.length) { + if (!this.front) this.front = new CSG.Node(); + this.front.build(front); + } + if (back.length) { + if (!this.back) this.back = new CSG.Node(); + this.back.build(back); + } + } +};