- More efficient GCode scrolling with very large files.
## v0.3.20
- Eliminated drift caused by miscounting half microsteps.
+ - Fixed disappearing GCode in Web.
+ - More efficient GCode scrolling with very large files.
## v0.3.19
- Fixed stopping problems. #127
link(rel="stylesheet" href="css/font-awesome.min.css")
link(href="css/Audiowide.css" rel="stylesheet" type="text/css")
+ link(href="css/clusterize.css" rel="stylesheet" type="text/css")
link(rel="stylesheet" href="/css/style-" + css_hash + ".css")
script(src="js/jquery-1.11.3.min.js")
script(src="js/vue.js")
script(src="js/sockjs.min.js")
+ script(src="js/clusterize.min.js")
script(src='/js/assets-' + js_hash + '.js')
script(src="js/ui.js")
:disabled="is_running || is_stopping")
option(v-for="file in files", :value="file") {{file}}
- .gcode(:class="{placeholder: !gcode}", @scroll="gcode_scroll")
- span(v-if="!gcode.length") GCode displays here.
- ul
- li(v-for="item in gcode", id="gcode-line-{{$index}}",
- track-by="$index")
- span {{$index + 1 + gcode_offset}}
- | {{item}}
+ gcode-viewer
section#content2.tab-content
.mdi.pure-form(title="Manual GCode entry.")
--- /dev/null
+//-/////////////////////////////////////////////////////////////////////////////
+//- //
+//- 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> //
+//- //
+//-/////////////////////////////////////////////////////////////////////////////
+
+script#gcode-viewer-template(type="text/x-template")
+ .gcode
+ .clusterize
+ .clusterize-scroll
+ ul.clusterize-content
+ li.clusterize-no-data.placeholder GCode displays here.
var api = require('./api');
-var maxLines = 1000;
-var pageSize = Math.round(maxLines / 10);
-
function _is_array(x) {
return Object.prototype.toString.call(x) === '[object Array]';
data: function () {
return {
mdi: '',
- last_file: '',
files: [],
axes: 'xyzabc',
- all_gcode: [],
- gcode: [],
- gcode_offset: 0,
history: [],
speed_override: 1,
feed_override: 1,
components: {
- 'axis-control': require('./axis-control')
+ 'axis-control': require('./axis-control'),
+ 'gcode-viewer': require('./gcode-viewer')
},
watch: {
- 'state.line': function () {this.update_gcode_line()},
+ 'state.line': function () {
+ if (this.mach_state != 'HOMING')
+ this.$broadcast('gcode-line', this.state.line);
+ },
+
+
'state.selected': function () {this.load()}
},
api.put('jog', data);
},
- connected: function () {this.update()},
-
- update: function () {console.log(this.state.xx, this.state.cycle)}
+ connected: function () {this.update()}
},
},
- gcode_move_up: function (count) {
- var lines = 0;
-
- for (var i = 0; i < count; i++) {
- if (!this.gcode_offset) break;
-
- this.gcode.unshift(this.all_gcode[this.gcode_offset - 1])
- this.gcode.pop();
- this.gcode_offset--;
- lines++;
- }
-
- return lines;
- },
-
-
- gcode_move_down: function (count) {
- var lines = 0;
-
- for (var i = 0; i < count; i++) {
- if (this.all_gcode.length <= this.gcode_offset + this.gcode.length)
- break;
-
- this.gcode.push(this.all_gcode[this.gcode_offset + this.gcode.length])
- this.gcode.shift();
- this.gcode_offset++;
- lines++
- }
-
- return lines;
- },
-
-
- gcode_scroll: function (e) {
- if (this.gcode.length == this.all_gcode.length) return;
-
- var t = e.target;
- var percentScroll = t.scrollTop / (t.scrollHeight - t.clientHeight);
-
- var lines = 0;
- if (percentScroll < 0.2) lines = this.gcode_move_up(pageSize);
- else if (0.8 < percentScroll) lines = -this.gcode_move_down(pageSize);
- else return;
-
- if (lines) t.scrollTop += t.scrollHeight * lines / maxLines;
- },
-
-
- update_gcode_line: function () {
- if (this.mach_state == 'HOMING') return;
-
- if (typeof this.last_line != 'undefined') {
- $('#gcode-line-' + this.last_line).removeClass('highlight');
- this.last_line = undefined;
- }
-
- if (0 <= this.state.line) {
- var line = this.state.line - 1;
-
- // Make sure the current GCode is loaded
- if (line < this.gcode_offset ||
- this.gcode_offset + this.gcode.length <= line) {
- this.gcode_offset = line - pageSize;
- if (this.gcode_offset < 0) this.gcode_offset = 0;
-
- this.gcode = this.all_gcode.slice(this.gcode_offset, maxLines);
- }
-
- Vue.nextTick(function () {
- var e = $('#gcode-line-' + line);
- if (e.length)
- e.addClass('highlight')[0]
- .scrollIntoView({behavior: 'smooth'});
-
- this.last_line = line;
- }.bind(this));
- }
- },
-
-
update: function () {
// Update file list
api.get('file').done(function (files) {
},
+ load: function () {
+ var file = this.state.selected;
+ if (typeof file != 'undefined') this.$broadcast('gcode-load', file);
+ this.$broadcast('gcode-line', this.state.line);
+ },
+
+
submit_mdi: function () {
this.send(this.mdi);
if (!this.history.length || this.history[0] != this.mdi)
api.upload('file', fd)
.done(function () {
- if (file.name == this.last_file) this.last_file = '';
+ this.$broadcast('gcode-reload', file.name);
this.update();
}.bind(this));
},
- load: function () {
- var file = this.state.selected;
- if (typeof file == 'undefined' || file == this.last_file) return;
-
- api.get('file/' + file)
- .done(function (data) {
- this.all_gcode = data.trimRight().split(/\r?\n/);
- this.gcode = this.all_gcode.slice(0, maxLines);
- this.gcode_offset = 0;
- this.last_file = file;
-
- Vue.nextTick(this.update_gcode_line);
- }.bind(this));
- },
-
-
deleteCurrent: function () {
if (this.state.selected)
api.delete('file/' + this.state.selected).done(this.update);
--- /dev/null
+/******************************************************************************\
+
+ 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'
+
+var api = require('./api');
+
+var max_lines = 1000;
+var page_size = Math.round(max_lines / 10);
+
+
+module.exports = {
+ template: '#gcode-viewer-template',
+
+
+ data: function () {
+ return {
+ empty: true,
+ file: '',
+ line: -1,
+ scrolling: false
+ }
+ },
+
+
+ events: {
+ 'gcode-load': function (file) {this.load(file)},
+ 'gcode-clear': function () {this.clear()},
+ 'gcode-reload': function (file) {this.reload(file)},
+ 'gcode-line': function (line) {this.update_line(line)}
+ },
+
+
+ methods: {
+ load: function (file) {
+ if (file == this.file) return;
+ this.clear();
+ this.file = file;
+
+ api.get('file/' + file)
+ .done(function (data) {
+ var lines = data.trimRight().split(/\r?\n/);
+
+ for (var i = 0; i < lines.length; i++) {
+ lines[i] = '<li class="line-' + (i + 1) + '">' +
+ '<span class="gcode-line">' + (i + 1) + '</span>' +
+ lines[i] + '</li>';
+ }
+
+ this.clusterize = new Clusterize({
+ rows: lines,
+ scrollElem: $(this.$el).find('.clusterize-scroll')[0],
+ contentElem: $(this.$el).find('.clusterize-content')[0],
+ callbacks: {clusterChanged: this.highlight}
+ });
+
+ this.empty = false;
+
+ Vue.nextTick(this.update_line);
+ }.bind(this));
+ },
+
+
+ clear: function () {
+ this.empty = true;
+ this.file = '';
+ this.line = -1;
+
+ if (typeof this.clusterize != 'undefined')
+ this.clusterize.destroy();
+ },
+
+
+ reload: function (file) {
+ if (file != this.file) return;
+ this.clear();
+ this.load(file);
+ },
+
+
+ highlight: function () {
+ var e = $(this.$el).find('.highlight');
+ if (e.length) e.removeClass('highlight');
+
+ e = $(this.$el).find('.line-' + this.line);
+ if (e.length) e.addClass('highlight');
+ },
+
+
+ update_line: function(line) {
+ if (typeof line != 'undefined') {
+ if (this.line == line) return;
+ this.line = line;
+
+ } else line = this.line;
+
+ if (typeof this.clusterize == 'undefined') return;
+
+ var totalLines = this.clusterize.getRowsAmount();
+
+ if (line <= 0) line = 1;
+ if (totalLines < line) line = totalLines;
+
+ var e = $(this.$el).find('.clusterize-scroll');
+
+ var lineHeight = e[0].scrollHeight / totalLines;
+ var linesPerPage = Math.floor(e[0].clientHeight / lineHeight);
+ var current = e[0].scrollTop / lineHeight;
+ var target = line - 1 - Math.floor(linesPerPage / 2);
+
+ // Update scroll position
+ if (!this.scrolling) {
+ if (target < current - 20 || current + 20 < target)
+ e[0].scrollTop = target * lineHeight;
+
+ else {
+ this.scrolling = true;
+ e.animate({scrollTop: target * lineHeight}, {
+ complete: function () {this.scrolling = false}.bind(this)
+ })
+ }
+ }
+
+ Vue.nextTick(this.highlight);
+ }
+ }
+}
################################################################################
import logging
-
import bbctrl
--- /dev/null
+/* max-height - the only parameter in this file that needs to be edited.
+ * Change it to suit your needs. The rest is recommended to leave as is.
+ */
+.clusterize-scroll{
+ max-height: 200px;
+ overflow: auto;
+}
+
+/**
+ * Avoid vertical margins for extra tags
+ * Necessary for correct calculations when rows have nonzero vertical margins
+ */
+.clusterize-extra-row{
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+}
+
+/* By default extra tag .clusterize-keep-parity added to keep parity of rows.
+ * Useful when used :nth-child(even/odd)
+ */
+.clusterize-extra-row.clusterize-keep-parity{
+ display: none;
+}
+
+/* During initialization clusterize adds tabindex to force the browser to keep focus
+ * on the scrolling list, see issue #11
+ * Outline removes default browser's borders for focused elements.
+ */
+.clusterize-content{
+ outline: 0;
+ counter-reset: clusterize-counter;
+}
+
+/* Centering message that appears when no data provided
+ */
+.clusterize-no-data td{
+ text-align: center;
+}
\ No newline at end of file
--- /dev/null
+/* Clusterize.js - v0.18.1 - 2018-01-02
+ http://NeXTs.github.com/Clusterize.js/
+ Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */
+
+;(function(q,n){"undefined"!=typeof module?module.exports=n():"function"==typeof define&&"object"==typeof define.amd?define(n):this[q]=n()})("Clusterize",function(){function q(b,a,c){return a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent("on"+b,c)}function n(b,a,c){return a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent("on"+b,c)}function r(b){return"[object Array]"===Object.prototype.toString.call(b)}function m(b,a){return window.getComputedStyle?window.getComputedStyle(a)[b]:
+a.currentStyle[b]}var l=function(){for(var b=3,a=document.createElement("b"),c=a.all||[];a.innerHTML="\x3c!--[if gt IE "+ ++b+"]><i><![endif]--\x3e",c[0];);return 4<b?b:document.documentMode}(),x=navigator.platform.toLowerCase().indexOf("mac")+1,p=function(b){if(!(this instanceof p))return new p(b);var a=this,c={rows_in_block:50,blocks_in_cluster:4,tag:null,show_no_data_row:!0,no_data_class:"clusterize-no-data",no_data_text:"No data",keep_parity:!0,callbacks:{}};a.options={};for(var d="rows_in_block blocks_in_cluster show_no_data_row no_data_class no_data_text keep_parity tag callbacks".split(" "),
+f=0,h;h=d[f];f++)a.options[h]="undefined"!=typeof b[h]&&null!=b[h]?b[h]:c[h];c=["scroll","content"];for(f=0;d=c[f];f++)if(a[d+"_elem"]=b[d+"Id"]?document.getElementById(b[d+"Id"]):b[d+"Elem"],!a[d+"_elem"])throw Error("Error! Could not find "+d+" element");a.content_elem.hasAttribute("tabindex")||a.content_elem.setAttribute("tabindex",0);var e=r(b.rows)?b.rows:a.fetchMarkup(),g={};b=a.scroll_elem.scrollTop;a.insertToDOM(e,g);a.scroll_elem.scrollTop=b;var k=!1,m=0,l=!1,t=function(){x&&(l||(a.content_elem.style.pointerEvents=
+"none"),l=!0,clearTimeout(m),m=setTimeout(function(){a.content_elem.style.pointerEvents="auto";l=!1},50));k!=(k=a.getClusterNum())&&a.insertToDOM(e,g);a.options.callbacks.scrollingProgress&&a.options.callbacks.scrollingProgress(a.getScrollProgress())},u=0,v=function(){clearTimeout(u);u=setTimeout(a.refresh,100)};q("scroll",a.scroll_elem,t);q("resize",window,v);a.destroy=function(b){n("scroll",a.scroll_elem,t);n("resize",window,v);a.html((b?a.generateEmptyRow():e).join(""))};a.refresh=function(b){(a.getRowsHeight(e)||
+b)&&a.update(e)};a.update=function(b){e=r(b)?b:[];b=a.scroll_elem.scrollTop;e.length*a.options.item_height<b&&(k=a.scroll_elem.scrollTop=0);a.insertToDOM(e,g);a.scroll_elem.scrollTop=b};a.clear=function(){a.update([])};a.getRowsAmount=function(){return e.length};a.getScrollProgress=function(){return this.options.scroll_top/(e.length*this.options.item_height)*100||0};var w=function(b,c){var d=r(c)?c:[];d.length&&(e="append"==b?e.concat(d):d.concat(e),a.insertToDOM(e,g))};a.append=function(a){w("append",
+a)};a.prepend=function(a){w("prepend",a)}};p.prototype={constructor:p,fetchMarkup:function(){for(var b=[],a=this.getChildNodes(this.content_elem);a.length;)b.push(a.shift().outerHTML);return b},exploreEnvironment:function(b,a){var c=this.options;c.content_tag=this.content_elem.tagName.toLowerCase();b.length&&(l&&9>=l&&!c.tag&&(c.tag=b[0].match(/<([^>\s/]*)/)[1].toLowerCase()),1>=this.content_elem.children.length&&(a.data=this.html(b[0]+b[0]+b[0])),c.tag||(c.tag=this.content_elem.children[0].tagName.toLowerCase()),
+this.getRowsHeight(b))},getRowsHeight:function(b){var a=this.options,c=a.item_height;a.cluster_height=0;if(b.length&&(b=this.content_elem.children,b.length)){var d=b[Math.floor(b.length/2)];a.item_height=d.offsetHeight;"tr"==a.tag&&"collapse"!=m("borderCollapse",this.content_elem)&&(a.item_height+=parseInt(m("borderSpacing",this.content_elem),10)||0);"tr"!=a.tag&&(b=parseInt(m("marginTop",d),10)||0,d=parseInt(m("marginBottom",d),10)||0,a.item_height+=Math.max(b,d));a.block_height=a.item_height*a.rows_in_block;
+a.rows_in_cluster=a.blocks_in_cluster*a.rows_in_block;a.cluster_height=a.blocks_in_cluster*a.block_height;return c!=a.item_height}},getClusterNum:function(){this.options.scroll_top=this.scroll_elem.scrollTop;return Math.floor(this.options.scroll_top/(this.options.cluster_height-this.options.block_height))||0},generateEmptyRow:function(){var b=this.options;if(!b.tag||!b.show_no_data_row)return[];var a=document.createElement(b.tag),c=document.createTextNode(b.no_data_text);a.className=b.no_data_class;
+if("tr"==b.tag){var d=document.createElement("td");d.colSpan=100;d.appendChild(c)}a.appendChild(d||c);return[a.outerHTML]},generate:function(b,a){var c=this.options,d=b.length;if(d<c.rows_in_block)return{top_offset:0,bottom_offset:0,rows_above:0,rows:d?b:this.generateEmptyRow()};var f=Math.max((c.rows_in_cluster-c.rows_in_block)*a,0),h=f+c.rows_in_cluster,e=Math.max(f*c.item_height,0);c=Math.max((d-h)*c.item_height,0);d=[];var g=f;for(1>e&&g++;f<h;f++)b[f]&&d.push(b[f]);return{top_offset:e,bottom_offset:c,
+rows_above:g,rows:d}},renderExtraTag:function(b,a){var c=document.createElement(this.options.tag);c.className=["clusterize-extra-row","clusterize-"+b].join(" ");a&&(c.style.height=a+"px");return c.outerHTML},insertToDOM:function(b,a){this.options.cluster_height||this.exploreEnvironment(b,a);var c=this.generate(b,this.getClusterNum()),d=c.rows.join(""),f=this.checkChanges("data",d,a),h=this.checkChanges("top",c.top_offset,a),e=this.checkChanges("bottom",c.bottom_offset,a),g=this.options.callbacks,
+k=[];f||h?(c.top_offset&&(this.options.keep_parity&&k.push(this.renderExtraTag("keep-parity")),k.push(this.renderExtraTag("top-space",c.top_offset))),k.push(d),c.bottom_offset&&k.push(this.renderExtraTag("bottom-space",c.bottom_offset)),g.clusterWillChange&&g.clusterWillChange(),this.html(k.join("")),"ol"==this.options.content_tag&&this.content_elem.setAttribute("start",c.rows_above),this.content_elem.style["counter-increment"]="clusterize-counter "+(c.rows_above-1),g.clusterChanged&&g.clusterChanged()):
+e&&(this.content_elem.lastChild.style.height=c.bottom_offset+"px")},html:function(b){var a=this.content_elem;if(l&&9>=l&&"tr"==this.options.tag){var c=document.createElement("div");for(c.innerHTML="<table><tbody>"+b+"</tbody></table>";b=a.lastChild;)a.removeChild(b);for(c=this.getChildNodes(c.firstChild.firstChild);c.length;)a.appendChild(c.shift())}else a.innerHTML=b},getChildNodes:function(b){b=b.children;for(var a=[],c=0,d=b.length;c<d;c++)a.push(b[c]);return a},checkChanges:function(b,a,c){var d=
+a!=c[b];c[b]=a;return d}};return p});
\ No newline at end of file
padding 0
list-style none
- li
- span
- display inline-block
- min-width 3em
- margin-right 1em
- padding 0 0.25em
- font-family courier
- color #e5aa3d
+ .gcode ul
+ li:nth-child(even)
+ background-color #fafafa
+
+ li.highlight
+ background-color #eaeaea
- &.highlight
- background-color #eaeaea
+ span.gcode-line
+ display inline-block
+ padding 0 0.25em
+ color #e5aa3d
+ min-width 4em
.history ul li
cursor pointer