$(function () { console.log("Create PrettyGCode View Model"); function PrettyGCodeViewModel(parameters) { var self = this; self.printerProfiles = parameters[2]; self.controlViewModel = parameters[3]; //Parse terminal data for file and pos updates. var curJobName = ""; var durJobDate = 0;//use date of file to check for update. function updateJob(job) { if (durJobDate != job.file.date) { curJobName = job.file.path; durJobDate = job.file.date; if (viewInitialized && gcodeProxy) { gcodeProxy.loadGcode('downloads/files/local/' + curJobName); printHeadSim = new PrintHeadSimulator(); //terminalGcodeProxy = new GCodeParser(); //terminalGcodeProxy;//used to display gcode actualy sent to printer. } } } self.fromHistoryData = function (data) { if (!viewInitialized) return; updateJob(data.job); }; /* Arc Interpolation Parameters */ self.mm_per_arc_segment = 1.0; // The absolute longest length of an interpolated segment self.min_arc_segments = 20; // The minimum number of interpolated segments in a full circle, 0 to disable // The absolute minimum length of an interpolated segment. // Limited by mm_per_arc_segment as a max and min_arc_segments as a minimum, 0 to disable self.min_mm_per_arc_segment = 0.1; // This controls how many arcs will be drawn before the exact position of the // next segment is recalculated. Reduces the number of sin/cos calls. // 0 to disable self.n_arc_correction = 24; // A function to interpolate arcs into straight segments. Returns an array of positions self.interpolateArc = function (state, arc) { // This is adapted from the Marlin arc interpolation routine found at // https://github.com/MarlinFirmware/Marlin/ // The license can be found here: https://github.com/MarlinFirmware/Marlin/blob/2.0.x/LICENSE // This allows the rendered arcs to be VERY close to what would be printed, // depending on the firmware settings. // Create vars to hold the initial and current position so we don't affect the state var initial_position = {}, current_position = {}; Object.assign(initial_position, state) Object.assign(current_position, state) // Create the results which contain the copied initial position var interpolated_segments = [initial_position]; // note that arc.is_clockwise determines if this is a G2, else it is a G3 // I'm going to also extract all the necessary variables up front to make this easier // to convert from the source c++ arc interpolation code // Convert r format to i j format if necessary // I have no code like this to test, so I am not 100% sure this will work as expected // commenting out for now /* if (arc.r) { if (arc.x != current_position.x || arc.y != current_position.y) { var vector = {x: (arc.x - current_position.x)/2.0, y: (arc.y - current_position.y)/2.0}; var e = arc.is_clockwise ^ (arc.r < 0) ? -1 : 1; var len = Math.sqrt(Math.pow(vector.x,2) + Math.pow(vector.y,2)); var h2 = (arc.r - len) * (arc.r + len); var h = (h2 >= 0) ? Math.sqrt(h2) : 0.0; var bisector = {x: -1.0*vector.y, y: vector.x }; arc.i = (vector.x + bisector.x) / len * e * h; arc.j = (vector.y + bisector.y) / len * e * h; } }*/ // Calculate the radius, we will be using it a lot. var radius = Math.hypot(arc.i, arc.j); // Radius Vector var v_radius = { x: -1.0 * arc.i, y: -1.0 * arc.j }; // Center of arc var center = { x: current_position.x - v_radius.x, y: current_position.y - v_radius.y }; // Z Travel Total var travel_z = arc.z - current_position.z; // Extruder Travel var travel_e = arc.e - current_position.e; // Radius Target Vector var v_radius_target = { x: arc.x - center.x, y: arc.y - center.y }; var angular_travel_total = Math.atan2( v_radius.x * v_radius_target.y - v_radius.y * v_radius_target.x, v_radius.x * v_radius_target.x + v_radius.y * v_radius_target.y ); // Having a positive angle is convenient here. We will make it negative later // if we need to. if (angular_travel_total < 0) { angular_travel_total += 2.0 * Math.PI } // Copy our mm_per_arc_segments var because we may be modifying it for this arc var mm_per_arc_segment = self.mm_per_arc_segment; // Enforce min_arc_segments if it is greater than 0 if (self.min_arc_segments > 0) { mm_per_arc_segment = (radius * ((2.0 * Math.PI) / self.min_arc_segments)); // We will need to enforce our max segment length later, flag this } // Enforce the minimum segment length if it is set if (self.min_mm_per_arc_segment > 0) { if (mm_per_arc_segment < self.min_mm_per_arc_segment) { mm_per_arc_segment = self.min_mm_per_arc_segment; } } // Enforce the maximum segment length if (mm_per_arc_segment > self.mm_per_arc_segment) { mm_per_arc_segment = self.mm_per_arc_segment; } // Adjust the angular travel if the direction is clockwise if (arc.is_clockwise) { angular_travel_total -= (2.0 * Math.PI); } // Compensate for a full circle, which would give us an angle of 0 here // We want that to be 2Pi. Note, full circles are bad in 3d printing, but they // should still render correctly if (current_position.x == arc.x && current_position.y == arc.y && angular_travel_total == 0) { angular_travel_total += 2.0 * Math.PI; } // Now it's time to calculate the mm of total travel along the arc, making sure we take Z into account var mm_of_travel_arc = Math.hypot(angular_travel_total * radius, Math.abs(travel_z)); // Get the number of segments total we will be generating var num_segments = Math.ceil(mm_of_travel_arc / mm_per_arc_segment); // Calculate xy_segment_theta, z_segment_theta, and e_segment_theta // This is the distance we will be moving for each interpolated segment var xy_segment_theta = angular_travel_total / num_segments; var z_segment_theta = travel_z / num_segments; var e_segment_theta = travel_e / num_segments; // Time to interpolate! if (num_segments > 1) { // it's possible for num_segments to be zero. If that's true, we just need to draw a line // from the start to the end coordinates, and this isn't needed. // I am NOT going to use the small angel approximation for sin and cos here, but it // could be easily added if performance is a problem. Here is code for this if it becomes // necessary: //var sq_theta_per_segment = theta_per_segment * theta_per_segment; //var sin_T = theta_per_segment - sq_theta_per_segment * theta_per_segment / 6; //var cos_T = 1 - 0.5f * sq_theta_per_segment; // Small angle approximation var cos_t = Math.cos(xy_segment_theta); var sin_t = Math.sin(xy_segment_theta); var r_axisi; // We are going to correct sin and cos only occasionally to reduce cpu usage var count = 0; // Loop through each interpolated segment, minus the endpoint which will be handled separately for (var i = 1; i < num_segments; i++) { if (count < self.n_arc_correction) { // not time to recalculate X and Y. // Apply the rotational vector r_axisi = v_radius.x * sin_t + v_radius.y * cos_t; v_radius.x = v_radius.x * cos_t - v_radius.y * sin_t; v_radius.y = r_axisi; count++; } else { // Arc correction to radius vector. Computed only every N_ARC_CORRECTION increments. // Compute exact location by applying transformation matrix from initial radius vector(=-offset). var sin_ti = Math.sin(i * xy_segment_theta); var cos_ti = Math.cos(i * xy_segment_theta); v_radius.x = (-1.0 * arc.i) * cos_ti + arc.j * sin_ti; v_radius.y = (-1.0 * arc.i) * sin_ti - arc.j * cos_ti; count = 0; } // Draw the segment var line = { x: center.x + v_radius.x, y: center.y + v_radius.y, z: current_position.z + z_segment_theta, e: current_position.e + e_segment_theta, f: arc.f }; /*console.debug( "Arc Segment " + i.toString() + ":" + " X" + line.x.toString() + " Y" + line.y.toString() + " Z" + line.z.toString() + " E" + line.e.toString() + " F" + line.f.toString() );*/ interpolated_segments.push(line); // Update the current state current_position.x = line.x; current_position.y = line.y; current_position.z = line.z; current_position.e = line.e; } } // Move to the target position var line = { x: arc.x, y: arc.y, z: arc.z, e: arc.e, f: arc.f }; interpolated_segments.push(line); //Done!!! return interpolated_segments; }; //used to animate the nozzle position in response to terminal messages function PrintHeadSimulator() { var buffer = []; var HeadState = function () { this.position = new THREE.Vector3(0, 0, 0); this.rate = 5.0 * 60; this.extrude = false; this.relative = false; //this.lastExtrudedZ=0;//used to better calc layer number this.layerLineNumber = 0; this.clone = function () { var newState = new HeadState(); newState.position.copy(this.position); newState.rate = this.rate; newState.extrude = this.extrude; newState.relative = this.relative; //newState.lastExtrudedZ=this.lastExtrudedZ; newState.layerLineNumber = this.layerLineNumber; return (newState); } }; var curState = new HeadState(); var curEnd = new HeadState(); var parserCurState = new HeadState(); var observedLayerCount = 0; var parserLayerLineNumber = 0; var parserLastExtrudedZ = 0; var curLastExtrudedZ = 0; parserCurState.extrude = true; this.getCurPosition = function () { return ({ position: curState.position, layerZ: curLastExtrudedZ, lineNumber: curState.layerLineNumber }); } this.getBufferStats = function () { return (buffer.length); } // //var currentFileOffset=0; //add gcode command to the buffer this.addCommand = function (cmd) { //currentFileOffset+=cmd.length; if (buffer.length > 1000) { console.log("PrintHeadSimulator buffer overflow") return; } var is_g0_g1 = cmd.indexOf(" G0") > -1 || cmd.indexOf(" G1") > -1; var is_g2_g3 = !is_g0_g1 && cmd.indexOf(" G2") > -1 || cmd.indexOf(" G3") > -1; if (is_g0_g1 || is_g2_g3) { var parserPreviousState = {}; // If this is a g2/g3, we need to know the previous state to interpolate the arcs if (is_g2_g3) { parserPreviousState = Object.assign(parserPreviousState, parserCurState); } // Extract x, y, z, f and e var x = parseFloat(cmd.split("X")[1]) if (!Number.isNaN(x)) { if (parserCurState.relative) parserCurState.position.x += x; else parserCurState.position.x = x; } var y = parseFloat(cmd.split("Y")[1]) if (!Number.isNaN(y)) { if (parserCurState.relative) parserCurState.position.y += y; else parserCurState.position.y = y; } var z = parseFloat(cmd.split("Z")[1]) if (!Number.isNaN(z)) { if (parserCurState.relative) parserCurState.position.z += z; else parserCurState.position.z = z; } var f = parseFloat(cmd.split("F")[1]) if (!Number.isNaN(f)) { parserCurState.rate = f; } var e = parseFloat(cmd.split("E")[1]) if (!Number.isNaN(e)) { parserCurState.extrude = true; if (parserLastExtrudedZ != parserCurState.position.z) { //new layer (probably) //observedLayerCount++ //console.log("New layer Z."+parserCurState.position.z+" File offset:"+currentFileOffset) parserLayerLineNumber = 0; parserLastExtrudedZ = parserCurState.position.z; } else parserLayerLineNumber++; } else { parserCurState.extrude = false; } parserCurState.layerLineNumber = parserLayerLineNumber; // if this is a g0/g1, push the state to the buffer if (is_g0_g1) { buffer.push(parserCurState.clone()); } else { // This is a g2/g3, so we need to do things a bit differently. // Extract I and J, R, and is_clockwise var is_clockwise = cmd.indexOf(" G2") > -1; var i = parseFloat(cmd.split("I")[1]); var j = parseFloat(cmd.split("J")[1]); var r = parseFloat(cmd.split("R")[1]); var arc = { // Get X Y and Z from the previous state if it is not // provided x: this.getCurrentCoordinate(x, parserPreviousState.position.x), y: this.getCurrentCoordinate(y, parserPreviousState.position.y), z: this.getCurrentCoordinate(z, parserPreviousState.position.z), // Set I and J and R to 0 if they are not provided. i: this.getCurrentCoordinate(i, 0), j: this.getCurrentCoordinate(j, 0), r: this.getCurrentCoordinate(r, 0), // K omitted, not sure what that's supposed to do //k: k !== undefined ? k : 0, // Since the amount extruded doesn't really matter, set it to 1 if we are extruding, // We don't want undefined values going into the arc interpolation routine e: this.getCurrentCoordinate(e, parserPreviousState.extrude ? 1 : 0), f: this.getCurrentCoordinate(r, parserPreviousState.rate), is_clockwise: is_clockwise }; // Need to handle R maybe var segments = self.interpolateArc(parserPreviousState, arc); for (var index = 1; index < segments.length; index++) { var cur_segment = segments[index]; var cur_state = parserCurState.clone(); cur_state.position = new THREE.Vector3(cur_segment.x, cur_segment.y, cur_segment.z); buffer.push(cur_state); } } } else if (cmd.indexOf(" G90") > -1) { //G90: Set to Absolute Positioning parserCurState.relative = false; } else if (cmd.indexOf(" G91") > -1) { //G91: Set to state.relative Positioning parserCurState.relative = true; } } //window.myMaxRate=120.0; //window.fudge=7; // Handle undefined and NaN for current coordinates. this.getCurrentCoordinate = function (cmdCoord, prevCoord) { if (cmdCoord === undefined || isNaN(cmdCoord)) { cmdCoord = prevCoord; } return cmdCoord; } //Update the printhead position based on time elapsed. this.updatePosition = function (timeStep) { //Convert the gcode feed rate (in MM/per min?) to rate per second. var rate = curState.rate / 60.0; //rate=rate/2;//todo. why still too fast? //adapt rate to keep up with buffer. //todo. Make dist based rather than just buffer size. if (buffer.length > 10) { rate = rate * (buffer.length / 5.0); //console.log(["Too Slow ",rate,buffer.length]) } if (buffer.length < 5) { rate = rate * (1.0 / (buffer.length * 5.0)); //console.log(["Too fast ",rate,buffer.length]) } //rate=Math.min(rate,window.myMaxRate); //dist head needs to travel this frame var dist = rate * timeStep while (buffer.length > 0 && dist > 0)//while some place to go and some dist left. { //direction var vectToCurEnd = curEnd.position.clone().sub(curState.position); var distToEnd = vectToCurEnd.length(); if (dist < distToEnd)//Inside current line? { //move pos the distance along line vectToCurEnd.setLength(dist); curState.position.add(vectToCurEnd); dist = 0;//all done } else { //move pos to end point. curState.position.copy(curEnd.position); curState.rate = curEnd.rate; //subract dist for next loop. dist = dist - distToEnd; //draw segment //todo. //update lastZ for display of layers. if (curEnd.extrude && curEnd.position.z != curLastExtrudedZ) { curLastExtrudedZ = curEnd.position.z; } //console.log([curState.position.z,curState.layerLineNumber]) //start on next buffer command buffer.shift(); if (buffer.length > 0) { curEnd = buffer[0]; curState.layerLineNumber = curEnd.layerLineNumber; } } } } } var printHeadSim = new PrintHeadSimulator(); var curPrinterState = null; var curPrintFilePos = 0; self.fromCurrentData = function (data) { //Dont do anything if view not initalized if (!viewInitialized) return; //update current loaded model. updateJob(data.job); if (curPrinterState && curPrinterState.text != data.state.text) { //console.log(["Printer state changed: ",curPrinterState.text," -> ",data.state.text]) if (data.state.text.startsWith("Operational")) { //console.log("Resetting print simulation"); printHeadSim = new PrintHeadSimulator(); } } curPrinterState = data.state; curPrintFilePos = data.progress.filepos; //parse logs position data for simulator if (data.logs.length) { data.logs.forEach(function (e, i) { if (e.startsWith("Send:")) { //console.log(["GCmd:",e]); if (printHeadSim) printHeadSim.addCommand(e); //Strip out the extra stuff in the terminal line. //match second space to * character. I hate regexp. if (terminalGcodeProxy) { var reg = new RegExp('(?<=\\s\\S*\\s).[^*]*', 'g'); var matches = e.match(reg); if (matches && matches.length > 0) terminalGcodeProxy.parse(matches[0] + '\n'); } } else if (e.startsWith("Recv: T:")) { //console.log(["GCmd:",e]); let parts = e.substr(6).split("@");//remove Recv: and checksum. let temps = parts[0]; let statusStr = temps;//+" Buffer:"+printHeadSim.getBufferStats() $(".pgstatus").text(statusStr); } }) } }; self.updateCss = function (newCss) { //alert(this) var newCss = $("#pg_add_css").val(); console.log(["Update css:", newCss]); localStorage.setItem('pg_add_css_val', newCss) $("#pgcss").html(newCss); } self.onAfterBinding = function () { console.log("onAfterBinding") //var addCss=$("#add_css").val(); $("