diff --git a/src/octoprint/plugins/svgtogcode/__init__.py b/src/octoprint/plugins/svgtogcode/__init__.py index 87b88767..a76f61ed 100644 --- a/src/octoprint/plugins/svgtogcode/__init__.py +++ b/src/octoprint/plugins/svgtogcode/__init__.py @@ -143,7 +143,7 @@ class SvgToGcodePlugin(octoprint.plugin.SlicerPlugin, def get_assets(self): return { - "js": [ "js/convert.js", "js/working_area.js", "js/lib/snap.svg-min.js"], + "js": [ "js/convert.js", "js/working_area.js", "js/gcode_parser.js", "js/lib/snap.svg-min.js"], "less": ["less/svgtogcode.less"], "css": ["css/svgtogcode.css", "css/mrbeam.css"] } diff --git a/src/octoprint/plugins/svgtogcode/static/js/convert.js b/src/octoprint/plugins/svgtogcode/static/js/convert.js index e8d12dc4..76dacbaa 100644 --- a/src/octoprint/plugins/svgtogcode/static/js/convert.js +++ b/src/octoprint/plugins/svgtogcode/static/js/convert.js @@ -48,12 +48,13 @@ $(function(){ self.laserSpeed(speed); self.svg = self.workingArea.getCompositionSVG(); + self.gcodeFilesToAppend = self.workingArea.getPlacedGcodes(); // TODO: js svg conversion self.title(gettext("Converting")); var gcodeFile = self.create_gcode_filename(self.workingArea.placedDesigns()); self.gcodeFilename(gcodeFile); - $("#dialog_vector_graphics_conversion").modal("show"); + $("#dialog_vector_graphics_conversion").modal("show"); // calls self.convert afterwards }; self.create_gcode_filename = function(placedDesigns){ @@ -61,9 +62,8 @@ $(function(){ var filemap = {}; for(var idx in placedDesigns){ var design = placedDesigns[idx]; - var start = design.url.lastIndexOf('/')+1; - var end = design.url.lastIndexOf('.'); - var name = design.url.substring(start, end); + var end = design.name.lastIndexOf('.'); + var name = design.name.substring(0, end); if(filemap[name] !== undefined) filemap[name] += 1; else filemap[name] = 1; } @@ -83,7 +83,9 @@ $(function(){ } return gcode_name + ".gco"; } else { - return "tmp"+Date.now()+".gco"; // TODO: user should not deal with gcode anymore. go and laser it. +// return "tmp"+Date.now()+".gco"; + console.error("no designs placed."); + return; } }; @@ -194,6 +196,9 @@ $(function(){ if(self.svg !== undefined){ data.svg = self.svg; } + if(self.gcodeFilesToAppend !== undefined){ + data.gcodeFilesToAppend = self.gcodeFilesToAppend; + } $.ajax({ url: API_BASEURL + "files/convert", diff --git a/src/octoprint/plugins/svgtogcode/static/js/gcode_parser.js b/src/octoprint/plugins/svgtogcode/static/js/gcode_parser.js new file mode 100644 index 00000000..1185355c --- /dev/null +++ b/src/octoprint/plugins/svgtogcode/static/js/gcode_parser.js @@ -0,0 +1,532 @@ +$(function() { + gcParser = function() { + self = this; + + self.toolOffsets = [{x: 0, y: 0}]; + + self.parse = function(gcode, blockDelimiter, blockCallback ) { + var argChar, numSlice; + + var x, y, z, pi, pj, pp = 0; + var clockwise = false; + var laser = 0; + var prevX = 0, prevY = 0, prevZ = -1; + var f, lastF = 4000; + var extrude = false, extrudeRelative = false, retract = 0; + var positionRelative = false; + + var dcExtrude = false; + var assumeNonDC = false; + + var tool = 0; + var prev_extrude = [{a: 0, b: 0, c: 0, e: 0, abs: 0}]; + var prev_retract = [0]; + var offset = self.toolOffsets[0]; + + var gcode_lines = gcode.split(/\n/); + + var model = []; + for (var i = 0; i < gcode_lines.length; i++) { + x = undefined; + y = undefined; + z = undefined; + pi = undefined; + pj = undefined; + pp = undefined; + clockwise = false; + retract = 0; + + extrude = false; + var line = gcode_lines[i].split(/[\(;]/)[0]; + + var addToModel = false; + var convertAndAddToModel = false; + var move = false; + + + if (/^(?:G0|G00|G1|G01)\s+/i.test(line)) { + var args = line.split(/\s+/); + + for (var j = 0; j < args.length; j++) { + switch (argChar = args[j].charAt(0).toLowerCase()) { + case 'x': + if (positionRelative) { + x = prevX + Number(args[j].slice(1)) + offset.x; + } else { + x = Number(args[j].slice(1)) + offset.x; + } + + break; + + case 'y': + if (positionRelative) { + y = prevY + Number(args[j].slice(1)) + offset.y; + } else { + y = Number(args[j].slice(1)) + offset.y; + } + + break; + + case 'z': + if (positionRelative) { + z = prevZ + Number(args[j].slice(1)); + } else { + z = Number(args[j].slice(1)); + } + + break; + + case 'e': + case 'a': + case 'b': + case 'c': + assumeNonDC = true; + numSlice = Number(args[j].slice(1)); + + if (!extrudeRelative) { + // absolute extrusion positioning + prev_extrude[tool]["abs"] = numSlice - prev_extrude[tool][argChar]; + prev_extrude[tool][argChar] = numSlice; + } else { + prev_extrude[tool]["abs"] = numSlice; + prev_extrude[tool][argChar] += numSlice; + } + + extrude = prev_extrude[tool]["abs"] > 0; + if (prev_extrude[tool]["abs"] < 0) { + prev_retract[tool] = -1; + retract = -1; + } else if (prev_extrude[tool]["abs"] === 0) { + retract = 0; + } else if (prev_extrude[tool]["abs"] > 0 && prev_retract[tool] < 0) { + prev_retract[tool] = 0; + retract = 1; + } else { + retract = 0; + } + + break; + + case 'f': + numSlice = parseFloat(args[j].slice(1)); + lastF = numSlice; + break; + } + } + + if (dcExtrude && !assumeNonDC) { + extrude = true; + prev_extrude[tool]["abs"] = Math.sqrt((prevX - x) * (prevX - x) + (prevY - y) * (prevY - y)); + } + + if (typeof (x) !== 'undefined' || typeof (y) !== 'undefined' || typeof (z) !== 'undefined' || retract !== 0) { + addToModel = true; + move = true; + } + } else if (/^(?:G2|G02|G3|G03)\s+/i.test(line)) { + var units = "G21"; // mm + var args = line.split(/\s+/); + var lastPos = {x: prevX, y: prevY, z: prevZ}; + + clockwise = /^(?:G2|G02)/i.test(args[0]); + for (var j = 0; j < args.length; j++) { + switch (argChar = args[j].charAt(0).toLowerCase()) { + case 'x': + if (positionRelative) { + x = prevX + Number(args[j].slice(1)) + offset.x; + } else { + x = Number(args[j].slice(1)) + offset.x; + } + + break; + + case 'y': + if (positionRelative) { + y = prevY + Number(args[j].slice(1)) + offset.y; + } else { + y = Number(args[j].slice(1)) + offset.y; + } + + break; + + case 'z': + if (positionRelative) { + z = prevZ + Number(args[j].slice(1)); + } else { + z = Number(args[j].slice(1)); + } + + break; + + case 'i': + pi = Number(args[j].slice(1)) + offset.x; + + break; + + case 'j': + pj = Number(args[j].slice(1)) + offset.y; + + break; + + case 'p': + pp = Number(args[j].slice(1)); + + break; + + case 'e': + case 'a': + case 'b': + case 'c': + assumeNonDC = true; + numSlice = Number(args[j].slice(1)); + + if (!extrudeRelative) { + // absolute extrusion positioning + prev_extrude[tool]["abs"] = numSlice - prev_extrude[tool][argChar]; + prev_extrude[tool][argChar] = numSlice; + } else { + prev_extrude[tool]["abs"] = numSlice; + prev_extrude[tool][argChar] += numSlice; + } + + extrude = prev_extrude[tool]["abs"] > 0; + if (prev_extrude[tool]["abs"] < 0) { + prev_retract[tool] = -1; + retract = -1; + } else if (prev_extrude[tool]["abs"] === 0) { + retract = 0; + } else if (prev_extrude[tool]["abs"] > 0 && prev_retract[tool] < 0) { + prev_retract[tool] = 0; + retract = 1; + } else { + retract = 0; + } + + break; + + case 'f': + numSlice = parseFloat(args[j].slice(1)); + lastF = numSlice; + break; + } + } + + if (dcExtrude && !assumeNonDC) { + extrude = true; + prev_extrude[tool]["abs"] = Math.sqrt((prevX - x) * (prevX - x) + (prevY - y) * (prevY - y)); + } + + if (typeof (x) !== 'undefined' || typeof (y) !== 'undefined' || typeof (z) !== 'undefined' + || typeof (pi) !== 'undefined' || typeof (pj) !== 'undefined' || typeof (pp) !== 'undefined' || retract !== 0) { + + convertAndAddToModel = true; + move = true; + } +// } else if (/^(?:M82)/i.test(line)) { +// extrudeRelative = false; + } else if (/^(?:M3|M03)/i.test(line)) { + var args = line.split(/\s+/); + for (var j = 0; j < args.length; j++) { + switch (argChar = args[j].charAt(0).toLowerCase()) { + case 's': + laser = Number(args[j].slice(1)); + break; + } + } + } else if (/^(?:M5|M05)/i.test(line)) { + laser = 0; + } else if (/^(?:G91)/i.test(line)) { + positionRelative = true; + extrudeRelative = true; + } else if (/^(?:G90)/i.test(line)) { + positionRelative = false; + extrudeRelative = false; +// } else if (/^(?:M83)/i.test(line)) { +// extrudeRelative = true; +// } else if (/^(?:M101)/i.test(line)) { +// dcExtrude = true; +// } else if (/^(?:M103)/i.test(line)) { +// dcExtrude = false; + } else if (/^(?:G92)/i.test(line)) { + var args = line.split(/\s/); + + for (var j = 0; j < args.length; j++) { + if (!args[j]) + continue; + + if (args.length === 1) { + // G92 without coordinates => reset all axes to 0 + x = 0; + y = 0; + z = 0; + prev_extrude[tool]["e"] = 0; + prev_extrude[tool]["a"] = 0; + prev_extrude[tool]["b"] = 0; + prev_extrude[tool]["c"] = 0; + } else { + switch (argChar = args[j].charAt(0).toLowerCase()) { + case 'x': + x = Number(args[j].slice(1)) + offset.x; + break; + + case 'y': + y = Number(args[j].slice(1)) + offset.y; + break; + + case 'z': + z = Number(args[j].slice(1)); + prevZ = z; + break; + + case 'e': + case 'a': + case 'b': + case 'c': + numSlice = Number(args[j].slice(1)); + if (!extrudeRelative) + prev_extrude[tool][argChar] = 0; + else { + prev_extrude[tool][argChar] = numSlice; + } + break; + } + } + } + + if (typeof (x) !== 'undefined' || typeof (y) !== 'undefined' || typeof (z) !== 'undefined') { + addToModel = true; + move = false; + } + + } else if (/^(?:G28|$H)/i.test(line)) { + var args = line.split(/\s/); + + if (args.length === 1) { + // G28 with no arguments => home all axis + x = 0; + y = 0; + z = 0; + } else { + for (j = 0; j < args.length; j++) { + switch (argChar = args[j].charAt(0).toLowerCase()) { + case 'x': + x = 0; + break; + case 'y': + y = 0; + break; + case 'z': + z = 0; + break; + default: + break; + } + } + } + + if (typeof (x) !== 'undefined' || typeof (y) !== 'undefined' || typeof (z) !== 'undefined' || retract !== 0) { + addToModel = true; + move = true; + } + } else if (/^(?:T\d+)/i.test(line)) { + tool = Number(line.split(/\s/)[0].slice(1)); + if (!prev_extrude[tool]) + prev_extrude[tool] = {a: 0, b: 0, c: 0, e: 0, abs: 0}; + if (!prev_retract[tool]) + prev_retract[tool] = 0; + + offset = self.toolOffsets[tool] || {x: 0, y: 0}; + } + + // ensure z is set. + if (typeof (z) === 'undefined') { + if (typeof (prevZ) !== 'undefined') { + z = prevZ; + } else { + z = 0; + } + } + + if (addToModel) { + model.push({ + x: x, + y: y, + z: z, + extrude: extrude, + laser: laser, + retract: retract, + noMove: !move, + extrusion: (extrude || retract) && prev_extrude[tool]["abs"] ? prev_extrude[tool]["abs"] : 0, + prevX: prevX, + prevY: prevY, + prevZ: prevZ, + speed: lastF, + gcodeLine: i, + percentage: i / gcode_lines.length, + tool: tool + }); + } + if (convertAndAddToModel) { + var parts = self._convertG2G3(clockwise, x, y, z, pi, pj, pp, lastPos, units); + var lastPart = parts[0]; + for (var l = 1; l < parts.length; l++) { + var part = parts[l]; + + model.push({ + x: part[0], + y: part[1], + z: part[2], + extrude: extrude, + laser: laser, + retract: retract, + noMove: !move, + extrusion: (extrude || retract) && prev_extrude[tool]["abs"] ? prev_extrude[tool]["abs"] : 0, + prevX: lastPart[0], + prevY: lastPart[1], + prevZ: lastPart[2], + speed: lastF, + gcodeLine: i, + percentage: i / gcode_lines.length, + tool: tool + }); + + lastPart = part; + } + } + + if (move) { + if (typeof (x) !== 'undefined') + prevX = x; + if (typeof (y) !== 'undefined') + prevY = y; + } + + if (typeof (blockCallback) === 'function' && typeof (blockDelimiter) !== 'undefined' && blockDelimiter.test(line)) { + blockCallback(model); + model = []; + } + } + + prevZ = z; + if (typeof (blockCallback) === 'function' && model.length > 0) { + blockCallback(model); + } + }; + + self._convertG2G3 = function(clockwise, x, y, z, i, j, p, lastPos, units) { + if (typeof (x) === 'undefined') + x = lastPos.x; + if (typeof (y) === 'undefined') + y = lastPos.y; + if (typeof (z) === 'undefined') + z = lastPos.z; + if (typeof (i) === 'undefined') + i = 0.0; + if (typeof (j) === 'undefined') + j = 0.0; + if (typeof (p) === 'undefined') + p = 1.0; + + var curveSection = 1.0; // mm + if (units === "G20") { // inches + curveSection = 1.0 / 25.4; + } + + // angle variables. + var angleA; + var angleB; + var angle; + + // delta variables. + var aX; + var aY; + var bX; + var bY; + + + // center of rotation + var cX = lastPos.x + i; + var cY = lastPos.y + j; + + aX = lastPos.x - cX; + aY = lastPos.y - cY; + bX = x - cX; + bY = y - cY; + + // Clockwise + if (clockwise) { + angleA = Math.atan2(bY, bX); + angleB = Math.atan2(aY, aX); + } else { + angleA = Math.atan2(aY, aX); + angleB = Math.atan2(bY, bX); + } + + // Make sure angleB is always greater than angleA + // and if not add 2PI so that it is (this also takes + // care of the special case of angleA == angleB, + // ie we want a complete circle) + if (angleB <= angleA) { + angleB += 2 * Math.PI * p; + } + angle = angleB - angleA; + + // calculate a couple useful things. + var radius = Math.sqrt(aX * aX + aY * aY); + var length = radius * angle; + + // for doing the actual move. + var steps; // TODO accuracy setting + var s; + + // Maximum of either 2.4 times the angle in radians + // or the length of the curve divided by the curve section constant + steps = Math.ceil(Math.max(angle * 2.4, length / curveSection)); + + + var fta; + if (!clockwise) { + fta = angleA + angle; + } else { + fta = angleA; + } + + // THis if arc is correct + // TODO move this into the validator + var r2 = Math.sqrt(bX * bX + bY * bY); + var percentage; + if (r2 > radius) { + percentage = Math.abs(radius / r2) * 100.0; + } else { + percentage = Math.abs(r2 / radius) * 100.0; + } + + if (percentage < 99.7) { + var sb = ""; + sb += "Radius to end of arc differs from radius to start:\n"; + sb += "r1=" + radius + "\n"; + sb += "r2=" + r2 + "\n"; + console.error("gcode_parser.js convertG2G3", sb); + } + + // this is the real line calculation. + var parts = []; + var arcStartZ = lastPos.z; + for (s = 1; s <= steps; s++) { + var step; + if (!clockwise) + step = s; + else + step = steps - s; + + var ta = (angleA + angle * (step / steps)); + + parts.push([cX + radius * Math.cos(ta), cY + radius * Math.sin(ta), lastPos.z + (z - arcStartZ) * s / steps]); + + } + + return parts; + }; + + }; +}); \ No newline at end of file diff --git a/src/octoprint/plugins/svgtogcode/static/js/working_area.js b/src/octoprint/plugins/svgtogcode/static/js/working_area.js index d57613bb..78cefe48 100644 --- a/src/octoprint/plugins/svgtogcode/static/js/working_area.js +++ b/src/octoprint/plugins/svgtogcode/static/js/working_area.js @@ -2,6 +2,8 @@ $(function(){ function WorkingAreaViewModel(params) { var self = this; + + self.parser = new gcParser(); self.loginState = params[0]; self.settings = params[1]; @@ -101,51 +103,6 @@ $(function(){ self.placedDesigns([]); }; - // initialize list helper - self.listHelper = new ItemListHelper( - "gcodeFiles", - { - "name": function(a, b) { - // sorts ascending - if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1; - if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1; - return 0; - }, - "upload": function(a, b) { - // sorts descending - if (b["date"] === undefined || a["date"] > b["date"]) return -1; - if (a["date"] < b["date"]) return 1; - return 0; - }, - "size": function(a, b) { - // sorts descending - if (b["bytes"] === undefined || a["bytes"] > b["bytes"]) return -1; - if (a["bytes"] < b["bytes"]) return 1; - return 0; - } - }, - { - "printed": function(file) { - return !(file["prints"] && file["prints"]["success"] && file["prints"]["success"] > 0); - }, - "sd": function(file) { - return file["origin"] && file["origin"] == "sdcard"; - }, - "local": function(file) { - return !(file["origin"] && file["origin"] == "sdcard"); - }, - "machinecode": function(file) { - return file["type"] && file["type"] == "machinecode"; - }, - "model": function(file) { - return file["type"] && file["type"] == "model"; - } - }, - "name", - [], - [["sd", "local"], ["machinecode", "model"]], - 0 - ); self.trigger_resize = function(){ self.availableHeight(document.documentElement.clientHeight - $('body>nav').outerHeight() - $('footer>*').outerHeight() - 39); // magic number @@ -192,6 +149,62 @@ $(function(){ return val * self.svgDPI()/25.4; }; + self.placeGcode = function(file){ + console.log(file); + var previewId = self.getEntryId(file); + + if(snap.select('#'+previewId)){ + console.error("working_area placeGcode: file already placed."); + return; + } else { + var g = snap.group(); + g.attr({id: previewId}); + snap.select('#placedGcodes').append(g); + self.placedDesigns.push(file); + } + + self.loadGcode(file, function(gcode){ + self.parser.parse(gcode, /(m0?3)|(m0?5)/i, function(block){ + var points = []; + var intensity = -1; + for (var idx = 0; idx < block.length; idx++) { + var item = block[idx]; + points.push( [ item.x, item.y ] ); + intensity = item.laser; + } + if(points.length > 0) + self.draw_gcode(points, intensity, '#'+previewId); + + }); + }); + }; + + self.loadGcode = function(file, callback){ + var url = file.refs.download; + var date = file.date; + $.ajax({ + url: url, + data: { "ctime": date }, + type: "GET", + success: function(response, rstatus) { + if(rstatus === 'success'){ + if(typeof(callback) === 'function'){ + callback(response); + } + } + }, + error: function() { + console.error("working_area.js placeGcode: unable to load ", url); + } + }); + + }; + + self.removeGcode = function(file){ + var previewId = self.getEntryId(file); + snap.select('#' + previewId).remove(); + self.placedDesigns.remove(file); + }; self.placeSVG = function(file) { var url = self._getSVGserveUrl(file); @@ -207,14 +220,15 @@ $(function(){ var newSvg = f.select("g"); newSvg.attr(namespaces); -// var id = self.generateId(url); - var id = self.getEntryId(file); // works better on multiple instances. - newSvg.attr({id: id}); + var id = self.getEntryId(file); + var previewId = self.generateUniqueId(id); // appends -# if multiple times the same design is placed. + newSvg.attr({id: previewId}); snap.select("#userContent").append(newSvg); newSvg.drag();// TODO debug drag. should not be affected by scale matrix - file.id = id; + file.id = previewId; + file.previewId = previewId; file.url = url; self.placedDesigns.push(file); @@ -227,17 +241,20 @@ $(function(){ }; self.removeSVG = function(file){ - var url = self._getSVGserveUrl(file); - for (var idx = 0; idx < self.placedDesigns().length; idx++) { - var svg = self.placedDesigns()[idx]; - if(svg.url === url){ - snap.select('#'+svg.id).remove(); - self.placedDesigns.remove(file); - break; - } - } + console.log("removeSVG", file.previewId, self.placedDesigns.indexOf(file)); + snap.select('#'+file.previewId).remove(); + self.placedDesigns.remove(file); + // TODO debug why remove always clears all items of this type. +// self.placedDesigns.remove(function(item){ +// console.log("item", item.previewId ); +// //return false; +// if(item.previewId === file.previewId){ +// console.log("match", item.previewId ); +// return true; +// } else return false; +// }); }; - + self._getSVGserveUrl = function(file){ if (file && file["refs"] && file["refs"]["download"]) { var url = file.refs.download.replace("downloads", "serve"); @@ -247,8 +264,9 @@ $(function(){ }; self.templateFor = function(data) { + console.log("data", data); var extension = data.name.split('.').pop().toLowerCase(); - if (extension == "svg") { + if (extension === "svg") { return "wa_template_" + data.type + "_svg"; } else { return "wa_template_" + data.type; @@ -310,14 +328,12 @@ $(function(){ } }; - self.generateId = function(url){ - var idBase = '_'+url.substring(url.lastIndexOf('/')+1).replace(/[^a-zA-Z0-9]/ig, '-'); // _ at first place if filename starts with a digit - idBase = idBase.replace('') + self.generateUniqueId = function(idBase){ var suffix = 0; var id = idBase + "-" + suffix; while(snap.select('#'+id) !== null){ suffix += 1; - id = idBase + suffix; + id = idBase + "-" + suffix; } return id; }; @@ -335,7 +351,16 @@ $(function(){ return svg; }; - self.draw_gcode = function(points, intensity){ + self.getPlacedGcodes = ko.computed(function() { + var gcodeFiles = []; + ko.utils.arrayForEach(self.placedDesigns(), function(design) { + if(design.type === 'machinecode') gcodeFiles.push(design); + }); + return gcodeFiles; + }, self); + + + self.draw_gcode = function(points, intensity, target){ var stroke_color = intensity === 0 ? '#BBBBBB' : '#FF0000'; var d = 'M'+points.join(' '); var p = snap.path(d).attr({ @@ -343,14 +368,17 @@ $(function(){ stroke: stroke_color, strokeWidth: 1 }); - snap.select('#gCodePreview').append(p); + console.log("target", target); + snap.select(target).append(p); }; self.clear_gcode = function(){ +// console.log("gcodeprev clear"); snap.select('#gCodePreview').clear(); }; self.onStartup = function(){ GCODE.workingArea = self; // Temporary hack to use the gcode parser from the gCodeViewer + self.state.workingArea = self; self.files.workingArea = self; self.conversion.workingArea = self; $(window).resize(function(){ diff --git a/src/octoprint/plugins/svgtogcode/templates/override_index.jinja2 b/src/octoprint/plugins/svgtogcode/templates/override_index.jinja2 index 7d6e22af..f95133eb 100644 --- a/src/octoprint/plugins/svgtogcode/templates/override_index.jinja2 +++ b/src/octoprint/plugins/svgtogcode/templates/override_index.jinja2 @@ -177,7 +177,7 @@