Check axis fit and display errors/warnings.
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Mon, 15 Oct 2018 08:38:18 +0000 (01:38 -0700)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Mon, 15 Oct 2018 08:38:18 +0000 (01:38 -0700)
12 files changed:
CHANGELOG.md
src/js/axis-vars.js [new file with mode: 0644]
src/js/control-view.js
src/js/path-viewer.js
src/pug/templates/control-view.pug
src/py/bbctrl/CommandQueue.py
src/py/bbctrl/FileHandler.py
src/py/bbctrl/PlanTimer.py
src/py/bbctrl/Preplanner.py
src/py/bbctrl/State.py
src/py/bbctrl/Web.py
src/stylus/style.styl

index b02288708fe93b7c4482ed67d0670417affc10e0..03d705b88d969c03a1137c1f4d82dd05f3c82e21 100644 (file)
@@ -17,6 +17,8 @@ Buildbotics CNC Controller Firmware Changelog
  - Display accurate time remaining, ETA and progress during run.
  - Automatically collapase moves in planner which are too short in time.
  - Show IO status indicators on configuration pages.
+ - Check that axis dimensions fit path plan dimensions.
+ - Show machine working envelope in path plan viewer.
 
 ## v0.3.28
  - Show step rate on motor configuration page.
diff --git a/src/js/axis-vars.js b/src/js/axis-vars.js
new file mode 100644 (file)
index 0000000..ffa0d4f
--- /dev/null
@@ -0,0 +1,184 @@
+/******************************************************************************\
+
+                 This file is part of the Buildbotics firmware.
+
+                   Copyright (c) 2015 - 2018, Buildbotics LLC
+                              All rights reserved.
+
+      This file ("the software") is free software: you can redistribute it
+      and/or modify it under the terms of the GNU General Public License,
+       version 2 as published by the Free Software Foundation. You should
+       have received a copy of the GNU General Public License, version 2
+      along with the software. If not, see <http://www.gnu.org/licenses/>.
+
+      The software is distributed in the hope that it will be useful, but
+           WITHOUT ANY WARRANTY; without even the implied warranty of
+       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+                Lesser General Public License for more details.
+
+        You should have received a copy of the GNU Lesser General Public
+                 License along with the software.  If not, see
+                        <http://www.gnu.org/licenses/>.
+
+                 For information regarding this software email:
+                   "Joseph Coffland" <joseph@buildbotics.com>
+
+\******************************************************************************/
+
+'use strict'
+
+
+function is_defined(x) {return typeof x != 'undefined'}
+
+
+module.exports = {
+  props: ['state', 'config'],
+
+
+  computed: {
+    x: function () {return this._compute_axis('x')},
+    y: function () {return this._compute_axis('y')},
+    z: function () {return this._compute_axis('z')},
+    a: function () {return this._compute_axis('a')},
+    b: function () {return this._compute_axis('b')},
+    c: function () {return this._compute_axis('c')},
+    axes: function () {return this._compute_axes()}
+  },
+
+
+  methods: {
+    _convert_length: function (value) {
+      return this.state.imperial ? value / 25.4 : value;
+    },
+
+
+    _length_str: function (value) {
+      return this._convert_length(value).toLocaleString() +
+        (this.state.imperial ? ' in' : ' mm');
+    },
+
+
+    _compute_axis: function (axis) {
+      var abs        = this.state[axis + 'p'] || 0;
+      var off        = this.state['offset_' + axis];
+      var motor_id   = this._get_motor_id(axis);
+      var motor      = motor_id == -1 ? {} : this.config.motors[motor_id];
+      var pm         = motor['power-mode'];
+      var enabled    = typeof pm != 'undefined' && pm != 'disabled';
+      var homingMode = motor['homing-mode']
+      var homed      = this.state[motor_id + 'homed'];
+      var min        = this.state[motor_id + 'tn'];
+      var max        = this.state[motor_id + 'tm'];
+      var dim        = max - min;
+      var pathMin    = this.state['path_min_' + axis];
+      var pathMax    = this.state['path_max_' + axis];
+      var pathDim    = pathMax - pathMin;
+      var under      = pathMin - off < min;
+      var over       = max < pathMax - off;
+      var klass      = (homed ? 'homed' : 'unhomed') + ' axis-' + axis;
+      var state      = 'UNHOMED';
+      var icon       = 'question-circle';
+      var title;
+
+      if (0 < dim && dim < pathDim) {
+        state = 'NO FIT';
+        klass += ' error';
+        icon = 'ban';
+
+      } else if (homed) {
+        state = 'HOMED'
+        icon = 'check-circle';
+
+        if (over || under) {
+          state = over ? 'OVER' : 'UNDER';
+          klass += ' warn';
+          icon = 'exclamation-circle';
+        }
+      }
+
+      switch (state) {
+      case 'UNHOMED':  title = 'Click the home button to home axis.'; break;
+      case 'HOMED': title = 'Axis successfuly homed.'; break;
+
+      case 'OVER':
+        title = 'Tool path would move ' +
+          this._length_str(pathMax - off - max) + ' beyond axis bounds.';
+        break;
+
+      case 'UNDER':
+        title = 'Tool path would move ' +
+          this._length_str(min - pathMin + off) + ' below axis bounds.';
+        break;
+
+      case 'NO FIT':
+        title = 'Tool path dimensions exceed axis dimensions by ' +
+          this._length_str(pathDim - dim) + '.';
+        break;
+      }
+
+      return {
+        pos: abs + off,
+        abs: abs,
+        off: off,
+        min: min,
+        max: max,
+        dim: dim,
+        pathMin: pathMin,
+        pathMax: pathMax,
+        pathDim: pathDim,
+        motor: motor_id,
+        enabled: enabled,
+        homingMode: homingMode,
+        homed: homed,
+        klass: klass,
+        state: state,
+        icon: icon,
+        title: title
+      }
+    },
+
+
+    _get_motor_id: function (axis) {
+      for (var i = 0; i < this.config.motors.length; i++) {
+        var motor = this.config.motors[i];
+        if (motor.axis.toLowerCase() == axis) return i;
+      }
+
+      return -1;
+    },
+
+
+    _compute_axes: function () {
+      var homed = false;
+
+      for (var name of 'xyzabc') {
+        var axis = this[name];
+
+        if (!axis.enabled) continue
+        if (!axis.homed) {homed = false; break}
+        homed = true;
+      }
+
+      var error = false;
+      var warn = false;
+
+      if (homed)
+        for (name of 'xyzabc') {
+          axis = this[name];
+
+          if (!axis.enabled) continue;
+          if (axis.klass.indexOf('error') != -1) error = true;
+          if (axis.klass.indexOf('warn') != -1) warn = true;
+        }
+
+      var klass = homed ? 'homed' : 'unhomed';
+      if (error) klass += ' error';
+      else if (warn) klass += ' warn';
+
+      return {
+        homed: homed,
+        klass: klass
+      }
+    }
+  }
+}
index ea3daba8acd6e92979822be4c68fdea7ee7736aa..87065c4efee67b916062dcdcaea7a398b68dfc8b 100644 (file)
@@ -181,57 +181,6 @@ module.exports = {
     send: function (msg) {this.$dispatch('send', msg)},
 
 
-    get_axis_motor_id: function (axis) {
-      axis = axis.toLowerCase();
-
-      for (var i = 0; i < this.config.motors.length; i++) {
-        var motor = this.config.motors[i];
-        if (motor.axis.toLowerCase() == axis) return i;
-      }
-
-      return -1;
-    },
-
-
-    get_axis_motor: function (axis) {
-      var motor = this.get_axis_motor_id(axis);
-      if (motor != -1) return this.config.motors[motor];
-    },
-
-
-    get_axis_motor_param: function (axis, name) {
-      var motor = this.get_axis_motor(axis);
-      if (typeof motor != 'undefined') return motor[name];
-    },
-
-
-    enabled: function (axis) {
-      var pm = this.get_axis_motor_param(axis, 'power-mode')
-      return typeof pm != 'undefined' && pm != 'disabled';
-    },
-
-
-    is_homed: function (axis) {
-      if (typeof axis == 'undefined') {
-        var enabled = false;
-        var axes = 'xyzabc';
-
-        for (var i in axes) {
-          if (this.enabled(axes.charAt(i))) {
-            if (!this.is_homed(axes.charAt(i))) return false;
-            else enabled = true;
-          }
-        }
-
-        return enabled;
-
-      } else {
-        var motor = this.get_axis_motor_id(axis);
-        return motor != -1 && this.state[motor + 'homed'];
-      }
-    },
-
-
     update: function () {
       // Update file list
       api.get('file').done(function (files) {
@@ -289,11 +238,7 @@ module.exports = {
 
 
     load_history: function (index) {this.mdi = this.history[index];},
-
-
-    open: function (e) {
-      $('.gcode-file-input').click();
-    },
+    open: function (e) {$('.gcode-file-input').click()},
 
 
     upload: function (e) {
@@ -313,14 +258,14 @@ module.exports = {
     },
 
 
-    deleteCurrent: function () {
+    delete_current: function () {
       if (this.state.selected)
         api.delete('file/' + this.state.selected).done(this.update);
       this.deleteGCode = false;
     },
 
 
-    deleteAll: function () {
+    delete_all: function () {
       api.delete('file').done(this.update);
       this.deleteGCode = false;
     },
@@ -330,8 +275,7 @@ module.exports = {
       if (typeof axis == 'undefined') api.put('home');
 
       else {
-        if (this.get_axis_motor_param(axis, 'homing-mode') != 'manual')
-          api.put('home/' + axis);
+        if (this[axis].homingMode != 'manual') api.put('home/' + axis);
         else this.manual_home[axis] = true;
       }
     },
@@ -355,28 +299,21 @@ module.exports = {
     },
 
 
-    get_position: function (axis) {
-      return this.state[axis + 'p'] + this.get_offset(axis);
-    },
-
-
-    get_offset: function (axis) {return this.state['offset_' + axis] || 0},
-
-
     set_position: function (axis, position) {
       this.position_msg[axis] = false;
       api.put('position/' + axis, {'position': parseFloat(position)});
     },
 
 
-    zero: function (axis) {
-      if (typeof axis == 'undefined') {
-        var axes = 'xyzabc';
-        for (var i in axes)
-          if (this.enabled(axes.charAt(i)))
-            this.zero(axes.charAt(i));
+    zero_all: function () {
+      for (var axis of 'xyzabc')
+        if (this[axis].enabled) this.zero(axis);
+    },
+
 
-      } else this.set_position(axis, 0);
+    zero: function (axis) {
+      if (typeof axis == 'undefined') this.zero_all();
+      else this.set_position(axis, 0);
     },
 
 
@@ -414,5 +351,8 @@ module.exports = {
       data[axis + 'pl'] = x;
       this.send(JSON.stringify(data));
     }
-  }
+  },
+
+
+  mixins: [require('./axis-vars')]
 }
index 58df8474b2c984eb06bad1dfa1aca6b8e96d808f..0ceed825da4a15889d1a32a5d8a6de15acd61e03 100644 (file)
@@ -5,7 +5,7 @@
 
                   For information regarding this software email:
                                  Joseph Coffland
-                          joseph@buildbotics.com
+                              joseph@buildbotics.com
 
         This software is free software: you clan redistribute it and/or
         modify it under the terms of the GNU Lesser General Public License
@@ -43,7 +43,7 @@ var surfaceModes = ['cut', 'wire', 'solid', 'off'];
 
 module.exports = {
   template: '#path-viewer-template',
-  props: ['toolpath', 'progress', 'state'],
+  props: ['toolpath', 'progress'],
 
 
   data: function () {
@@ -63,26 +63,6 @@ module.exports = {
 
 
   computed: {
-    x: function () {return this.xAbs + this.xOff},
-    y: function () {return this.yAbs + this.yOff},
-    z: function () {return this.zAbs + this.zOff},
-
-    xAbs: function () {return this.state.xp || 0},
-    yAbs: function () {return this.state.xy || 0},
-    zAbs: function () {return this.state.xz || 0},
-
-    xOff: function () {return this.state.offset_x || 0},
-    yOff: function () {return this.state.offset_y || 0},
-    zOff: function () {return this.state.offset_z || 0},
-
-    xMin: function () {return this.state.xtn},
-    yMin: function () {return this.state.ytn},
-    zMin: function () {return this.state.ztn},
-
-    xMax: function () {return this.state.xtm},
-    yMax: function () {return this.state.ytm},
-    zMax: function () {return this.state.ztm},
-
     hasPath: function () {return typeof this.toolpath.path != 'undefined'},
     target: function () {return $(this.$el).find('.path-viewer-content')[0]}
   },
@@ -90,15 +70,22 @@ module.exports = {
 
   watch: {
     toolpath: function () {Vue.nextTick(this.update)},
-    surfaceMode: function (mode) {this.updateSurfaceMode(mode)},
+    surfaceMode: function (mode) {this.update_surface_mode(mode)},
     small: function () {Vue.nextTick(this.update_view)},
     showPath: function (enable) {set_visible(this.path, enable)},
     showTool: function (enable) {set_visible(this.tool, enable)},
-    showBBox: function (enable) {set_visible(this.bbox, enable)},
     showAxes: function (enable) {set_visible(this.axes, enable)},
-    x: function () {this.update_tool()},
-    y: function () {this.update_tool()},
-    z: function () {this.update_tool()}
+
+
+    showBBox: function (enable) {
+      set_visible(this.bbox, enable);
+      set_visible(this.envelope, enable);
+    },
+
+
+    x: function () {this.axis_changed()},
+    y: function () {this.axis_changed()},
+    z: function () {this.axis_changed()}
   },
 
 
@@ -128,7 +115,7 @@ module.exports = {
     },
 
 
-    updateSurfaceMode: function (mode) {
+    update_surface_mode: function (mode) {
       if (!this.enabled) return;
 
       if (typeof this.surfaceMaterial != 'undefined') {
@@ -159,7 +146,7 @@ module.exports = {
     },
 
 
-    getDims: function () {
+    get_dims: function () {
       var t = $(this.target);
       var width = t.innerWidth();
       var height = t.innerHeight();
@@ -169,7 +156,7 @@ module.exports = {
 
     update_view: function () {
       if (!this.enabled) return;
-      var dims = this.getDims();
+      var dims = this.get_dims();
 
       this.camera.aspect = dims.width / dims.height;
       this.camera.updateProjectionMatrix();
@@ -181,9 +168,34 @@ module.exports = {
       if (!this.enabled) return;
       if (typeof tool == 'undefined') tool = this.tool;
       if (typeof tool == 'undefined') return;
-      tool.position.x = this.x;
-      tool.position.y = this.y;
-      tool.position.z = this.z;
+      tool.position.x = this.x.pos;
+      tool.position.y = this.y.pos;
+      tool.position.z = this.z.pos;
+    },
+
+
+    update_envelope: function (envelope) {
+      if (!this.enabled || !this.axes.homed) return;
+      if (typeof envelope == 'undefined') envelope = this.envelope;
+      if (typeof envelope == 'undefined') return;
+
+      var min = new THREE.Vector3();
+      var max = new THREE.Vector3();
+
+      for (var axis of 'xyz') {
+        min[axis] = this[axis].min + this[axis].off;
+        max[axis] = this[axis].max + this[axis].off;
+      }
+
+      var bounds = new THREE.Box3(min, max);
+      if (bounds.isEmpty()) envelope.geometry = this.create_empty_geom();
+      else envelope.geometry = this.create_bbox_geom(bounds);
+    },
+
+
+    axis_changed: function () {
+      this.update_tool();
+      this.update_envelope();
     },
 
 
@@ -227,7 +239,7 @@ module.exports = {
       this.lights.add(backLight);
 
       // Surface material
-      this.surfaceMaterial = this.createSurfaceMaterial();
+      this.surfaceMaterial = this.create_surface_material();
 
       // Controls
       this.controls = new orbit(this.camera, this.renderer.domElement);
@@ -257,7 +269,7 @@ module.exports = {
     },
 
 
-    createSurfaceMaterial: function () {
+    create_surface_material: function () {
       return new THREE.MeshPhongMaterial({
         specular: 0x111111,
         shininess: 10,
@@ -267,7 +279,7 @@ module.exports = {
     },
 
 
-    drawWorkpiece: function (scene, material) {
+    draw_workpiece: function (scene, material) {
       if (typeof this.workpiece == 'undefined') return;
 
       var min = this.workpiece.min;
@@ -294,7 +306,7 @@ module.exports = {
     },
 
 
-    drawSurface: function (scene, material) {
+    draw_surface: function (scene, material) {
       if (typeof this.vertices == 'undefined') return;
 
       var geometry = new THREE.BufferGeometry();
@@ -311,7 +323,7 @@ module.exports = {
     },
 
 
-    drawTool: function (scene, bbox) {
+    draw_tool: function (scene, bbox) {
       // Tool size is relative to bounds
       var size = bbox.getSize(new THREE.Vector3());
       var length = (size.x + size.y + size.z) / 24;
@@ -335,7 +347,7 @@ module.exports = {
     },
 
 
-    drawAxis: function (axis, up, length, radius) {
+    draw_axis: function (axis, up, length, radius) {
       var color;
 
       if (axis == 0)      color = 0xff0000; // Red
@@ -362,7 +374,7 @@ module.exports = {
     },
 
 
-    drawAxes: function (scene, bbox) {
+    draw_axes: function (scene, bbox) {
       var size = bbox.getSize(new THREE.Vector3());
       var length = (size.x + size.y + size.z) / 3;
       length /= 10;
@@ -372,7 +384,7 @@ module.exports = {
 
       for (var axis = 0; axis < 3; axis++)
         for (var up = 0; up < 2; up++)
-          group.add(this.drawAxis(axis, up, length, radius));
+          group.add(this.draw_axis(axis, up, length, radius));
 
       group.visible = this.showAxes;
       scene.add(group);
@@ -381,13 +393,13 @@ module.exports = {
     },
 
 
-    drawPath: function (scene) {
+    draw_path: function (scene) {
       var cutting = [0, 1, 0];
       var rapid = [1, 0, 0];
 
-      var x = this.x;
-      var y = this.y;
-      var z = this.z;
+      var x = this.x.pos;
+      var y = this.y.pos;
+      var z = this.z.pos;
       var color = undefined;
 
       var positions = [];
@@ -436,48 +448,62 @@ module.exports = {
     },
 
 
-    drawBBox: function (scene, bbox) {
-      if (bbox.isEmpty()) return;
+    create_empty_geom: function () {
+      var geometry = new THREE.BufferGeometry();
+      geometry.addAttribute('position',
+                            new THREE.Float32BufferAttribute([], 3));
+      return geometry;
+    },
+
 
+    create_bbox_geom: function (bbox) {
       var vertices = [];
 
-      // Top
-      vertices.push(bbox.min.x, bbox.min.y, bbox.min.z);
-      vertices.push(bbox.max.x, bbox.min.y, bbox.min.z);
-      vertices.push(bbox.max.x, bbox.min.y, bbox.min.z);
-      vertices.push(bbox.max.x, bbox.min.y, bbox.max.z);
-      vertices.push(bbox.max.x, bbox.min.y, bbox.max.z);
-      vertices.push(bbox.min.x, bbox.min.y, bbox.max.z);
-      vertices.push(bbox.min.x, bbox.min.y, bbox.max.z);
-      vertices.push(bbox.min.x, bbox.min.y, bbox.min.z);
-
-      // Bottom
-      vertices.push(bbox.min.x, bbox.max.y, bbox.min.z);
-      vertices.push(bbox.max.x, bbox.max.y, bbox.min.z);
-      vertices.push(bbox.max.x, bbox.max.y, bbox.min.z);
-      vertices.push(bbox.max.x, bbox.max.y, bbox.max.z);
-      vertices.push(bbox.max.x, bbox.max.y, bbox.max.z);
-      vertices.push(bbox.min.x, bbox.max.y, bbox.max.z);
-      vertices.push(bbox.min.x, bbox.max.y, bbox.max.z);
-      vertices.push(bbox.min.x, bbox.max.y, bbox.min.z);
-
-      // Sides
-      vertices.push(bbox.min.x, bbox.min.y, bbox.min.z);
-      vertices.push(bbox.min.x, bbox.max.y, bbox.min.z);
-      vertices.push(bbox.max.x, bbox.min.y, bbox.min.z);
-      vertices.push(bbox.max.x, bbox.max.y, bbox.min.z);
-      vertices.push(bbox.max.x, bbox.min.y, bbox.max.z);
-      vertices.push(bbox.max.x, bbox.max.y, bbox.max.z);
-      vertices.push(bbox.min.x, bbox.min.y, bbox.max.z);
-      vertices.push(bbox.min.x, bbox.max.y, bbox.max.z);
+      if (!bbox.isEmpty()) {
+        // Top
+        vertices.push(bbox.min.x, bbox.min.y, bbox.min.z);
+        vertices.push(bbox.max.x, bbox.min.y, bbox.min.z);
+        vertices.push(bbox.max.x, bbox.min.y, bbox.min.z);
+        vertices.push(bbox.max.x, bbox.min.y, bbox.max.z);
+        vertices.push(bbox.max.x, bbox.min.y, bbox.max.z);
+        vertices.push(bbox.min.x, bbox.min.y, bbox.max.z);
+        vertices.push(bbox.min.x, bbox.min.y, bbox.max.z);
+        vertices.push(bbox.min.x, bbox.min.y, bbox.min.z);
+
+        // Bottom
+        vertices.push(bbox.min.x, bbox.max.y, bbox.min.z);
+        vertices.push(bbox.max.x, bbox.max.y, bbox.min.z);
+        vertices.push(bbox.max.x, bbox.max.y, bbox.min.z);
+        vertices.push(bbox.max.x, bbox.max.y, bbox.max.z);
+        vertices.push(bbox.max.x, bbox.max.y, bbox.max.z);
+        vertices.push(bbox.min.x, bbox.max.y, bbox.max.z);
+        vertices.push(bbox.min.x, bbox.max.y, bbox.max.z);
+        vertices.push(bbox.min.x, bbox.max.y, bbox.min.z);
+
+        // Sides
+        vertices.push(bbox.min.x, bbox.min.y, bbox.min.z);
+        vertices.push(bbox.min.x, bbox.max.y, bbox.min.z);
+        vertices.push(bbox.max.x, bbox.min.y, bbox.min.z);
+        vertices.push(bbox.max.x, bbox.max.y, bbox.min.z);
+        vertices.push(bbox.max.x, bbox.min.y, bbox.max.z);
+        vertices.push(bbox.max.x, bbox.max.y, bbox.max.z);
+        vertices.push(bbox.min.x, bbox.min.y, bbox.max.z);
+        vertices.push(bbox.min.x, bbox.max.y, bbox.max.z);
+      }
 
       var geometry = new THREE.BufferGeometry();
-      var material = new THREE.LineBasicMaterial({color: 0xffffff});
 
       geometry.addAttribute('position',
                             new THREE.Float32BufferAttribute(vertices, 3));
 
-      var line = new THREE.LineSegments(geometry, material)
+      return geometry;
+    },
+
+
+    draw_bbox: function (scene, bbox) {
+      var geometry = this.create_bbox_geom(bbox);
+      var material = new THREE.LineBasicMaterial({color: 0xffffff});
+      var line = new THREE.LineSegments(geometry, material);
 
       line.visible = this.showBBox;
 
@@ -487,24 +513,39 @@ module.exports = {
     },
 
 
+    draw_envelope: function (scene) {
+      var geometry = this.create_empty_geom();
+      var material = new THREE.LineBasicMaterial({color: 0x00f7ff});
+      var line = new THREE.LineSegments(geometry, material);
+
+      line.visible = this.showBBox;
+
+      scene.add(line);
+      this.update_envelope(line);
+
+      return line;
+    },
+
+
     draw: function (scene) {
       // Lights
       scene.add(this.ambient);
       scene.add(this.lights);
 
       // Model
-      this.path = this.drawPath(scene);
-      this.surfaceMesh = this.drawSurface(scene, this.surfaceMaterial);
-      this.workpieceMesh = this.drawWorkpiece(scene, this.surfaceMaterial);
-      this.updateSurfaceMode(this.surfaceMode);
+      this.path = this.draw_path(scene);
+      this.surfaceMesh = this.draw_surface(scene, this.surfaceMaterial);
+      this.workpieceMesh = this.draw_workpiece(scene, this.surfaceMaterial);
+      this.update_surface_mode(this.surfaceMode);
 
       // Compute bounding box
       var bbox = this.get_model_bounds();
 
       // Tool, axes & bounds
-      this.tool = this.drawTool(scene, bbox);
-      this.axes = this.drawAxes(scene, bbox);
-      this.bbox = this.drawBBox(scene, bbox);
+      this.tool = this.draw_tool(scene, bbox);
+      this.axes = this.draw_axes(scene, bbox);
+      this.bbox = this.draw_bbox(scene, bbox);
+      this.envelope = this.draw_envelope(scene);
     },
 
 
@@ -614,5 +655,8 @@ module.exports = {
 
       this.camera.position.copy(offset.multiplyScalar(dist * 1.2).add(center));
     }
-  }
+  },
+
+
+  mixins: [require('./axis-vars')]
 }
index d63526459a44eab0efe27349863d0c0090eea18a..5d9f92879ce4403bd83710e910d5797ecc933625 100644 (file)
 script#control-view-template(type="text/x-template")
   #control
     table.axes
-      tr(:class="{'homed': is_homed()}")
+      tr(:class="axes.klass")
         th.name Axis
         th.position Position
         th.absolute Absolute
         th.offset Offset
+        th.state State
         th.actions
           button.pure-button(:disabled="!can_mdi",
             title="Zero all axis offsets.", @click="zero()") &empty;
@@ -42,13 +43,16 @@ script#control-view-template(type="text/x-template")
             .fa.fa-home
 
       each axis in 'xyzabc'
-        tr.axis(:class=`{'homed': is_homed('${axis}'), 'axis-${axis}': true}`,
-          v-if=`enabled('${axis}')`)
+        tr.axis(:class=`${axis}.klass`, v-if=`${axis}.enabled`,
+          :title=`${axis}.title`)
           th.name= axis
-          td.position
-            unit-value(:value=`get_position('${axis}')`, precision="4")
-          td.absolute: unit-value(:value="state." + axis + "p", precision="3")
-          td.offset: unit-value(:value=`get_offset('${axis}')`, precision="3")
+          td.position: unit-value(:value=`${axis}.pos`, precision=4)
+          td.absolute: unit-value(:value=`${axis}.abs`, precision=3)
+          td.offset: unit-value(:value=`${axis}.off`, precision=3)
+          td.state
+            .fa(:class=`'fa-' + ${axis}.icon`)
+            | {{#{axis}.state}}
+
           th.actions
             button.pure-button(:disabled="!can_mdi",
               title=`Set {{'${axis}' | upper}} axis position.`,
@@ -59,9 +63,8 @@ script#control-view-template(type="text/x-template")
               title=`Zero {{'${axis}' | upper}} axis offset.`,
               @click=`zero('${axis}')`) &empty;
 
-            button.pure-button(:disabled="!can_mdi",
-              title=`Home {{'${axis}' | upper}} axis.`,
-              @click=`home('${axis}')`)
+            button.pure-button(:disabled="!can_mdi", @click=`home('${axis}')`,
+              title=`Home {{'${axis}' | upper}} axis.`)
               .fa.fa-home
 
             message(:show.sync=`position_msg['${axis}']`)
@@ -79,7 +82,7 @@ script#control-view-template(type="text/x-template")
                 button.pure-button(@click=`position_msg['${axis}'] = false`)
                   | Cancel
 
-                button.pure-button(v-if="is_homed('" + axis + "')",
+                button.pure-button(v-if=`${axis}.homed`,
                   @click=`unhome('${axis}')`) Unhome
 
                 button.pure-button.button-success(
@@ -239,10 +242,10 @@ script#control-view-template(type="text/x-template")
             p(slot="body")
             div(slot="footer")
               button.pure-button(@click="deleteGCode = false") Cancel
-              button.pure-button.button-error(@click="deleteAll")
+              button.pure-button.button-error(@click="delete_all")
                 .fa.fa-trash
                 | &nbsp;all
-              button.pure-button.button-success(@click="deleteCurrent")
+              button.pure-button.button-success(@click="delete_current")
                 .fa.fa-trash
                 | &nbsp;selected
 
@@ -251,7 +254,7 @@ script#control-view-template(type="text/x-template")
             option(v-for="file in files", :value="file") {{file}}
 
         path-viewer(:toolpath="toolpath", :progress="toolpath_progress",
-          :state="state")
+          :state="state", :config="config")
         gcode-viewer
 
       section#content2.tab-content
@@ -276,16 +279,16 @@ script#control-view-template(type="text/x-template")
       section#content3.tab-content
         .jog
           axis-control(axes="XY", :colors="['red', 'green']",
-            :enabled="[enabled('x'), enabled('y')]",
-            v-if="enabled('x') || enabled('y')", :adjust="jog_adjust")
+            :enabled="[x.enabled, y.enabled]",
+            v-if="x.enabled || y.enabled", :adjust="jog_adjust")
 
           axis-control(axes="AZ", :colors="['orange', 'blue']",
-            :enabled="[enabled('a'), enabled('z')]",
-            v-if="enabled('a') || enabled('z')", :adjust="jog_adjust")
+            :enabled="[a.enabled, z.enabled]",
+            v-if="a.enabled || z.enabled", :adjust="jog_adjust")
 
           axis-control(axes="BC", :colors="['cyan', 'purple']",
-            :enabled="[enabled('b'), enabled('c')]",
-            v-if="enabled('b') || enabled('c')", :adjust="jog_adjust")
+            :enabled="[b.enabled, c.enabled]",
+            v-if="b.enabled || c.enabled", :adjust="jog_adjust")
 
         .jog-adjust
           | Fine adjust
index 59fecfa70d6dcfb9a433b9b669edfec4882aa063..b04ad63748d4d30d6a36b8a6fe10a586182e3980 100644 (file)
@@ -29,7 +29,7 @@ import logging
 from collections import deque
 
 log = logging.getLogger('CmdQ')
-log.setLevel(logging.INFO)
+log.setLevel(logging.WARNING)
 
 
 class CommandQueue():
index 4b63c1365cc7b2ddfcb414f68306f69fa5bc64c8..6d03fa5bb6370deb4ac7a155d586838b9b74b5d5 100644 (file)
 import os
 import bbctrl
 import glob
+import logging
+
+
+log = logging.getLogger('FileHandler')
 
 
 def safe_remove(path):
@@ -69,6 +73,8 @@ class FileHandler(bbctrl.APIHandler):
         self.ctrl.preplanner.invalidate(gcode['filename'])
         self.ctrl.state.set('selected', gcode['filename'])
 
+        log.info('GCode updated: ' + gcode['filename'])
+
 
     def get(self, path):
         if path:
index 4968fc4b021c0ed48286f20476c6a30a96989a56..05b9d60d3885ca3e8f0f18bfca7e5bbfd12d0806 100644 (file)
@@ -35,19 +35,18 @@ log = logging.getLogger('PlanTimer')
 class PlanTimer(object):
     def __init__(self, ctrl):
         self.ctrl = ctrl
+        self.plan_times = None
 
         self.reset()
-        self._report()
-
         self.ctrl.state.set('plan_time', 0)
         ctrl.state.add_listener(self._update)
+        self._report()
 
 
     def reset(self):
         self.plan_time = 0
         self.move_start = None
         self.hold_start = None
-        self.plan_times = None
         self.plan_index = 0
 
 
@@ -66,42 +65,53 @@ class PlanTimer(object):
 
                 self.ctrl.state.set('plan_time', round(t))
 
-        self.timer = self.ctrl.ioloop.call_later(1, self._report)
+        self.ctrl.ioloop.call_later(1, self._report)
 
 
-    def _update(self, update):
-        # Check state
-        if 'xx' in update:
-            state = update['xx']
+    def _update_state(self, state):
+        if state in ['READY', 'ESTOPPED']:
+            self.ctrl.state.set('plan_time', 0)
+            self.reset()
 
-            if state in ['READY', 'ESTOPPED']:
-                self.ctrl.state.set('plan_time', 0)
-                self.reset()
+        elif state == 'HOLDING': self.hold_start = time.time()
+        elif (state == 'RUNNING' and self.hold_start is not None and
+              self.move_start is not None):
+            self.move_start += time.time() - self.hold_start
+            self.hold_start = None
 
-            elif state == 'HOLDING': self.hold_start = time.time()
-            elif (state == 'RUNNING' and self.hold_start is not None and
-                  self.move_start is not None):
-                self.move_start += time.time() - self.hold_start
-                self.hold_start = None
 
-        # Get plan times
-        if self.plan_times is None or 'selected' in update:
-            active_plan = self.ctrl.state.get('selected', '')
+    def _update_times(self, filename):
+        if not filename: return
+        future = self.ctrl.preplanner.get_plan(filename)
+
+        def cb(future):
+            if (filename != self.ctrl.state.get('selected') or
+                future.cancelled()): return
 
-            if active_plan:
-                plan = self.ctrl.preplanner.get_plan(active_plan)
+            self.reset()
+            path, meta = future.result()
+            self.plan_times = meta['times']
 
-                if plan is not None and plan.done():
-                    self.reset()
-                    self.plan_times = plan.result()[1]
+        self.ctrl.ioloop.add_future(future, cb)
+
+
+    def _update_time(self, currentID):
+        if self.plan_times is None: return
+
+        while self.plan_index < len(self.plan_times):
+            id, t = self.plan_times[self.plan_index]
+            if id <= currentID: self.move_start = time.time()
+            if currentID <= id: break
+            self.plan_time = t
+            self.plan_index += 1
+
+
+    def _update(self, update):
+        # Check state
+        if 'xx' in update: self._update_state(update['xx'])
+
+        # Get plan times
+        if 'selected' in update: self._update_times(update['selected'])
 
         # Get plan time for current id
-        if self.plan_times is not None and 'id' in update:
-            currentID = update['id']
-
-            while self.plan_index < len(self.plan_times):
-                id, t = self.plan_times[self.plan_index]
-                if id <= currentID: self.move_start = time.time()
-                if currentID <= id: break
-                self.plan_time = t
-                self.plan_index += 1
+        if 'id' in update: self._update_time(update['id'])
index 92e62a2505a4668ce09f43fce6b7e55728c9fd45..f506afc17072e5ad1bfb25b191278800a600a0fa 100644 (file)
@@ -32,6 +32,7 @@ import json
 import hashlib
 import gzip
 import glob
+import math
 import threading
 from concurrent.futures import Future, ThreadPoolExecutor, TimeoutError
 from tornado import gen
@@ -103,7 +104,9 @@ class Preplanner(object):
         self.max_plan_time = max_plan_time
         self.max_loop_time = max_loop_time
 
-        for dir in ['plans', 'times']:
+        ctrl.state.add_listener(self._update)
+
+        for dir in ['plans', 'meta']:
             if not os.path.exists(dir): os.mkdir(dir)
 
         self.started = Future()
@@ -113,6 +116,30 @@ class Preplanner(object):
         self.lock = threading.Lock()
 
 
+    def _update(self, update):
+        if not 'selected' in update: return
+        filename = update['selected']
+        future = self.get_plan(filename)
+
+        def set_bounds(type, bounds):
+            for axis in 'xyzabc':
+                if axis in bounds[type]:
+                    self.ctrl.state.set('path_%s_%s' % (type, axis),
+                                        bounds[type][axis])
+
+        def cb(future):
+            if (filename != self.ctrl.state.get('selected') or
+                future.cancelled()): return
+
+            path, meta = future.result()
+            bounds = meta['bounds']
+
+            set_bounds('min', bounds)
+            set_bounds('max', bounds)
+
+        self.ctrl.ioloop.add_future(future, cb)
+
+
     def start(self):
         log.info('Preplanner started')
         self.started.set_result(True)
@@ -202,13 +229,14 @@ class Preplanner(object):
         # Check if this plan was already run
         hid = plan_hash(filename, config)
         plan_path = 'plans/' + filename + '.' + hid + '.gz'
-        times_path = 'times/' + filename + '.' + hid + '.gz'
+        meta_path = 'meta/' + filename + '.' + hid + '.gz'
 
         try:
-            if os.path.exists(plan_path) and os.path.exists(times_path):
+            if os.path.exists(plan_path) and os.path.exists(meta_path):
                 with open(plan_path, 'rb') as f: data = f.read()
-                with open(times_path, 'rb') as f: times = f.read()
-                return (data, json.loads(gzip.decompress(times).decode('utf8')))
+                with open(meta_path, 'rb') as f: meta = f.read()
+                meta = json.loads(gzip.decompress(meta).decode('utf8'))
+                return (data, meta)
 
         except Exception as e: log.error(e)
 
@@ -234,12 +262,24 @@ class Preplanner(object):
         maxLine = 0
         maxLineTime = time.time()
         totalTime = 0
-        position = dict(x = 0, y = 0, z = 0)
+        position = {}
         rapid = False
         moves = []
         times = []
+        bounds = dict(min = {}, max = {})
         messages = []
         count = 0
+        cancelled = False
+
+        for axis in 'xyzabc':
+            position[axis] = 0
+            bounds['min'][axis] = math.inf
+            bounds['max'][axis] = -math.inf
+
+        def add_to_bounds(axis, value):
+            if value < bounds['min'][axis]: bounds['min'][axis] = value
+            if bounds['max'][axis] < value: bounds['max'][axis] = value
+
 
         levels = dict(I = 'info', D = 'debug', W = 'warning', E = 'error',
                       C = 'critical')
@@ -264,17 +304,18 @@ class Preplanner(object):
                 if planner.is_synchronizing(): planner.synchronize(0)
 
                 if cmd['type'] == 'line':
-                    if not 'first' in cmd:
-                        totalTime += sum(cmd['times']) / 1000
-                        times.append((cmd['id'], totalTime))
+                    if 'first' in cmd: continue
 
+                    totalTime += sum(cmd['times']) / 1000
+                    times.append((cmd['id'], totalTime))
                     target = cmd['target']
                     move = {}
 
-                    for axis in 'xyz':
+                    for axis in 'xyzabc':
                         if axis in target:
                             position[axis] = target[axis]
                             move[axis] = target[axis]
+                            add_to_bounds(axis, move[axis])
 
                     if 'rapid' in cmd: move['rapid'] = cmd['rapid']
 
@@ -291,6 +332,7 @@ class Preplanner(object):
                     times.append((cmd['id'], totalTime))
 
                 if not self._progress(filename, maxLine / totalLines):
+                    cancelled = True
                     raise Exception('Plan canceled.')
 
                 if self.max_plan_time < time.time() - start:
@@ -309,14 +351,23 @@ class Preplanner(object):
 
         self._progress(filename, 1)
 
+        # Remove infinity from bounds
+        for axis in 'xyzabc':
+            if bounds['min'][axis] == math.inf: del bounds['min'][axis]
+            if bounds['max'][axis] == -math.inf: del bounds['max'][axis]
+
         # Encode data as string
         data = dict(time = totalTime, lines = totalLines, path = moves,
                     messages = messages)
         data = gzip.compress(dump_json(data).encode('utf8'))
 
-        # Save plan & times
-        with open(plan_path, 'wb') as f: f.write(data)
-        with open(times_path, 'wb') as f:
-            f.write(gzip.compress(dump_json(times).encode('utf8')))
+        # Meta data
+        meta = dict(times = times, bounds = bounds)
+        meta_comp = gzip.compress(dump_json(meta).encode('utf8'))
+
+        # Save plan & meta data
+        if not cancelled:
+            with open(plan_path, 'wb') as f: f.write(data)
+            with open(meta_path, 'wb') as f: f.write(meta_comp)
 
-        return (data, times)
+        return (data, meta)
index 3b346609e6e0c5b403bac211f99dce2e8ed4674b..b724c2a36e19350af70c5a7f2cb2ad8b1ee2add9 100644 (file)
@@ -186,8 +186,9 @@ class State(object):
 
 
     def add_listener(self, listener):
+        log.info(self.vars)
         self.listeners.append(listener)
-        if self.vars: listener(self.vars)
+        listener(self.vars)
 
 
     def remove_listener(self, listener): self.listeners.remove(listener)
index f63d4b3fed65947eb8d786d693b25bb14295c796..9b7c8b5d419145272a61175e5b57bd4b82d12c90 100644 (file)
@@ -237,6 +237,7 @@ class PathHandler(bbctrl.APIHandler):
     def get(self, filename):
         if not os.path.exists('upload/' + filename):
             raise HTTPError(404, 'File not found')
+
         future = self.ctrl.preplanner.get_plan(filename)
 
         try:
@@ -249,7 +250,7 @@ class PathHandler(bbctrl.APIHandler):
             return
 
         if data is not None:
-            data = data[0]
+            data = data[0] # We only want the compressed path
             self.set_header('Content-Encoding', 'gzip')
 
             # Respond with chunks to avoid long delays
index df18854823f63968fbf52cb1db3227855260e5c0..3daa69779a252603be067846b2cca5709ee88712 100644 (file)
@@ -247,6 +247,12 @@ span.unit
       background-color #ccffcc
       color #000
 
+    .warn
+      background-color #ffffcc
+
+    .error
+      background-color #ffcccc
+
     .axis
       .name
         text-transform capitalize
@@ -258,6 +264,14 @@ span.unit
       .position
         width 99%
 
+      td.state
+        text-align left
+
+        .fa
+          font-size 140%
+          margin-left 2px
+          margin-right 6px
+
       .absolute, .offset
         min-width 6em