Permalink
| // namespace | |
| var SLAcer = SLAcer || {}; | |
| ;(function() { | |
| // global settings | |
| var globalSettings = { | |
| color: 0xffff00 | |
| }; | |
| // ------------------------------------------------------------------------- | |
| function Point(p) { | |
| this.x = p.x; | |
| this.y = p.y; | |
| this.s = p.x + ':' + p.y; | |
| } | |
| function Line(p1, p2) { | |
| this.p1 = new Point(p1); | |
| this.p2 = new Point(p2); | |
| } | |
| function isSameValue(v1, v2) { | |
| return Math.abs(v1 - v2) <= Number.EPSILON; | |
| } | |
| function isSamePoint(p1, p2) { | |
| return isSameValue(p1.x, p2.x) && isSameValue(p1.y, p2.y); | |
| } | |
| function linesToPolygons(lines) { | |
| var polygons = []; | |
| var polygon = []; | |
| var firstLine, lastPoint, i, line; | |
| function getNextPoint() { | |
| var found = false; | |
| for (i = 0; i < lines.length; i++) { | |
| line = lines[i]; | |
| if (isSamePoint(lastPoint, line.p1)) { | |
| lines.splice(i, 1); | |
| if (isSamePoint(firstLine.p1, line.p2)) { | |
| //console.log('closed loop'); | |
| break; | |
| } | |
| polygon.push(line.p2); | |
| lastPoint = line.p2; | |
| found = true; | |
| } | |
| else if (isSamePoint(lastPoint, line.p2)) { | |
| lines.splice(i, 1); | |
| if (isSamePoint(firstLine.p1, line.p1)) { | |
| //console.log('closed loop'); | |
| break; | |
| } | |
| polygon.push(line.p1); | |
| lastPoint = line.p1; | |
| found = true; | |
| } | |
| } | |
| return found; | |
| } | |
| while (lines.length) { | |
| firstLine = lines.shift(); | |
| lastPoint = firstLine.p2; | |
| polygon.push(firstLine.p1); | |
| polygon.push(firstLine.p2); | |
| while (getNextPoint()) {} | |
| polygons.push(polygon); | |
| polygon = []; | |
| } | |
| return polygons; | |
| } | |
| function pointInPolygon(point, polygon) { | |
| // ray-casting algorithm based on | |
| // https://github.com/substack/point-in-polygon/blob/master/index.js | |
| // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html | |
| var inside = false; | |
| var il, j, pi, pj, intersect; | |
| for (i = 0, il = polygon.length, j = il - 1; i < il; j = i++) { | |
| pi = polygon[i]; | |
| pj = polygon[j]; | |
| (((pi.y > point.y) != (pj.y > point.y)) | |
| && (point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x)) | |
| && (inside = !inside); | |
| } | |
| return inside; | |
| }; | |
| function makeNodes(polygons) { | |
| // single polygon... | |
| if (polygons.length == 1) { | |
| return { 0: { parents: [], isHole: false } }; | |
| } | |
| // nodes collection | |
| var nodes = {}; | |
| // variables | |
| var i, il, point, y, yl; | |
| // for each polygon extract parents and childs polygons | |
| for (i = 0, il = polygons.length; i < il; i++) { | |
| // only check for first point in polygon | |
| point = polygons[i][0]; | |
| // not enough faces ( !!! investigation required !!!) | |
| if (polygons[i].length < 3) { | |
| //console.log('--------------------'); | |
| //console.log(il, i, polygons[i]); | |
| continue; | |
| } | |
| // for each polygons | |
| for (y = 0, yl = il; y < yl; y++) { | |
| // do not check self intersection | |
| if (i == y) continue; | |
| // create default node | |
| nodes[i] || (nodes[i] = { parents: [], isHole: false }); | |
| nodes[y] || (nodes[y] = { parents: [], isHole: false }); | |
| // check if point in poylgon | |
| if (pointInPolygon(point, polygons[y])) { | |
| // push parent and child | |
| nodes[i].parents.push(y); | |
| // odd parents number ==> hole | |
| nodes[i].isHole = !! (nodes[i].parents.length % 2); | |
| nodes[y].isHole = !! (nodes[y].parents.length % 2); | |
| } | |
| } | |
| } | |
| // return nodes collection | |
| return nodes; | |
| } | |
| function polygonsToShapes(polygons) { | |
| // shapes collection | |
| var shapes = []; | |
| // make the nodes collection | |
| var nodes = makeNodes(polygons); | |
| //console.log('nodes:', nodes); | |
| // variables | |
| var key, node, i, il, parentKey; | |
| // make base collection | |
| for (key in nodes) { | |
| node = nodes[key]; | |
| if (! node.isHole) { | |
| shapes[key] = new THREE.Shape(polygons[key]); | |
| } | |
| } | |
| // push holes | |
| for (key in nodes) { | |
| node = nodes[key]; | |
| if (node.isHole) { | |
| for (i = 0, il = node.parents.length; i < il; i++) { | |
| parentKey = node.parents[i]; | |
| if ((il - 1) == nodes[parentKey].parents.length) { | |
| shapes[parentKey].holes.push(new THREE.Path(polygons[key])); | |
| } | |
| } | |
| } | |
| } | |
| // return shapes collection | |
| return shapes; | |
| } | |
| // ------------------------------------------------------------------------- | |
| // Constructor | |
| function Slicer(settings) { | |
| // settings | |
| this.settings = _.defaults({}, settings || {}, Slicer.globalSettings); | |
| // faces collection | |
| this.faces = []; | |
| // position | |
| this.zHeight = 0; | |
| this.zOffset = 0; | |
| // plane | |
| this.mesh = null; | |
| this.plane = null; | |
| this.slice = null; | |
| } | |
| // ------------------------------------------------------------------------- | |
| Slicer.prototype.loadMesh = function(mesh) { | |
| // mesh | |
| this.mesh = mesh; | |
| // slice | |
| this.slice = mesh.clone(); | |
| // bounding box | |
| var box = mesh.geometry.boundingBox.clone(); | |
| // mesh size | |
| var size = box.size(); | |
| // z height | |
| this.zHeight = size.z; | |
| this.zOffset = box.min.z; | |
| // min/max faces (z) | |
| this.facesMinMax = []; | |
| var i, length, face, v1, v2, v3; | |
| var geometry = this.slice.geometry; | |
| for (i = 0, length = geometry.faces.length; i < length; i++) { | |
| face = geometry.faces[i]; | |
| v1 = geometry.vertices[face.a]; | |
| v2 = geometry.vertices[face.b]; | |
| v3 = geometry.vertices[face.c]; | |
| this.facesMinMax.push({ | |
| min: Math.min(v1.z, v2.z, v3.z), | |
| max: Math.max(v1.z, v2.z, v3.z) | |
| }); | |
| } | |
| // plane | |
| this.plane = new SLAcer.Mesh( | |
| new THREE.PlaneGeometry(size.x, size.y, 1), | |
| new THREE.MeshBasicMaterial({ | |
| color: this.settings.color, side: THREE.DoubleSide | |
| }) | |
| ); | |
| }; | |
| Slicer.prototype.getFaces = function(zPosition) { | |
| var time = Date.now(); | |
| zPosition += this.zOffset; | |
| var source = this.slice.geometry; | |
| var geometry = new THREE.Geometry(); | |
| var plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), -zPosition); | |
| var i, length, minMax, face, vertices, v1, v2, v3, index, normal; | |
| var lines = []; | |
| var line, top, bot; | |
| function addLine(p1, p2) { | |
| lines.push(new Line(p1, p2)); | |
| } | |
| for (i = 0, length = source.faces.length; i < length; i++) { | |
| minMax = this.facesMinMax[i]; | |
| if (minMax.min <= zPosition && minMax.max >= zPosition) { | |
| face = source.faces[i]; | |
| vertices = source.vertices; | |
| v1 = vertices[face.a].clone(); | |
| v2 = vertices[face.b].clone(); | |
| v3 = vertices[face.c].clone(); | |
| geometry.vertices.push(v1, v2, v3); | |
| index = geometry.vertices.length; | |
| normal = face.normal.clone(); | |
| geometry.faces.push(new THREE.Face3( | |
| index-3, index-2, index-1, normal | |
| )); | |
| // slice... | |
| t1 = isSameValue(v1.z, zPosition); | |
| t2 = isSameValue(v2.z, zPosition); | |
| t3 = isSameValue(v3.z, zPosition); | |
| touch = 0; | |
| t1 && touch++; | |
| t2 && touch++; | |
| t3 && touch++; | |
| // all points on plane | |
| if (touch == 3) { | |
| // skip since is shared with two points case | |
| continue; | |
| } | |
| // two points on plane | |
| if (touch == 2) { | |
| if (t1 && t2 && !t3) addLine(v1, v2); | |
| else if (!t1 && t2 && t3) addLine(v2, v3); | |
| else addLine(v3, v1); | |
| continue; | |
| } | |
| // one point on plane | |
| if (touch == 1) { | |
| // test if faces intersect the plane | |
| if (t1 && ((v2.z > zPosition && v3.z < zPosition) || (v2.z < zPosition && v3.z > zPosition))) { | |
| addLine(v1, plane.intersectLine(new THREE.Line3(v2, v3))); | |
| } | |
| else if (t2 && ((v3.z > zPosition && v1.z < zPosition) || (v3.z < zPosition && v1.z > zPosition))) { | |
| addLine(v2, plane.intersectLine(new THREE.Line3(v3, v1))); | |
| } | |
| else if (t3 && ((v1.z > zPosition && v2.z < zPosition) || (v1.z < zPosition && v2.z > zPosition))) { | |
| addLine(v3, plane.intersectLine(new THREE.Line3(v1, v2))); | |
| } | |
| // no intersection! | |
| // skip since is shared with two points case | |
| continue; | |
| } | |
| // no points on plane (need intersection) | |
| if (touch == 0) { | |
| top = []; | |
| bot = []; | |
| v1.z > zPosition && top.push(v1) || bot.push(v1); | |
| v2.z > zPosition && top.push(v2) || bot.push(v2); | |
| v3.z > zPosition && top.push(v3) || bot.push(v3); | |
| if (top.length == 1) { | |
| addLine( | |
| plane.intersectLine(new THREE.Line3(top[0], bot[0])), | |
| plane.intersectLine(new THREE.Line3(top[0], bot[1])) | |
| ); | |
| } | |
| else { | |
| addLine( | |
| plane.intersectLine(new THREE.Line3(top[0], bot[0])), | |
| plane.intersectLine(new THREE.Line3(top[1], bot[0])) | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| var polygons = linesToPolygons(lines); | |
| var shapes = polygonsToShapes(polygons); | |
| var meshes = []; | |
| for (key in shapes) { | |
| try { | |
| var color = this.settings.color; | |
| //var color = ((1<<24)*Math.random()|0); | |
| var geo = new THREE.ShapeGeometry(shapes[key]); | |
| if (!geo.faces.length || !geo.vertices.length) { | |
| delete shapes[key]; | |
| continue; | |
| } | |
| meshes.push(new THREE.Mesh( | |
| geo, | |
| new THREE.MeshBasicMaterial({ | |
| color: color, side: THREE.DoubleSide | |
| }) | |
| )); | |
| } | |
| catch(e) { | |
| console.error(e); | |
| console.log(shapes[key]); | |
| } | |
| } | |
| // remove empty shapes... | |
| shapes = shapes.filter(function(n){ return n != undefined }); | |
| return { | |
| time : Date.now() - time, | |
| geometry: geometry, | |
| polygons: polygons, | |
| shapes : shapes, | |
| meshes : meshes | |
| }; | |
| }; | |
| // ------------------------------------------------------------------------- | |
| // global settings | |
| Slicer.globalSettings = globalSettings; | |
| // export module | |
| SLAcer.Slicer = Slicer; | |
| })(); |