import THREE from 'three.js'; import Paths from './paths.js'; import Slice from './slice.js'; import GCode from './gcode.js'; export default class { constructor () { this.progress = { createdLines: false, sliced: false, generatedInnerLines: false, generatedInfills: false, generatedSupport: false, optimizedPaths: false, generatedGCode: false }; } setMesh (mesh) { mesh.updateMatrix(); this.setGeometry(mesh.geometry, mesh.matrix); return this; } setGeometry (geometry, matrix) { //convert buffergeometry to geometry; if (geometry.type === 'BufferGeometry') { geometry = new THREE.Geometry().fromBufferGeometry(geometry); } else if (geometry.type === 'Geometry') { geometry = geometry.clone(); } else { console.warn('Geometry is not an instance of BufferGeometry or Geometry'); return; } if (matrix instanceof THREE.Matrix4) { geometry.applyMatrix(matrix); } geometry.computeBoundingBox(); geometry.computeFaceNormals(); geometry.mergeVertices(); this.geometry = geometry; return this; } slice (settings) { var supportEnabled = settings.config['supportEnabled']; //get unique lines from geometry; var lines = this._createLines(settings); var slices = this._slice(lines, settings); this._generateInnerLines(slices, settings); this._generateInfills(slices, settings); if (supportEnabled) { this._generateSupport(slices, settings); } this._optimizePaths(slices, settings); var gcode = this._slicesToGCode(slices, settings); if (this.onfinish !== undefined) { this.onfinish(gcode); } return gcode; } _createLines (settings) { console.log('constructing lines from geometry'); var lines = []; var lineLookup = {}; var addLine = (a, b) => { var index = lineLookup[b + '_' + a]; if (index === undefined) { index = lines.length; lineLookup[a + '_' + b] = index; lines.push({ line: new THREE.Line3(this.geometry.vertices[a], this.geometry.vertices[b]), connects: [], normals: [] }); } return index; } for (var face of this.geometry.faces) { if (face.normal.y !== 1 && face.normal.y !== -1) { var normal = new THREE.Vector2(face.normal.z, face.normal.x).normalize(); //check for only adding unique lines //returns index of said line var a = addLine(face.a, face.b); var b = addLine(face.b, face.c); var c = addLine(face.c, face.a); //set connecting lines (based on face) lines[a].connects.push(b, c); lines[b].connects.push(c, a); lines[c].connects.push(a, b); lines[a].normals.push(normal); lines[b].normals.push(normal); lines[c].normals.push(normal); } } this.progress.createdLines = true; this._updateProgress(settings); return lines; } _slice (lines, settings) { console.log("generating slices"); var layerHeight = settings.config["layerHeight"]; var height = settings.config["dimensionsZ"]; var numLayers = height / layerHeight; var layersIntersections = []; for (var layer = 0; layer < numLayers; layer ++) { layersIntersections[layer] = []; } for (var lineIndex = 0; lineIndex < lines.length; lineIndex ++) { var line = lines[lineIndex].line; var min = Math.ceil(Math.min(line.start.y, line.end.y) / layerHeight); var max = Math.floor(Math.max(line.start.y, line.end.y) / layerHeight); for (var layerIndex = min; layerIndex <= max; layerIndex ++) { if (layerIndex >= 0 && layerIndex < numLayers) { layersIntersections[layerIndex].push(lineIndex); } } } var slices = []; for (var layer = 1; layer < layersIntersections.length; layer ++) { var layerIntersections = layersIntersections[layer]; if (layerIntersections.length > 0) { var y = layer * layerHeight; var intersections = []; for (var index of layerIntersections) { var line = lines[index].line; if (line.start.y === line.end.y) { var x = line.start.x; var z = line.start.z; } else { var alpha = (y - line.start.y) / (line.end.y - line.start.y); var x = line.end.x * alpha + line.start.x * (1 - alpha); var z = line.end.z * alpha + line.start.z * (1 - alpha); } intersections[index] = new THREE.Vector2(z, x); } var done = []; var sliceParts = []; for (var index of layerIntersections) { var firstPoint = index; var closed = false; if (done.indexOf(index) === -1) { var shape = []; while (index !== -1) { done.push(index); var intersection = intersections[index]; //uppercase X and Y because clipper vector shape.push({X: intersection.x, Y: intersection.y}); var connects = lines[index].connects.map((value) => value); var faceNormals = lines[index].normals.map((value) => value); for (var i = 0; i < connects.length; i ++) { var index = connects[i]; if (shape.length > 2 && index === firstPoint) { closed = true; break; } //check if index is already used if (done.indexOf(index) === -1) { //check if index has an intersection if (intersections[index] !== undefined) { var faceNormal = faceNormals[Math.floor(i / 2)]; var a = new THREE.Vector2(intersection.x, intersection.y); var b = new THREE.Vector2(intersections[index].x, intersections[index].y); if (a.distanceTo(b) < 0.0001 || (faceNormal.x === 0 && faceNormal.y === 0)) { done.push(index); connects = connects.concat(lines[index].connects); faceNormals = faceNormals.concat(lines[index].normals); index = -1; } else { // THREE.Vector2.normal is not yet implimented // var normal = a.sub(b).normal().normalize(); var normal = a.sub(b); normal.set(-normal.y, normal.x).normalize(); if (normal.dot(faceNormal) > 0) { break; } else { index = -1; } } } else { done.push(index); index = -1; } } else { index = -1; } } } if (!closed) { var index = firstPoint; while (index !== -1) { if (index !== firstPoint) { done.push(index); var intersection = intersections[index]; // PERFORMACE // maybe performance can be increased by unshifting to sepperate // array and to later concat the original en the new array shape.unshift({X: intersection.x, Y: intersection.y}); } var connects = lines[index].connects; for (var index of connects) { index = connects[j]; if (done.indexOf(index) === -1) { if (intersections[index] !== undefined) { break; } else { done.push(index); index = -1; } } else { index = -1; } } } } if (shape.length > 0) { var part = new Paths([shape], closed).clean(0.01); sliceParts.push(part); } } } sliceParts.sort(function (a, b) { return b.area() - a.area(); }); var slice = new Slice(); for (var i = 0; i < sliceParts.length; i ++) { var slicePart1 = sliceParts[i]; if (slicePart1.closed) { var merge = false; for (var j = 0; j < slice.parts.length; j ++) { var slicePart2 = slice.parts[j].intersect; if (slicePart2.closed && slicePart2.intersect(slicePart1).length > 0) { slicePart2.join(slicePart1); merge = true; break; } } if (!merge) { slice.add(slicePart1); } } else { slice.add(slicePart1); } } slices.push(slice); } } this.progress.sliced = true; this._updateProgress(settings); return slices; } _generateInnerLines (slices, settings) { console.log("generating outer lines and inner lines"); //need to scale up everything because of clipper rounding errors var scale = 100; var layerHeight = settings.config["layerHeight"]; var nozzleDiameter = settings.config["nozzleDiameter"] * scale; var shellThickness = settings.config["shellThickness"] * scale; var nozzleRadius = nozzleDiameter / 2; for (var layer = 0; layer < slices.length; layer ++) { var slice = slices[layer]; for (var i = 0; i < slice.parts.length; i ++) { var part = slice.parts[i]; if (!part.intersect.closed) { continue; } //var outerLine = part.intersect.clone().scaleUp(scale).offset(-nozzleRadius); var outerLine = part.intersect.scaleUp(scale).offset(-nozzleRadius); if (outerLine.length > 0) { part.outerLine = outerLine; for (var offset = nozzleDiameter; offset <= shellThickness; offset += nozzleDiameter) { var innerLine = outerLine.offset(-offset); if (innerLine.length > 0) { part.innerLines.push(innerLine); } else { break; } } } } } this.progress.generatedInnerLines = true; this._updateProgress(settings); } _generateInfills (slices, settings) { console.log("generating infills"); //need to scale up everything because of clipper rounding errors var scale = 100; var layerHeight = settings.config["layerHeight"]; var fillGridSize = settings.config["fillGridSize"] * scale; var bottomThickness = settings.config["bottomThickness"]; var topThickness = settings.config["topThickness"]; var nozzleDiameter = settings.config["nozzleDiameter"] * scale; var infillOverlap = settings.config["infillOverlap"] * scale; var bottomSkinCount = Math.ceil(bottomThickness/layerHeight); var topSkinCount = Math.ceil(topThickness/layerHeight); var nozzleRadius = nozzleDiameter / 2; var hightemplateSize = Math.sqrt(2 * Math.pow(nozzleDiameter, 2)); for (var layer = 0; layer < slices.length; layer ++) { var slice = slices[layer]; if (layer - bottomSkinCount >= 0 && layer + topSkinCount < slices.length) { var downSkin = slices[layer - bottomSkinCount].getOutline(); var upSkin = slices[layer + topSkinCount].getOutline(); var surroundingLayer = upSkin.intersect(downSkin); } else { var surroundingLayer = false; } for (var i = 0; i < slice.parts.length; i ++) { var part = slice.parts[i]; if (!part.intersect.closed) { continue; } var outerLine = part.outerLine; if (outerLine.length > 0) { var inset = (part.innerLines.length > 0) ? part.innerLines[part.innerLines.length - 1] : outerLine; var fillArea = inset.offset(-nozzleRadius); if (surroundingLayer) { var highFillArea = fillArea.difference(surroundingLayer); if (infillOverlap > 0) { highFillArea = highFillArea.offset(infillOverlap); } highFillArea = highFillArea.intersect(fillArea); var lowFillArea = fillArea.difference(highFillArea); } else { var highFillArea = fillArea; } var fill = new Paths([], false); if (lowFillArea !== undefined && lowFillArea.length > 0) { var bounds = lowFillArea.bounds(); var lowFillTemplate = this._getFillTemplate(bounds, fillGridSize, true, true); part.fill.join(lowFillTemplate.intersect(lowFillArea)); } if (highFillArea.length > 0) { var bounds = highFillArea.bounds(); var even = (layer % 2 === 0); var highFillTemplate = this._getFillTemplate(bounds, hightemplateSize, even, !even); part.fill.join(highFillTemplate.intersect(highFillArea)); } } } } this.progress.generatedInfills = true; this._updateProgress(settings); } _generateSupport (slices, settings) { console.log("generating support"); //need to scale up everything because of clipper rounding errors var scale = 100; var layerHeight = settings.config["layerHeight"]; var supportGridSize = settings.config["supportGridSize"] * scale; var supportAcceptanceMargin = settings.config["supportAcceptanceMargin"] * scale; var supportMargin = settings.config["supportMargin"] * scale; var plateSize = settings.config["supportPlateSize"] * scale; var supportDistanceY = settings.config["supportDistanceY"]; var supportDistanceLayers = Math.ceil(supportDistanceY / layerHeight); var nozzleDiameter = settings.config["nozzleDiameter"] * scale; var supportAreas = new Paths([], true); for (var layer = slices.length - 1 - supportDistanceLayers; layer >= 0; layer --) { if (supportAreas.length > 0) { if (layer >= supportDistanceLayers) { //var sliceSkin = slices[layer - supportDistanceLayers].getOutline(); var sliceSkin = slices[layer].getOutline(); sliceSkin = sliceSkin.offset(supportMargin); supportAreas = supportAreas.difference(sliceSkin); } var currentSlice = slices[layer]; if (layer === 0) { supportAreas = supportAreas.offset(plateSize).difference(sliceSkin); var template = this._getFillTemplate(supportAreas.bounds(), nozzleDiameter, true, false); currentSlice.support = template.intersect(supportAreas); } else { var supportTemplate = this._getFillTemplate(supportAreas.bounds(), supportGridSize, true, true); currentSlice.support = supportTemplate.intersect(supportAreas).join(supportAreas.clone()); } } var supportSkin = slices[layer + supportDistanceLayers - 1].getOutline(); var slice = slices[layer + supportDistanceLayers]; for (var i = 0; i < slice.parts.length; i ++) { var slicePart = slice.parts[i]; var outerLine = slicePart.outerLine; var overlap = supportSkin.offset(supportAcceptanceMargin).intersect(outerLine); var overhang = outerLine.difference(overlap); if (overlap.length === 0 || overhang.length > 0) { supportAreas = supportAreas.union(overhang.offset(supportAcceptanceMargin).intersect(outerLine)); } } } this.progress.generatedSupport = true; this._updateProgress(settings); } _optimizePaths (slices, settings) { console.log("opimize paths"); //need to scale up everything because of clipper rounding errors var scale = 100; var brimOffset = settings.config["brimOffset"] * scale; var start = new THREE.Vector2(0, 0); for (var layer = 0; layer < slices.length; layer ++) { var slice = slices[layer]; if (layer === 0) { slice.brim = slice.getOutline().offset(brimOffset); } start = slice.optimizePaths(start); for (var i = 0; i < slice.parts.length; i ++) { var part = slice.parts[i]; if (part.intersect.closed) { part.outerLine.scaleDown(scale); for (var j = 0; j < part.innerLines.length; j ++) { var innerLine = part.innerLines[j]; innerLine.scaleDown(scale); } part.fill.scaleDown(scale); } } if (slice.support !== undefined) { slice.support.scaleDown(scale); } if (slice.brim !== undefined) { slice.brim.scaleDown(scale); } } this.progress.optimizedPaths = true; this._updateProgress(settings); } _getFillTemplate (bounds, size, even, uneven) { var paths = new Paths([], false); var left = Math.floor(bounds.left / size) * size; var right = Math.ceil(bounds.right / size) * size; var top = Math.floor(bounds.top / size) * size; var bottom = Math.floor(bounds.bottom / size) * size; var width = right - left; if (even) { for (var y = top; y <= bottom + width; y += size) { paths.push([ {X: left, Y: y}, {X: right, Y: y - width} ]); } } if (uneven) { for (var y = top - width; y <= bottom; y += size) { paths.push([ {X: left, Y: y}, {X: right, Y: y + width} ]); } } return paths; } _slicesToGCode (slices, settings) { var gcode = new GCode().setSettings(settings); function pathToGCode (path, retract, unRetract, type) { for (var i = 0; i < path.length; i ++) { var shape = path[i]; var length = path.closed ? (shape.length + 1) : shape.length; for (var j = 0; j < length; j ++) { var point = shape[j % shape.length]; if (j === 0) { //TODO //moveTo should impliment combing gcode.moveTo(point.X, point.Y, layer); if (unRetract) { gcode.unRetract(); } } else { gcode.lineTo(point.X, point.Y, layer, type); } } } if (retract) { gcode.retract(); } } for (var layer = 0; layer < slices.length; layer ++) { var slice = slices[layer]; if (layer === 1) { gcode.turnFanOn(); gcode.bottom = false; } if (slice.brim !== undefined) { pathToGCode(slice.brim, true, true, "brim"); } for (var i = 0; i < slice.parts.length; i ++) { var part = slice.parts[i]; if (part.intersect.closed) { pathToGCode(part.outerLine, false, true, "outerLine"); for (var j = 0; j < part.innerLines.length; j ++) { var innerLine = part.innerLines[j]; pathToGCode(innerLine, false, false, "innerLine"); } pathToGCode(part.fill, true, false, "fill"); } else { var retract = !(slice.parts.length === 1 && slice.support === undefined); pathToGCode(part.intersect, retract, retract, "outerLine"); } } if (slice.support !== undefined) { pathToGCode(slice.support, true, true, "support"); } } this.progress.generatedGCode = true; this._updateProgress(settings); return gcode.getGCode(); } _updateProgress (settings) { if (this.onprogress !== undefined) { var supportEnabled = settings.config["supportEnabled"]; var progress = {}; var procent = 0; var length = 0; for (var i in this.progress) { if (!(!supportEnabled && i === "generatedSupport")) { progress[i] = this.progress[i]; if (progress[i]) { procent += 1; } length += 1; } } progress.procent = procent / length; this.onprogress(progress); } } }