Verison 1.0.3 Release

Based on Buildbotics 0.4.14
This commit is contained in:
OneFinityCNC
2020-08-27 23:20:27 -04:00
parent 6137475077
commit 24dfa6c64d
302 changed files with 58865 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
/******************************************************************************\
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');
module.exports = {
template: '#admin-general-view-template',
props: ['config', 'state'],
data: function () {
return {
configRestored: false,
confirmReset: false,
configReset: false,
latest: '',
autoCheckUpgrade: true
}
},
events: {
latest_version: function (version) {this.latest = version}
},
ready: function () {
this.autoCheckUpgrade = this.config.admin['auto-check-upgrade']
},
methods: {
backup: function () {
document.getElementById('download-target').src = '/api/config/download';
},
restore_config: function () {
// If we don't reset the form the browser may cache file if name is same
// even if contents have changed
$('.restore-config')[0].reset();
$('.restore-config input').click();
},
restore: function (e) {
var files = e.target.files || e.dataTransfer.files;
if (!files.length) return;
var fr = new FileReader();
fr.onload = function (e) {
var config;
try {
config = JSON.parse(e.target.result);
} catch (ex) {
api.alert("Invalid config file");
return;
}
api.put('config/save', config).done(function (data) {
this.$dispatch('update');
this.configRestored = true;
}.bind(this)).fail(function (error) {
api.alert('Restore failed', error);
})
}.bind(this);
fr.readAsText(files[0]);
},
reset: function () {
this.confirmReset = false;
api.put('config/reset').done(function () {
this.$dispatch('update');
this.configReset = true;
}.bind(this)).fail(function (error) {
api.alert('Reset failed', error);
});
},
check: function () {this.$dispatch('check')},
upgrade: function () {this.$dispatch('upgrade')},
upload_firmware: function () {
// If we don't reset the form the browser may cache file if name is same
// even if contents have changed
$('.upload-firmware')[0].reset();
$('.upload-firmware input').click();
},
upload: function (e) {
var files = e.target.files || e.dataTransfer.files;
if (!files.length) return;
this.$dispatch('upload', files[0]);
},
change_auto_check_upgrade: function () {
this.config.admin['auto-check-upgrade'] = this.autoCheckUpgrade;
this.$dispatch('config-changed');
}
}
}

View File

@@ -0,0 +1,177 @@
/******************************************************************************\
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');
module.exports = {
template: '#admin-network-view-template',
props: ['config', 'state'],
data: function () {
return {
hostnameSet: false,
usernameSet: false,
passwordSet: false,
redirectTimeout: 0,
hostname: '',
username: '',
current: '',
password: '',
password2: '',
wifi_mode: 'client',
wifi_ssid: '',
wifi_ch: undefined,
wifi_pass: '',
wifiConfirm: false,
rebooting: false
}
},
ready: function () {
api.get('hostname').done(function (hostname) {
this.hostname = hostname;
}.bind(this));
api.get('remote/username').done(function (username) {
this.username = username;
}.bind(this));
api.get('wifi').done(function (config) {
this.wifi_mode = config.mode;
this.wifi_ssid = config.ssid;
this.wifi_ch = config.channel;
}.bind(this));
},
methods: {
redirect: function (hostname) {
if (0 < this.redirectTimeout) {
this.redirectTimeout -= 1;
setTimeout(function () {this.redirect(hostname)}.bind(this), 1000);
} else location.hostname = hostname;
},
set_hostname: function () {
api.put('hostname', {hostname: this.hostname}).done(function () {
this.redirectTimeout = 45;
this.hostnameSet = true;
api.put('reboot').always(function () {
if (String(location.hostname) == 'localhost') return;
var hostname = this.hostname;
if (String(location.hostname).endsWith('.local'))
hostname += '.local'
this.$dispatch('hostname-changed', hostname);
this.redirect(hostname);
}.bind(this));
}.bind(this)).fail(function (error) {
api.alert('Set hostname failed', error);
})
},
set_username: function () {
api.put('remote/username', {username: this.username}).done(function () {
this.usernameSet = true;
}.bind(this)).fail(function (error) {
api.alert('Set username failed', error);
})
},
set_password: function () {
if (this.password != this.password2) {
alert('Passwords to not match');
return;
}
if (this.password.length < 6) {
alert('Password too short');
return;
}
api.put('remote/password', {
current: this.current,
password: this.password
}).done(function () {
this.passwordSet = true;
}.bind(this)).fail(function (error) {
api.alert('Set password failed', error);
})
},
config_wifi: function () {
this.wifiConfirm = false;
if (!this.wifi_ssid.length) {
alert('SSID not set');
return;
}
if (32 < this.wifi_ssid.length) {
alert('SSID longer than 32 characters');
return;
}
if (this.wifi_pass.length && this.wifi_pass.length < 8) {
alert('WiFi password shorter than 8 characters');
return;
}
if (128 < this.wifi_pass.length) {
alert('WiFi password longer than 128 characters');
return;
}
this.rebooting = true;
var config = {
mode: this.wifi_mode,
channel: this.wifi_ch,
ssid: this.wifi_ssid,
pass: this.wifi_pass
}
api.put('wifi', config).fail(function (error) {
api.alert('Failed to configure WiFi', error);
this.rebooting = false;
}.bind(this))
}
}
}

104
src/js/api.js Normal file
View File

@@ -0,0 +1,104 @@
/******************************************************************************\
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 api_cb(method, url, data, config) {
config = $.extend({
type: method,
url: '/api/' + url,
dataType: 'json',
cache: false
}, config);
if (typeof data == 'object') {
config.data = JSON.stringify(data);
config.contentType = 'application/json; charset=utf-8';
}
var d = $.Deferred();
$.ajax(config).success(function (data, status, xhr) {
d.resolve(data, status, xhr);
}).error(function (xhr, status, error) {
var text = xhr.responseText;
try {text = $.parseJSON(xhr.responseText)} catch(e) {}
if (!text) text = error;
d.reject(text, xhr, status, error);
console.debug('API Error: ' + url + ': ' + text);
});
return d.promise();
}
module.exports = {
get: function (url, config) {
return api_cb('GET', url, undefined, config);
},
put: function(url, data, config) {
return api_cb('PUT', url, data, config);
},
post: function(url, data, config) {
return api_cb('POST', url, data, config);
},
upload: function(url, data, config) {
config = $.extend({
processData: false,
contentType: false,
cache: false,
data: data
}, config);
return api_cb('PUT', url, undefined, config);
},
'delete': function (url, config) {
return api_cb('DELETE', url, undefined, config);
},
alert: function (msg, error) {
if (typeof error != 'undefined') {
if (typeof error.message != 'undefined')
msg += '\n' + error.message;
else msg += '\n' + JSON.stringify(error);
}
alert(msg);
}
}

431
src/js/app.js Normal file
View File

@@ -0,0 +1,431 @@
/******************************************************************************\
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 cookie = require('./cookie')('bbctrl-');
var Sock = require('./sock');
const { exec } = require('child_process');
function compare_versions(a, b) {
var reStripTrailingZeros = /(\.0+)+$/;
var segsA = a.replace(reStripTrailingZeros, '').split('.');
var segsB = b.replace(reStripTrailingZeros, '').split('.');
var l = Math.min(segsA.length, segsB.length);
for (var i = 0; i < l; i++) {
var diff = parseInt(segsA[i], 10) - parseInt(segsB[i], 10);
if (diff) return diff;
}
return segsA.length - segsB.length;
}
function is_object(o) {return o !== null && typeof o == 'object'}
function is_array(o) {return Array.isArray(o)}
function update_array(dst, src) {
while (dst.length) dst.pop()
for (var i = 0; i < src.length; i++)
Vue.set(dst, i, src[i]);
}
function update_object(dst, src, remove) {
var props, index, key, value;
if (remove) {
props = Object.getOwnPropertyNames(dst);
for (index in props) {
key = props[index];
if (!src.hasOwnProperty(key))
Vue.delete(dst, key);
}
}
props = Object.getOwnPropertyNames(src);
for (index in props) {
key = props[index];
value = src[key];
if (is_array(value) && dst.hasOwnProperty(key) && is_array(dst[key]))
update_array(dst[key], value);
else if (is_object(value) && dst.hasOwnProperty(key) && is_object(dst[key]))
update_object(dst[key], value, remove);
else Vue.set(dst, key, value);
}
}
module.exports = new Vue({
el: 'body',
data: function () {
return {
status: 'connecting',
currentView: 'loading',
index: -1,
modified: false,
template: require('../resources/config-template.json'),
config: {
settings: {units: 'METRIC'},
motors: [{}, {}, {}, {}],
version: '<loading>'
},
state: {messages: []},
video_size: cookie.get('video-size', 'small'),
crosshair: cookie.get('crosshair', 'false') != 'false',
errorTimeout: 30,
errorTimeoutStart: 0,
errorShow: false,
errorMessage: '',
confirmUpgrade: false,
confirmUpload: false,
firmwareUpgrading: false,
checkedUpgrade: false,
firmwareName: '',
latestVersion: '',
password: '',
ipAddress: '0.0.0.0',
wifiSSID: ''
}
},
components: {
'estop': {template: '#estop-template'},
'loading-view': {template: '<h1>Loading...</h1>'},
'control-view': require('./control-view'),
'settings-view': require('./settings-view'),
'motor-view': require('./motor-view'),
'tool-view': require('./tool-view'),
'io-view': require('./io-view'),
'admin-general-view': require('./admin-general-view'),
'admin-network-view': require('./admin-network-view'),
'help-view': {template: '#help-view-template'},
'cheat-sheet-view': {
template: '#cheat-sheet-view-template',
data: function () {return {showUnimplemented: false}}
}
},
events: {
'config-changed': function () {this.modified = true;},
'hostname-changed': function (hostname) {this.hostname = hostname},
send: function (msg) {
if (this.status == 'connected') {
console.debug('>', msg);
this.sock.send(msg);
}
},
connected: function () {this.update()},
update: function () {this.update()},
check: function () {
this.latestVersion = '';
$.ajax({
type: 'GET',
url: 'https://https://raw.githubusercontent.com/OneFinityCNC/onefinity/master/latest.txt',
data: {hid: this.state.hid},
cache: false
}).done(function (data) {
this.latestVersion = data;
this.$broadcast('latest_version', data);
}.bind(this))
},
upgrade: function () {
this.password = '';
this.confirmUpgrade = true;
},
upload: function (firmware) {
this.firmware = firmware;
this.firmwareName = firmware.name;
this.password = '';
this.confirmUpload = true;
},
error: function (msg) {
// Honor user error blocking
if (Date.now() - this.errorTimeoutStart < this.errorTimeout * 1000)
return;
// Wait at least 1 sec to pop up repeated errors
if (1 < msg.repeat && Date.now() - msg.ts < 1000) return;
// Popup error dialog
this.errorShow = true;
this.errorMessage = msg.msg;
}
},
computed: {
popupMessages: function () {
var msgs = [];
for (var i = 0; i < this.state.messages.length; i++) {
var text = this.state.messages[i].text;
if (!/^#/.test(text)) msgs.push(text);
}
return msgs;
}
},
ready: function () {
$(window).on('hashchange', this.parse_hash);
this.connect();
},
methods: {
metric: function () {return this.config.settings.units != 'IMPERIAL'},
block_error_dialog: function () {
this.errorTimeoutStart = Date.now();
this.errorShow = false;
},
toggle_video: function (e) {
if (this.video_size == 'small') this.video_size = 'large';
else if (this.video_size == 'large') this.video_size = 'small';
cookie.set('video-size', this.video_size);
},
toggle_crosshair: function (e) {
e.preventDefault();
this.crosshair = !this.crosshair;
cookie.set('crosshair', this.crosshair);
},
estop: function () {
if (this.state.xx == 'ESTOPPED') api.put('clear');
else api.put('estop');
},
upgrade_confirmed: function () {
this.confirmUpgrade = false;
api.put('upgrade', {password: this.password}).done(function () {
this.firmwareUpgrading = true;
}.bind(this)).fail(function () {
api.alert('Invalid password');
}.bind(this))
},
upload_confirmed: function () {
this.confirmUpload = false;
var form = new FormData();
form.append('firmware', this.firmware);
if (this.password) form.append('password', this.password);
$.ajax({
url: '/api/firmware/update',
type: 'PUT',
data: form,
cache: false,
contentType: false,
processData: false
}).success(function () {
this.firmwareUpgrading = true;
}.bind(this)).error(function () {
api.alert('Invalid password or bad firmware');
}.bind(this))
},
show_upgrade: function () {
if (!this.latestVersion) return false;
return compare_versions(this.config.version, this.latestVersion) < 0;
},
update: function () {
api.get('config/load').done(function (config) {
update_object(this.config, config, true);
this.parse_hash();
if (!this.checkedUpgrade) {
this.checkedUpgrade = true;
var check = this.config.admin['auto-check-upgrade'];
if (typeof check == 'undefined' || check)
this.$emit('check');
}
this.check_ip_address();
this.check_ssid();
}.bind(this))
},
check_ip_address : function() {
$.ajax({
type: 'GET',
url: 'hostinfo.txt',
data: {hid: this.state.hid},
cache: false
}).done(function (data) {
console.debug('>', data);
this.ipAddress = data;
this.$broadcast('ipAddress', data);
}.bind(this))
},
check_ssid : function() {
$.ajax({
type: 'GET',
url: 'ssidinfo.txt',
data: {hid: this.state.hid},
cache: false
}).done(function (data) {
console.debug('>', data);
this.wifiSSID = data;
this.$broadcast('wifiSSID', data);
}.bind(this))
},
get_ip_address : function() {
console.debug('get_ip>', this.ipAddress);
return this.ipAddress;
},
get_ssid : function() {
console.debug('get_ssid>', this.wifiSSID);
return this.wifiSSID;
},
connect: function () {
this.sock = new Sock('//' + window.location.host + '/sockjs');
this.sock.onmessage = function (e) {
if (typeof e.data != 'object') return;
if ('log' in e.data) {
this.$broadcast('log', e.data.log);
delete e.data.log;
}
// Check for session ID change on controller
if ('sid' in e.data) {
if (typeof this.sid == 'undefined') this.sid = e.data.sid;
else if (this.sid != e.data.sid) {
if (typeof this.hostname != 'undefined' &&
String(location.hostname) != 'localhost')
location.hostname = this.hostname;
location.reload(true);
}
}
update_object(this.state, e.data, false);
this.$broadcast('update');
}.bind(this)
this.sock.onopen = function (e) {
this.status = 'connected';
this.$emit(this.status);
this.$broadcast(this.status);
}.bind(this)
this.sock.onclose = function (e) {
this.status = 'disconnected';
this.$emit(this.status);
this.$broadcast(this.status);
}.bind(this)
},
parse_hash: function () {
var hash = location.hash.substr(1);
if (!hash.trim().length) {
location.hash = 'control';
return;
}
var parts = hash.split(':');
if (parts.length == 2) this.index = parts[1];
this.currentView = parts[0];
},
save: function () {
api.put('config/save', this.config).done(function (data) {
this.modified = false;
}.bind(this)).fail(function (error) {
api.alert('Save failed', error);
});
},
close_messages: function (action) {
if (action == 'stop') api.put('stop');
if (action == 'continue') api.put('unpause');
// Acknowledge messages
if (this.state.messages.length) {
var id = this.state.messages.slice(-1)[0].id
api.put('message/' + id + '/ack');
}
}
}
})

65
src/js/axis-control.js Normal file
View File

@@ -0,0 +1,65 @@
/******************************************************************************\
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'
module.exports = {
template: '#axis-control-template',
props: ['axes', 'colors', 'enabled', 'adjust', 'step'],
methods: {
jog: function (axis, ring, direction) {
var value = direction * this.value(ring);
this.$dispatch(this.step ? 'step' : 'jog', this.axes[axis], value);
},
back2zero: function(axis0,axis1) {
this.$dispatch('back2zero',this.axes[axis0],this.axes[axis1])
},
release: function (axis) {
if (!this.step) this.$dispatch('jog', this.axes[axis], 0)
},
value: function (ring) {
var adjust = [0.01, 0.1, 1][this.adjust];
if (this.step) return adjust * [0.1, 1, 10, 100][ring];
return adjust * [0.1, 0.25, 0.5, 1][ring];
},
text: function (ring) {
var value = this.value(ring) * (this.step ? 1 : 100);
value = parseFloat(value.toFixed(3));
return value + (this.step ? '' : '%');
}
}
}

203
src/js/axis-vars.js Normal file
View File

@@ -0,0 +1,203 @@
/******************************************************************************\
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 enabled = typeof motor.enabled != 'undefined' && motor.enabled;
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 fault = this.state[motor_id + 'df'] & 0x1f;
var shutdown = this.state.power_shutdown;
var title;
if (fault || shutdown) {
state = shutdown ? 'SHUTDOWN' : 'FAULT';
klass += ' error';
icon = 'exclamation-circle';
} else 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;
case 'FAULT':
title = 'Motor driver fault. A potentially damaging electrical ' +
'condition was detected and the motor driver was shutdown. ' +
'Please power down the controller and check your motor cabling. ' +
'See the "Motor Faults" table on the "Indicators" tab for more ' +
'information.';
break;
case 'SHUTDOWN':
title = 'Motor power fault. All motors in shutdown. ' +
'See the "Power Faults" table on the "Indicators" tab for more ' +
'information. Reboot controller to reset.';
}
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
}
}
}
}

87
src/js/console.js Normal file
View File

@@ -0,0 +1,87 @@
/******************************************************************************\
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 _msg_equal(a, b) {
return a.level == b.level && a.source == b.source && a.where == b.where &&
a.msg == b.msg;
}
// Shared among all instances
var messages = [];
module.exports = {
template: '#console-template',
data: function () {
return {messages: messages}
},
events: {
log: function (msg) {
// There may be multiple instances of this module so ignore messages
// that have already been processed.
if (msg.logged) return;
msg.logged = true;
// Make sure we have a message level
msg.level = msg.level || 'info';
// Add to message log and count and collapse repeats
var repeat = messages.length && _msg_equal(msg, messages[0]);
if (repeat) messages[0].repeat++;
else {
msg.repeat = msg.repeat || 1;
messages.unshift(msg);
while (256 < messages.length) messages.pop();
}
msg.ts = Date.now();
// Write message to browser console for debugging
var text = JSON.stringify(msg);
if (msg.level == 'error' || msg.level == 'critical') console.error(text);
else if (msg.level == 'warning') console.warn(text);
else if (msg.level == 'debug' && console.debug) console.debug(text);
else console.log(text);
// Event on errors
if (msg.level == 'error' || msg.level == 'critical')
this.$dispatch('error', msg);
}
},
methods: {
clear: function () {messages.splice(0, messages.length);},
}
}

667
src/js/control-view.js Normal file
View File

@@ -0,0 +1,667 @@
/******************************************************************************\
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 cookie = require('./cookie')('bbctrl-');
function _is_array(x) {
return Object.prototype.toString.call(x) === '[object Array]';
}
function escapeHTML(s) {
var entityMap = {'&': '&amp;', '<': '&lt;', '>': '&gt;'};
return String(s).replace(/[&<>]/g, function (s) {return entityMap[s];});
}
module.exports = {
template: '#control-view-template',
props: ['config', 'template', 'state'],
data: function () {
return {
mach_units: 'METRIC',
mdi: '',
last_file: undefined,
last_file_time: undefined,
toolpath: {},
toolpath_progress: 0,
axes: 'xyzabc',
history: [],
speed_override: 1,
feed_override: 1,
manual_home: {x: false, y: false, z: false, a: false, b: false, c: false},
position_msg:
{x: false, y: false, z: false, a: false, b: false, c: false},
axis_position: 0,
jog_step: cookie.get_bool('jog-step'),
jog_adjust: parseInt(cookie.get('jog-adjust', 2)),
deleteGCode: false,
tab: 'auto',
jog_incr: 1.0,
tool_msg: false,
tool_diameter: 6.35
}
},
components: {
'axis-control': require('./axis-control'),
'path-viewer': require('./path-viewer'),
'gcode-viewer': require('./gcode-viewer')
},
watch: {
'state.imperial': {
handler: function (imperial) {
this.mach_units = imperial ? 'IMPERIAL' : 'METRIC';
},
immediate: true
},
mach_units: function (units) {
if ((units == 'METRIC') != this.metric)
this.send(units == 'METRIC' ? 'G21' : 'G20');
this.units_changed();
},
'state.line': function () {
if (this.mach_state != 'HOMING')
this.$broadcast('gcode-line', this.state.line);
},
'state.selected_time': function () {this.load()},
jog_step: function () {cookie.set_bool('jog-step', this.jog_step)},
jog_adjust: function () {cookie.set('jog-adjust', this.jog_adjust)}
},
computed: {
metric: function () {return !this.state.imperial},
mach_state: function () {
var cycle = this.state.cycle;
var state = this.state.xx;
if (typeof cycle != 'undefined' && state != 'ESTOPPED' &&
(cycle == 'jogging' || cycle == 'homing'))
return cycle.toUpperCase();
return state || ''
},
pause_reason: function () {return this.state.pr},
is_running: function () {
return this.mach_state == 'RUNNING' || this.mach_state == 'HOMING';
},
is_stopping: function () {return this.mach_state == 'STOPPING'},
is_holding: function () {return this.mach_state == 'HOLDING'},
is_ready: function () {return this.mach_state == 'READY'},
is_idle: function () {return this.state.cycle == 'idle'},
is_paused: function () {
return this.is_holding &&
(this.pause_reason == 'User pause' ||
this.pause_reason == 'Program pause')
},
can_mdi: function () {return this.is_idle || this.state.cycle == 'mdi'},
can_set_axis: function () {
return this.is_idle
// TODO allow setting axis position during pause
return this.is_idle || this.is_paused
},
message: function () {
if (this.mach_state == 'ESTOPPED') return this.state.er;
if (this.mach_state == 'HOLDING') return this.state.pr;
if (this.state.messages.length)
return this.state.messages.slice(-1)[0].text;
return '';
},
highlight_state: function () {
return this.mach_state == 'ESTOPPED' || this.mach_state == 'HOLDING';
},
plan_time: function () {return this.state.plan_time},
plan_time_remaining: function () {
if (!(this.is_stopping || this.is_running || this.is_holding)) return 0;
return this.toolpath.time - this.plan_time
},
eta: function () {
if (this.mach_state != 'RUNNING') return '';
var remaining = this.plan_time_remaining;
var d = new Date();
d.setSeconds(d.getSeconds() + remaining);
return d.toLocaleString();
},
progress: function () {
if (!this.toolpath.time || this.is_ready) return 0;
var p = this.plan_time / this.toolpath.time;
return p < 1 ? p : 1;
}
},
events: {
jog: function (axis, power) {
var data = {ts: new Date().getTime()};
data[axis] = power;
api.put('jog', data);
},
back2zero: function(axis0,axis1) {
this.send("G0"+axis0+"0"+axis1+"0");
},
step: function (axis, value) {
this.send('M70\nG91\nG0' + axis + value + '\nM72');
}
},
ready: function () {this.load()},
methods: {
units_changed : function() {
console.log("Units changed!");
if(this.mach_units == 'METRIC') {
document.getElementById("jog_button_fine").innerHTML = "0.1";
document.getElementById("jog_button_small").innerHTML = "1.0";
document.getElementById("jog_button_medium").innerHTML = "10";
document.getElementById("jog_button_large").innerHTML = "100";
this.tool_diameter = this.tool_diameter * 25.4;
this.tool_diameter = this.tool_diameter.toFixed(3);
} else {
document.getElementById("jog_button_fine").innerHTML = "0.005";
document.getElementById("jog_button_small").innerHTML = "0.05";
document.getElementById("jog_button_medium").innerHTML = "0.5";
document.getElementById("jog_button_large").innerHTML = "5";
this.tool_diameter = this.tool_diameter / 25.4;
this.tool_diameter = this.tool_diameter.toFixed(3);
}
this.set_jog_incr('small');
},
set_tool_diameter : function (new_diameter) {
if(isNaN(new_diameter))
return;
this.tool_msg = false;
this.tool_diameter = parseFloat(new_diameter);
this.probe_xyz();
},
set_jog_incr: function(newValue) {
//this.jog_incr = newValue;
document.getElementById("jog_button_fine").style.fontWeight = 'normal';
document.getElementById("jog_button_small").style.fontWeight = 'normal';
document.getElementById("jog_button_medium").style.fontWeight = 'normal';
document.getElementById("jog_button_large").style.fontWeight = 'normal';
if(newValue == 'fine')
{
document.getElementById("jog_button_fine").style.fontWeight = 'bold';
if(this.mach_units == 'METRIC')
this.jog_incr = 0.1;
else
this.jog_incr = 0.005;
} else if(newValue == 'small') {
document.getElementById("jog_button_small").style.fontWeight = 'bold';
if(this.mach_units == 'METRIC')
this.jog_incr = 1.0;
else
this.jog_incr = 0.05;
} else if(newValue == 'medium') {
document.getElementById("jog_button_medium").style.fontWeight = 'bold';
if(this.mach_units == 'METRIC')
this.jog_incr = 10;
else
this.jog_incr = 0.5;
} else if(newValue == 'large') {
document.getElementById("jog_button_large").style.fontWeight = 'bold';
if(this.mach_units == 'METRIC')
this.jog_incr = 100;
else
this.jog_incr = 5;
}
},
goto_zero(zero_x,zero_y,zero_z,zero_a) {
var xcmd = "";
var ycmd = "";
var zcmd = "";
var acmd = "";
if(zero_x) xcmd = "X0";
if(zero_y) ycmd = "Y0";
if(zero_z) zcmd = "Z0";
if(zero_a) acmd = "A0";
this.send('M70\nG90\nG0' + xcmd + ycmd + zcmd + acmd + '\nM72');
},
probe_xyz() {
var pcmd = "";
var xoffset = this.config.probe["probe-xdim"];
var yoffset = this.config.probe["probe-ydim"];
var zoffset = this.config.probe["probe-zdim"];
var fastSeek = this.config.probe["probe-fast-seek"];
var slowSeek = this.config.probe["probe-slow-seek"];
debugger;
if(this.mach_units == "METRIC") {
fastSeek = "F" + fastSeek;
slowSeek = "F" + slowSeek;
//Metric Probing
pcmd += "G92 X0\n";
pcmd += "G92 Y0\n";
pcmd += "G92 Z0\n";
pcmd += "G21\n";
pcmd += "G38.2 Z-25.4 " + fastSeek + "\n";
pcmd += "G91 G0 Z1.5\n";
pcmd += "G38.2 Z-2.5 " + slowSeek + "\n";
//var zoffset = 16.383;
pcmd += "G92 Z " + zoffset + "\n";
pcmd += "G91 G0 Z 3.175\n";
pcmd += "G91 G0 X 19.05\n";
pcmd += "G91 G0 Z -12.7\n";
pcmd += "G38.2 X -19.05 " + fastSeek + "\n";
pcmd += "G91 G1 X 1.27 " + fastSeek +"\n";
pcmd += "G38.2 X -4 " + slowSeek + "\n";
xoffset += this.tool_diameter/2.0;
xoffset = xoffset.toFixed(5);
pcmd += "G92 X " + xoffset + "\n";
pcmd += "G91 G0 X 2.5\n";
pcmd += "G91 G0 Y 17\n";
pcmd += "G91 G0 X -13\n";
pcmd += "G38.2 Y -17 " + fastSeek + "\n";
pcmd += "G91 G0 Y 1.27\n";
pcmd += "G38.2 Y -4 " + slowSeek +"\n";
yoffset += this.tool_diameter/2.0;
yoffset = yoffset.toFixed(5);
pcmd += "G92 Y " + yoffset + "\n";
pcmd += "G91 G0 Y2.54\n";
pcmd += "G91 G0 Z 25.4\n";
pcmd += "G90 G0 X0 Y0\n";
} else {
//Imperial Probing
xoffset = xoffset / 25.4;
yoffset = yoffset / 25.4;
zoffset = zoffset / 25.4;
slowSeek = slowSeek / 25.4;
slowSeek = slowSeek.toFixed(5);
slowSeek = "F" + slowSeek;
fastSeek = fastSeek / 25.4;
fastSeek = fastSeek.toFixed(5);
fastSeek = "F" + fastSeek;
pcmd += "G92 X0\n";
pcmd += "G92 Y0\n";
pcmd += "G92 Z0\n";
pcmd += "G20\n";
pcmd += "G38.2 Z-1.0 " + fastSeek + "\n";
pcmd += "G91 G0 Z0.06\n";
pcmd += "G38.2 Z-0.1 " + slowSeek + "\n";
//var zoffset = 0.645;
zoffset = zoffset.toFixed(5);
pcmd += "G92 Z " + zoffset + "\n";
pcmd += "G91 G0 Z 0.125\n";
pcmd += "G91 G0 X 0.75\n";
pcmd += "G91 G0 Z -0.5\n";
pcmd += "G38.2 X -0.75 " + fastSeek + "\n";
pcmd += "G91 G1 X 0.05 " + fastSeek + "\n";
pcmd += "G38.2 X -0.15 " + slowSeek + "\n";
xoffset += this.tool_diameter/2.0;
xoffset = xoffset.toFixed(5);
pcmd += "G92 X " + xoffset + "\n";
pcmd += "G91 G0 X 0.1\n";
pcmd += "G91 G0 Y 0.75\n";
pcmd += "G91 G0 X -0.5\n";
pcmd += "G38.2 Y -0.75 " + fastSeek + "\n";
pcmd += "G91 G0 Y 0.05\n";
pcmd += "G38.2 Y -0.15 " + slowSeek +"\n";
yoffset += this.tool_diameter/2.0;
yoffset = yoffset.toFixed(5);
pcmd += "G92 Y " + yoffset + "\n";
pcmd += "G91 G0 Y0.1\n";
pcmd += "G91 G0 Z1\n";
pcmd += "G90 G0 X0 Y0\n";
}
this.send(pcmd);
},
probe_z() {
var pcmd = "";
var fastSeek = this.config.probe["probe-fast-seek"];
var slowSeek = this.config.probe["probe-slow-seek"];
var zoffset = this.config.probe["probe-zdim"];
debugger;
if(this.mach_units == "METRIC") {
fastSeek = "F" + fastSeek;
slowSeek = "F" + slowSeek;
pcmd += "G92 Z0\n";
pcmd += "G21\n";
pcmd += "G38.2 Z-25 " + fastSeek + "\n";
pcmd += "G91 G0 Z1.5\n";
pcmd += "G38.2 Z-2.5 " + slowSeek + "\n";
pcmd += "G92 Z " + zoffset + "\n";
pcmd += "G91 G0 Z3\n";
} else {
zoffset = zoffset / 25.4;
slowSeek = slowSeek / 25.4;
slowSeek = slowSeek.toFixed(5);
slowSeek = "F" + slowSeek;
fastSeek = fastSeek / 25.4;
fastSeek = fastSeek.toFixed(5);
fastSeek = "F" + fastSeek;
pcmd += "G92 Z0\n";
pcmd += "G20\n";
pcmd += "G38.2 Z-1.0 " + fastSeek +"\n";
pcmd += "G91 G0 Z0.06\n";
pcmd += "G38.2 Z-0.1 " + slowSeek + "\n";
zoffset = zoffset.toFixed(5);
pcmd += "G92 Z " + zoffset + "\n";
pcmd += "G91 G0 Z0.125\n";
}
this.send(pcmd);
},
jog_fn: function (x_jog,y_jog,z_jog,a_jog) {
var xcmd = "X" + x_jog * this.jog_incr;
var ycmd = "Y" + y_jog * this.jog_incr;
var zcmd = "Z" + z_jog * this.jog_incr;
var acmd = "A" + a_jog * this.jog_incr;
console.log("Jog command: " + this.jog_incr);
//debugger;
this.send('M70\nG91\nG0' + xcmd + ycmd + zcmd + acmd + '\nM72');
},
send: function (msg) {this.$dispatch('send', msg)},
load: function () {
var file_time = this.state.selected_time;
var file = this.state.selected;
if (this.last_file == file && this.last_file_time == file_time) return;
this.last_file = file;
this.last_file_time = file_time;
this.$broadcast('gcode-load', file);
this.$broadcast('gcode-line', this.state.line);
this.toolpath_progress = 0;
this.load_toolpath(file, file_time);
},
load_toolpath: function (file, file_time) {
this.toolpath = {};
if (!file) return;
api.get('path/' + file).done(function (toolpath) {
if (this.last_file_time != file_time) return;
if (typeof toolpath.progress == 'undefined') {
toolpath.filename = file;
this.toolpath_progress = 1;
this.toolpath = toolpath;
var state = this.$root.state;
var bounds = toolpath.bounds;
for (var axis of 'xyzabc') {
Vue.set(state, 'path_min_' + axis, bounds.min[axis]);
Vue.set(state, 'path_max_' + axis, bounds.max[axis]);
}
} else {
this.toolpath_progress = toolpath.progress;
this.load_toolpath(file, file_time); // Try again
}
}.bind(this));
},
submit_mdi: function () {
this.send(this.mdi);
if (!this.history.length || this.history[0] != this.mdi)
this.history.unshift(this.mdi);
this.mdi = '';
},
mdi_start_pause: function () {
if (this.state.xx == 'RUNNING') this.pause();
else if (this.state.xx == 'STOPPING' || this.state.xx == 'HOLDING')
this.unpause();
else this.submit_mdi();
},
load_history: function (index) {this.mdi = this.history[index];},
open: function (e) {
// If we don't reset the form the browser may cache file if name is same
// even if contents have changed
$('.gcode-file-input')[0].reset();
$('.gcode-file-input input').click();
},
upload: function (e) {
var files = e.target.files || e.dataTransfer.files;
if (!files.length) return;
var file = files[0];
var fd = new FormData();
fd.append('gcode', file);
api.upload('file', fd)
.done(function () {
this.last_file_time = undefined; // Force reload
this.$broadcast('gcode-reload', file.name);
}.bind(this)).fail(function (error) {
api.alert('Upload failed', error)
}.bind(this));
},
delete_current: function () {
if (this.state.selected)
api.delete('file/' + this.state.selected);
this.deleteGCode = false;
},
delete_all: function () {
api.delete('file');
this.deleteGCode = false;
},
home: function (axis) {
if (typeof axis == 'undefined') api.put('home');
else {
if (this[axis].homingMode != 'manual') api.put('home/' + axis);
else this.manual_home[axis] = true;
}
},
set_home: function (axis, position) {
this.manual_home[axis] = false;
api.put('home/' + axis + '/set', {position: parseFloat(position)});
},
unhome: function (axis) {
this.position_msg[axis] = false;
api.put('home/' + axis + '/clear');
},
show_set_position: function (axis) {
this.axis_position = 0;
this.position_msg[axis] = true;
},
set_position: function (axis, position) {
this.position_msg[axis] = false;
api.put('position/' + axis, {'position': parseFloat(position)});
},
zero_all: function () {
for (var axis of 'xyzabc')
if (this[axis].enabled) this.zero(axis);
},
zero: function (axis) {
if (typeof axis == 'undefined') this.zero_all();
else this.set_position(axis, 0);
},
start_pause: function () {
if (this.state.xx == 'RUNNING') this.pause();
else if (this.state.xx == 'STOPPING' || this.state.xx == 'HOLDING')
this.unpause();
else this.start();
},
start: function () {api.put('start')},
pause: function () {api.put('pause')},
unpause: function () {api.put('unpause')},
optional_pause: function () {api.put('pause/optional')},
stop: function () {api.put('stop')},
step: function () {api.put('step')},
override_feed: function () {api.put('override/feed/' + this.feed_override)},
override_speed: function () {
api.put('override/speed/' + this.speed_override)
},
current: function (axis, value) {
var x = value / 32.0;
if (this.state[axis + 'pl'] == x) return;
var data = {};
data[axis + 'pl'] = x;
this.send(JSON.stringify(data));
}
},
mixins: [require('./axis-vars')]
}

69
src/js/cookie.js Normal file
View File

@@ -0,0 +1,69 @@
/******************************************************************************\
Copyright 2018. Buildbotics LLC
All Rights Reserved.
For information regarding this software email:
Joseph Coffland
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
as published by the Free Software Foundation, either version 2.1 of
the License, or (at your option) any later version.
This 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 C! library. If not, see
<http://www.gnu.org/licenses/>.
\******************************************************************************/
'use strict'
module.exports = function (prefix) {
if (typeof prefix == 'undefined') prefix = '';
var cookie = {
get: function (name, defaultValue) {
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
name = prefix + name + '=';
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1);
if (!c.indexOf(name)) return c.substring(name.length, c.length);
}
return defaultValue;
},
set: function (name, value, days) {
var offset = 2147483647; // Max value
if (typeof days != 'undefined') offset = days * 24 * 60 * 60 * 1000;
var d = new Date();
d.setTime(d.getTime() + offset);
var expires = 'expires=' + d.toUTCString();
document.cookie = prefix + name + '=' + value + ';' + expires + ';path=/';
},
set_bool: function (name, value) {
cookie.set(name, value ? 'true' : 'false');
},
get_bool: function (name, defaultValue) {
return cookie.get(name, defaultValue ? 'true' : 'false') == 'true';
}
}
return cookie;
}

172
src/js/gcode-viewer.js Normal file
View File

@@ -0,0 +1,172 @@
/******************************************************************************\
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 entityMap = {
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
'/': '&#x2F;', '`': '&#x60;', '=': '&#x3D;'}
function escapeHTML(s) {
return s.replace(/[&<>"'`=\/]/g, function (c) {return entityMap[c]})
}
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)}
},
ready: function () {
this.clusterize = new Clusterize({
rows: [],
scrollElem: $(this.$el).find('.clusterize-scroll')[0],
contentElem: $(this.$el).find('.clusterize-content')[0],
no_data_text: 'GCode view...',
callbacks: {clusterChanged: this.highlight}
});
},
attached: function () {
if (typeof this.clusterize != 'undefined')
this.clusterize.refresh(true);
},
methods: {
load: function (file) {
if (file == this.file) return;
this.clear();
this.file = file;
if (!file) return;
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/file/' + file + '?' + Math.random(), true);
xhr.responseType = 'text';
xhr.onload = function (e) {
if (this.file != file) return;
var lines = escapeHTML(xhr.response.trimRight()).split(/\r?\n/);
for (var i = 0; i < lines.length; i++) {
lines[i] = '<li class="ln' + (i + 1) + '">' +
'<b>' + (i + 1) + '</b>' + lines[i] + '</li>';
}
this.clusterize.update(lines);
this.empty = false;
Vue.nextTick(this.update_line);
}.bind(this)
xhr.send();
},
clear: function () {
this.empty = true;
this.file = '';
this.line = -1;
this.clusterize.clear();
},
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('.ln' + 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;
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);
}
}
}

File diff suppressed because one or more lines are too long

107
src/js/indicators.js Normal file
View File

@@ -0,0 +1,107 @@
/******************************************************************************\
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 modbus = require('./modbus.js');
module.exports = {
template: '#indicators-template',
props: ['state'],
computed: {
modbus_status: function () {return modbus.status_to_string(this.state.mx)},
sense_error: function () {
var error = '';
if (this.state.motor_voltage_sense_error) error += 'Motor voltage\n';
if (this.state.motor_current_sense_error) error += 'Motor current\n';
if (this.state.load1_sense_error) error += 'Load 1\n';
if (this.state.load2_sense_error) error += 'Load 2\n';
if (this.state.vdd_current_sense_error) error += 'Vdd current\n';
return error;
}
},
methods: {
is_motor_enabled: function (motor) {
return typeof this.state[motor + 'me'] != 'undefined' &&
this.state[motor + 'me'];
},
get_min_pin: function (motor) {
switch (motor) {
case 0: return 3;
case 1: return 5;
case 2: return 9;
case 3: return 11;
}
},
get_max_pin: function (motor) {
switch (motor) {
case 0: return 4;
case 1: return 8;
case 2: return 10;
case 3: return 12;
}
},
motor_fault_class: function (motor, bit) {
if (typeof motor == 'undefined') {
var status = this.state['fa'];
if (typeof status == 'undefined') return 'fa-question';
return 'fa-thumbs-' + (status ? 'down error' : 'up success')
}
var flags = this.state[motor + 'df'];
if (typeof flags == 'undefined') return 'fa-question';
return (flags & (1 << bit)) ? 'fa-thumbs-down error' :
'fa-thumbs-up success';
},
motor_reset: function (motor) {
if (typeof motor == 'undefined') {
var cmd = '';
for (var i = 0; i < 4; i++)
cmd += '\\$' + i + 'df=0\n';
this.$dispatch('send', cmd);
} else this.$dispatch('send', '\\$' + motor + 'df=0');
}
}
}

177
src/js/io-indicator.js Normal file
View File

@@ -0,0 +1,177 @@
/******************************************************************************\
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'
module.exports = {
template: "#io-indicator-template",
props: ['name', 'state'],
computed: {
klass: function () {
if (this.name == 'min-switch-0') return this.get_motor_min_class(0);
if (this.name == 'min-switch-1') return this.get_motor_min_class(1);
if (this.name == 'min-switch-2') return this.get_motor_min_class(2);
if (this.name == 'min-switch-3') return this.get_motor_min_class(3);
if (this.name == 'max-switch-0') return this.get_motor_max_class(0);
if (this.name == 'max-switch-1') return this.get_motor_max_class(1);
if (this.name == 'max-switch-2') return this.get_motor_max_class(2);
if (this.name == 'max-switch-3') return this.get_motor_max_class(3);
if (this.name == 'estop') return this.get_input_class('ew', 'et');
if (this.name == 'probe') return this.get_input_class('pw', 'pt');
if (this.name == 'load-1') return this.get_output_class('1');
if (this.name == 'load-2') return this.get_output_class('2');
if (this.name == 'fault') return this.get_output_class('f');
if (this.name == 'tool-enable-mode') return this.get_output_class('e');
if (this.name == 'tool-direction-mode') return this.get_output_class('d');
},
tooltip: function () {
if (this.name == 'min-switch-0') return this.get_motor_min_tooltip(0);
if (this.name == 'min-switch-1') return this.get_motor_min_tooltip(1);
if (this.name == 'min-switch-2') return this.get_motor_min_tooltip(2);
if (this.name == 'min-switch-3') return this.get_motor_min_tooltip(3);
if (this.name == 'max-switch-0') return this.get_motor_max_tooltip(0);
if (this.name == 'max-switch-1') return this.get_motor_max_tooltip(1);
if (this.name == 'max-switch-2') return this.get_motor_max_tooltip(2);
if (this.name == 'max-switch-3') return this.get_motor_max_tooltip(3);
if (this.name == 'estop') return this.get_input_tooltip('ew', 'et');
if (this.name == 'probe') return this.get_input_tooltip('pw', 'pt');
if (this.name == 'load-1') return this.get_output_tooltip('1');
if (this.name == 'load-2') return this.get_output_tooltip('2');
if (this.name == 'fault') return this.get_output_tooltip('f');
if (this.name == 'tool-direction-mode')
return this.get_output_tooltip('d');
if (this.name == 'tool-enable-mode')
return this.get_output_tooltip('e');
}
},
methods: {
get_io_state_class: function (active, state) {
if (typeof active == 'undefined' || typeof state == 'undefined')
return 'fa-exclamation-triangle warn';
if (state == 2) return 'fa-circle-o';
return (state ? 'fa-plus-circle' : 'fa-minus-circle') + ' ' +
(active ? 'active' : 'inactive');
},
get_input_active: function (stateCode, typeCode) {
var type = this.state[typeCode];
var state = this.state[stateCode];
if (type == 1) return !state; // Normally open
else if (type == 2) return state; // Normally closed
return false
},
get_input_class: function (stateCode, typeCode) {
return this.get_io_state_class(this.get_input_active(stateCode, typeCode),
this.state[stateCode]);
},
get_output_class: function (output) {
return this.get_io_state_class(this.state[output + 'oa'],
this.state[output + 'os']);
},
get_motor_min_class: function (motor) {
return this.get_input_class(motor + 'lw', motor + 'ls');
},
get_motor_max_class: function (motor) {
return this.get_input_class(motor + 'xw', motor + 'xs');
},
get_tooltip: function (mode, active, state) {
if (typeof mode == 'undefined' || typeof active == 'undefined' ||
typeof state == 'undefined') return 'Invalid';
if (state == 0) state = 'Lo/Gnd';
else if (state == 1) state = 'Hi/+3.3v';
else if (state == 2) state = 'Tristated';
else return 'Invalid';
return 'Mode: ' + mode + '\nActive: ' + (active ? 'True' : 'False') +
'\nLevel: ' + state;
},
get_input_tooltip: function (stateCode, typeCode) {
var type = this.state[typeCode];
if (type == 0) return 'Disabled';
else if (type == 1) type = 'Normally open';
else if (type == 2) type = 'Normally closed';
var active = this.get_input_active(stateCode, typeCode);
var state = this.state[stateCode];
return this.get_tooltip(type, active, state);
},
get_output_tooltip: function (output) {
var mode = this.state[output + 'om'];
if (mode == 0) return 'Disabled';
else if (mode == 1) mode = 'Lo/Hi';
else if (mode == 2) mode = 'Hi/Lo';
else if (mode == 3) mode = 'Tri/Lo';
else if (mode == 4) mode = 'Tri/Hi';
else if (mode == 5) mode = 'Lo/Tri';
else if (mode == 6) mode = 'Hi/Tri';
else mode = undefined;
var active = this.state[output + 'oa'];
var state = this.state[output + 'os'];
return this.get_tooltip(mode, active, state);
},
get_motor_min_tooltip: function (motor) {
return this.get_input_tooltip(motor + 'lw', motor + 'ls');
},
get_motor_max_tooltip: function (motor) {
return this.get_input_tooltip(motor + 'xw', motor + 'xs');
}
}
}

42
src/js/io-view.js Normal file
View File

@@ -0,0 +1,42 @@
/******************************************************************************\
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'
module.exports = {
template: '#io-view-template',
props: ['config', 'template', 'state'],
events: {
'input-changed': function() {
this.$dispatch('config-changed');
return false;
}
}
}

147
src/js/main.js Normal file
View File

@@ -0,0 +1,147 @@
/******************************************************************************\
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 cookie_get(name) {
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
name = name + '=';
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1);
if (!c.indexOf(name)) return c.substring(name.length, c.length);
}
}
function cookie_set(name, value, days) {
var d = new Date();
d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000);
var expires = 'expires=' + d.toUTCString();
document.cookie = name + '=' + value + ';' + expires + ';path=/';
}
var uuid_chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+';
function uuid(length) {
if (typeof length == 'undefined') length = 52;
var s = '';
for (var i = 0; i < length; i++)
s += uuid_chars[Math.floor(Math.random() * uuid_chars.length)];
return s
}
$(function() {
if (typeof cookie_get('client-id') == 'undefined')
cookie_set('client-id', uuid(), 10000);
// Vue debugging
Vue.config.debug = true;
//Vue.util.warn = function (msg) {console.debug('[Vue warn]: ' + msg)}
// Register global components
Vue.component('templated-input', require('./templated-input'));
Vue.component('message', require('./message'));
Vue.component('indicators', require('./indicators'));
Vue.component('io-indicator', require('./io-indicator'));
Vue.component('console', require('./console'));
Vue.component('unit-value', require('./unit-value'));
Vue.filter('number', function (value) {
if (isNaN(value)) return 'NaN';
return value.toLocaleString();
});
Vue.filter('percent', function (value, precision) {
if (typeof value == 'undefined') return '';
if (typeof precision == 'undefined') precision = 2;
return (value * 100.0).toFixed(precision) + '%';
});
Vue.filter('non_zero_percent', function (value, precision) {
if (!value) return '';
if (typeof precision == 'undefined') precision = 2;
return (value * 100.0).toFixed(precision) + '%';
});
Vue.filter('fixed', function (value, precision) {
if (typeof value == 'undefined') return '0';
return parseFloat(value).toFixed(precision)
});
Vue.filter('upper', function (value) {
if (typeof value == 'undefined') return '';
return value.toUpperCase()
});
Vue.filter('time', function (value, precision) {
if (isNaN(value)) return '';
if (isNaN(precision)) precision = 0;
var MIN = 60;
var HR = MIN * 60;
var DAY = HR * 24;
var parts = [];
if (DAY <= value) {
parts.push(Math.floor(value / DAY));
value %= DAY;
}
if (HR <= value) {
parts.push(Math.floor(value / HR));
value %= HR;
}
if (MIN <= value) {
parts.push(Math.floor(value / MIN));
value %= MIN;
} else parts.push(0);
parts.push(value);
for (var i = 0; i < parts.length; i++) {
parts[i] = parts[i].toFixed(i == parts.length - 1 ? precision : 0);
if (i && parts[i] < 10) parts[i] = '0' + parts[i];
}
return parts.join(':');
});
// Vue app
require('./app');
});

41
src/js/message.js Normal file
View File

@@ -0,0 +1,41 @@
/******************************************************************************\
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'
module.exports = {
template: '#message-template',
props: {
show: {
type: Boolean,
required: true,
twoWay: true
}
}
}

48
src/js/modbus-reg.js Normal file
View File

@@ -0,0 +1,48 @@
/******************************************************************************\
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'
module.exports = {
replace: true,
template: '#modbus-reg-view-template',
props: ['index', 'model', 'template', 'enable'],
computed: {
has_user_value: function () {
var type = this.model['reg-type'];
return type.indexOf('write') != -1 || type.indexOf('fixed') != -1;
}
},
methods: {
change: function () {this.$dispatch('input-changed')}
}
}

51
src/js/modbus.js Normal file
View File

@@ -0,0 +1,51 @@
/******************************************************************************\
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'
// Must match modbus.c
var exports = {
DISCONNECTED: 0,
OK: 1,
CRC: 2,
INVALID: 3,
TIMEDOUT: 4
};
exports.status_to_string =
function (status) {
if (status == exports.OK) return 'Ok';
if (status == exports.CRC) return 'CRC error';
if (status == exports.INVALID) return 'Invalid response';
if (status == exports.TIMEDOUT) return 'Timedout';
return 'Disconnected';
}
module.exports = exports;

136
src/js/motor-view.js Normal file
View File

@@ -0,0 +1,136 @@
/******************************************************************************\
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'
module.exports = {
template: '#motor-view-template',
props: ['index', 'config', 'template', 'state'],
computed: {
metric: function () {return this.$root.metric()},
is_slave: function () {
for (var i = 0; i < this.index; i++)
if (this.motor.axis == this.config.motors[i].axis)
return true;
return false;
},
motor: function () {return this.config.motors[this.index]},
invalidMaxVelocity: function () {
return this.maxMaxVelocity < this.motor['max-velocity'];
},
maxMaxVelocity: function () {
return 1 * (15 * this.umPerStep / this.motor['microsteps']).toFixed(3);
},
ustepPerSec: function () {
return this.rpm * this.stepsPerRev * this.motor['microsteps'] / 60;
},
rpm: function () {
return 1000 * this.motor['max-velocity'] / this.motor['travel-per-rev'];
},
gForce: function () {return this.motor['max-accel'] * 0.0283254504},
gForcePerMin: function () {return this.motor['max-jerk'] * 0.0283254504},
stepsPerRev: function () {return 360 / this.motor['step-angle']},
umPerStep: function () {
return this.motor['travel-per-rev'] * this.motor['step-angle'] / 0.36
},
milPerStep: function () {return this.umPerStep / 25.4},
invalidStallVelocity: function() {
if(!this.motor['homing-mode'].startsWith('stall-')) return false;
return this.maxStallVelocity < this.motor['search-velocity'];
},
stallRPM: function() {
var v = this.motor['search-velocity'];
return 1000 * v / this.motor['travel-per-rev'];
},
maxStallVelocity: function() {
var maxRate = 900000/this.motor['stall-sample-time'];
var ustep = this.motor['stall-microstep'];
var angle = this.motor['step-angle'];
var travel = this.motor['travel-per-rev'];
var maxStall = maxRate * 60/ 360 /1000 * angle/ ustep * travel;
return 1 * maxStall.toFixed(3);
},
stallUStepPerSec: function() {
var ustep = this.motor['stall-microstep'];
return this.stallRPM * this.stepsPerRev * ustep / 60;
}
},
events: {
'input-changed': function() {
Vue.nextTick(function () {
// Limit max-velocity
if (this.invalidMaxVelocity)
this.$set('motor["max-velocity"]', this.maxMaxVelocity);
//Limit stall-velocity
if(this.invalidStallVelocity)
this.$set('motor["search-velocity"]', this.maxStallVelocity);
this.$dispatch('config-changed');
}.bind(this))
return false;
}
},
methods: {
show: function(name, templ) {
if(templ.hmodes == undefined) return true;
return templ.hmodes.indexOf(this.motor['homing-mode']) != -1;
}
}
}

682
src/js/orbit.js Normal file
View File

@@ -0,0 +1,682 @@
/**
* @author qiao / https://github.com/qiao
* @author mrdoob / http://mrdoob.com
* @author alteredq / http://alteredqualia.com/
* @author WestLangley / http://github.com/WestLangley
* @author erich666 / http://erichaines.com
* @author jcoffland / https://buildbotics.com/
*/
'use strict'
// This set of controls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up
// (+Y by default).
//
// Orbit - left mouse / touch: one-finger move
// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
// Pan - right mouse, or arrow keys / touch: two-finger move
var OrbitControls = function (object, domElement) {
this.object = object;
this.domElement = domElement != undefined ? domElement : document;
// Set to false to disable this control
this.enabled = true;
// "target" sets the location of focus, where the object orbits around
this.target = new THREE.Vector3();
// How far you can zoom in and out (OrthographicCamera only)
this.minZoom = 0;
this.maxZoom = Infinity;
// How far you can orbit vertically, upper and lower limits.
// Range is 0 to Math.PI radians.
this.minPolarAngle = 0; // radians
this.maxPolarAngle = Math.PI; // radians
// How far you can orbit horizontally, upper and lower limits.
// If set, must be a sub-interval of the interval [- Math.PI, Math.PI].
this.minAzimuthAngle = -Infinity; // radians
this.maxAzimuthAngle = Infinity; // radians
// Set to true to enable damping (inertia)
// If damping is enabled, call controls.update() in your animation loop
this.enableDamping = false;
this.dampingFactor = 0.25;
// This option enables dollying in and out;
// left as "zoom" for backwards compatibility.
// Set to false to disable zooming
this.enableZoom = true;
this.zoomSpeed = 1.0;
// Set to false to disable rotating
this.enableRotate = true;
this.rotateSpeed = 1.0;
// Set to false to disable panning
this.enablePan = true;
this.panSpeed = 1.0;
this.screenSpacePanning = false; // if true, pan in screen-space
this.keyPanSpeed = 7.0; // pixels moved per arrow key push
// Set to true to automatically rotate around the target
// If auto-rotate is enabled, call controls.update() in your animation loop
this.autoRotate = false;
this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
// Set to false to disable use of the keys
this.enableKeys = true;
// The four arrow keys
this.keys = {LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40};
// Mouse buttons
this.mouseButtons = {
ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT
};
// for reset
this.target0 = this.target.clone();
this.position0 = this.object.position.clone();
this.zoom0 = this.object.zoom;
// public methods
this.getPolarAngle = function () {return spherical.phi}
this.getAzimuthalAngle = function () {return spherical.theta}
this.saveState = function () {
scope.target0.copy(scope.target);
scope.position0.copy(scope.object.position);
scope.zoom0 = scope.object.zoom;
}
this.reset = function () {
scope.target.copy(scope.target0);
scope.object.position.copy(scope.position0);
scope.object.zoom = scope.zoom0;
scope.object.updateProjectionMatrix();
scope.dispatchEvent(changeEvent);
scope.update();
state = STATE.NONE;
}
this.update = function () {
var offset = new THREE.Vector3();
// so camera.up is the orbit axis
var quat = new THREE.Quaternion()
.setFromUnitVectors(object.up, new THREE.Vector3(0, 1, 0));
var quatInverse = quat.clone().inverse();
var lastPosition = new THREE.Vector3();
var lastQuaternion = new THREE.Quaternion();
return function update() {
var position = scope.object.position;
offset.copy(position).sub(scope.target);
// rotate offset to "y-axis-is-up" space
offset.applyQuaternion(quat);
// angle from z-axis around y-axis
spherical.setFromVector3(offset);
if (scope.autoRotate && state == STATE.NONE)
rotateLeft(getAutoRotationAngle());
spherical.theta += sphericalDelta.theta;
spherical.phi += sphericalDelta.phi;
// restrict theta to be between desired limits
spherical.theta =
Math.max(scope.minAzimuthAngle,
Math.min(scope.maxAzimuthAngle, spherical.theta));
// restrict phi to be between desired limits
spherical.phi =
Math.max(scope.minPolarAngle,
Math.min(scope.maxPolarAngle, spherical.phi));
spherical.makeSafe();
spherical.radius *= scale;
// restrict radius to be between desired limits
spherical.radius =
Math.max(10, Math.min(scope.object.far * 0.8, spherical.radius));
// move target to panned location
scope.target.add(panOffset);
offset.setFromSpherical(spherical);
// rotate offset back to "camera-up-vector-is-up" space
offset.applyQuaternion(quatInverse);
position.copy(scope.target).add(offset);
scope.object.lookAt(scope.target);
if (scope.enableDamping) {
sphericalDelta.theta *= (1 - scope.dampingFactor);
sphericalDelta.phi *= (1 - scope.dampingFactor);
panOffset.multiplyScalar(1 - scope.dampingFactor);
} else {
sphericalDelta.set(0, 0, 0);
panOffset.set(0, 0, 0);
}
// update condition is:
// min(camera displacement, camera rotation in radians)^2 > EPS
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
if (zoomChanged || scale != 1 ||
lastPosition.distanceToSquared(scope.object.position) > EPS ||
8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) {
scope.dispatchEvent(changeEvent);
lastPosition.copy(scope.object.position);
lastQuaternion.copy(scope.object.quaternion);
zoomChanged = false;
scale = 1;
return true;
}
return false;
}
}()
this.dispose = function () {
scope.domElement.removeEventListener('contextmenu', onContextMenu, false);
scope.domElement.removeEventListener('mousedown', onMouseDown, false);
scope.domElement.removeEventListener('wheel', onMouseWheel, false);
scope.domElement.removeEventListener('touchstart', onTouchStart, false);
scope.domElement.removeEventListener('touchend', onTouchEnd, false);
scope.domElement.removeEventListener('touchmove', onTouchMove, false);
document.removeEventListener('mousemove', onMouseMove, false);
document.removeEventListener('mouseup', onMouseUp, false);
window.removeEventListener('keydown', onKeyDown, false);
}
// internals
var scope = this;
var changeEvent = {type: 'change'};
var startEvent = {type: 'start'};
var endEvent = {type: 'end'};
var STATE = {
NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY_PAN: 4
};
var state = STATE.NONE;
var EPS = 0.000001;
// current position in spherical coordinates
var spherical = new THREE.Spherical();
var sphericalDelta = new THREE.Spherical();
var scale = 1;
var panOffset = new THREE.Vector3();
var zoomChanged = false;
var rotateStart = new THREE.Vector2();
var rotateEnd = new THREE.Vector2();
var rotateDelta = new THREE.Vector2();
var panStart = new THREE.Vector2();
var panEnd = new THREE.Vector2();
var panDelta = new THREE.Vector2();
var dollyStart = new THREE.Vector2();
var dollyEnd = new THREE.Vector2();
var dollyDelta = new THREE.Vector2();
function getAutoRotationAngle() {
return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
}
function getZoomScale() {return Math.pow(0.95, scope.zoomSpeed)}
function rotateLeft(angle) {sphericalDelta.theta -= angle}
function rotateUp(angle) {sphericalDelta.phi -= angle}
var panLeft = function () {
var v = new THREE.Vector3();
return function panLeft(distance, objectMatrix) {
v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix
v.multiplyScalar(-distance);
panOffset.add(v);
}
}()
var panUp = function () {
var v = new THREE.Vector3();
return function panUp(distance, objectMatrix) {
if (scope.screenSpacePanning) v.setFromMatrixColumn(objectMatrix, 1);
else {
v.setFromMatrixColumn(objectMatrix, 0);
v.crossVectors(scope.object.up, v);
}
v.multiplyScalar(distance);
panOffset.add(v);
}
}()
function unknownCamera() {
console.warn('WARNING: OrbitControls.js encountered an unknown camera ' +
'type - pan & zoom disabled.');
scope.enablePan = false;
scope.enableZoom = false;
}
// deltaX and deltaY are in pixels; right and down are positive
var pan = function () {
var offset = new THREE.Vector3();
return function pan(deltaX, deltaY) {
var element = scope.domElement === document ?
scope.domElement.body : scope.domElement;
if (scope.object.isPerspectiveCamera) {
// perspective
offset.copy(scope.object.position).sub(scope.target);
var targetDistance = offset.length();
// half of the fov is center to top of screen
targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0);
// we use only clientHeight here so aspect ratio does not distort speed
panLeft(2 * deltaX * targetDistance / element.clientHeight,
scope.object.matrix);
panUp(2 * deltaY * targetDistance / element.clientHeight,
scope.object.matrix);
} else if (scope.object.isOrthographicCamera) {
// orthographic
panLeft(deltaX * (scope.object.right - scope.object.left) /
scope.object.zoom / element.clientWidth, scope.object.matrix);
panUp(deltaY * (scope.object.top - scope.object.bottom) /
scope.object.zoom / element.clientHeight, scope.object.matrix);
} else unknownCamera();
}
}()
function dollyIn(dollyScale) {
if (scope.object.isPerspectiveCamera) scale /= dollyScale;
else if (scope.object.isOrthographicCamera) {
scope.object.zoom =
Math.max(scope.minZoom,
Math.min(scope.maxZoom, scope.object.zoom * dollyScale));
scope.object.updateProjectionMatrix();
zoomChanged = true;
} else unknownCamera();
}
function dollyOut(dollyScale) {
if (scope.object.isPerspectiveCamera) scale *= dollyScale;
else if (scope.object.isOrthographicCamera) {
scope.object.zoom =
Math.max(scope.minZoom,
Math.min(scope.maxZoom, scope.object.zoom / dollyScale));
scope.object.updateProjectionMatrix();
zoomChanged = true;
} else unknownCamera();
}
// event callbacks - update the object state
function handleMouseDownRotate(event) {
rotateStart.set(event.clientX, event.clientY);
}
function handleMouseDownDolly(event) {
dollyStart.set(event.clientX, event.clientY);
}
function handleMouseDownPan(event) {
panStart.set(event.clientX, event.clientY);
}
function handleMouseMoveRotate(event) {
rotateEnd.set(event.clientX, event.clientY);
rotateDelta.subVectors(rotateEnd, rotateStart)
.multiplyScalar(scope.rotateSpeed);
var element = scope.domElement === document ?
scope.domElement.body : scope.domElement;
// yes, height
rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight);
rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight);
rotateStart.copy(rotateEnd);
scope.update();
}
function handleMouseMoveDolly(event) {
dollyEnd.set(event.clientX, event.clientY);
dollyDelta.subVectors(dollyEnd, dollyStart);
if (dollyDelta.y > 0) dollyIn(getZoomScale());
else if (dollyDelta.y < 0) dollyOut(getZoomScale());
dollyStart.copy(dollyEnd);
scope.update();
}
function handleMouseMovePan(event) {
panEnd.set(event.clientX, event.clientY);
panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed);
pan(panDelta.x, panDelta.y);
panStart.copy(panEnd);
scope.update();
}
function handleMouseUp(event) {}
function handleMouseWheel(event) {
if (event.deltaY < 0) dollyOut(getZoomScale());
else if (event.deltaY > 0) dollyIn(getZoomScale());
scope.update();
}
function handleKeyDown(event) {
switch (event.keyCode) {
case scope.keys.UP:
pan(0, scope.keyPanSpeed);
scope.update();
break;
case scope.keys.BOTTOM:
pan(0, -scope.keyPanSpeed);
scope.update();
break;
case scope.keys.LEFT:
pan(scope.keyPanSpeed, 0);
scope.update();
break;
case scope.keys.RIGHT:
pan(-scope.keyPanSpeed, 0);
scope.update();
break;
}
}
function handleTouchStartRotate(event) {
rotateStart.set(event.touches[0].pageX, event.touches[0].pageY);
}
function handleTouchStartDollyPan(event) {
if (scope.enableZoom) {
var dx = event.touches[0].pageX - event.touches[1].pageX;
var dy = event.touches[0].pageY - event.touches[1].pageY;
var distance = Math.sqrt(dx * dx + dy * dy);
dollyStart.set(0, distance);
}
if (scope.enablePan) {
var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
panStart.set(x, y);
}
}
function handleTouchMoveRotate(event) {
rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY);
rotateDelta.subVectors(rotateEnd, rotateStart)
.multiplyScalar(scope.rotateSpeed);
var element = scope.domElement === document ?
scope.domElement.body : scope.domElement;
// yes, height
rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight);
rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight);
rotateStart.copy(rotateEnd);
scope.update();
}
function handleTouchMoveDollyPan(event) {
if (scope.enableZoom) {
var dx = event.touches[0].pageX - event.touches[1].pageX;
var dy = event.touches[0].pageY - event.touches[1].pageY;
var distance = Math.sqrt(dx * dx + dy * dy);
dollyEnd.set(0, distance);
dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed));
dollyIn(dollyDelta.y);
dollyStart.copy(dollyEnd);
}
if (scope.enablePan) {
var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
panEnd.set(x, y);
panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed);
pan(panDelta.x, panDelta.y);
panStart.copy(panEnd);
}
scope.update();
}
function handleTouchEnd(event) {}
// event handlers - listen for events and reset state
function onMouseDown(event) {
if (!scope.enabled) return;
event.preventDefault();
switch (event.button) {
case scope.mouseButtons.ORBIT:
if (!scope.enableRotate) return;
handleMouseDownRotate(event);
state = STATE.ROTATE;
break;
case scope.mouseButtons.ZOOM:
if (!scope.enableZoom) return;
handleMouseDownDolly(event);
state = STATE.DOLLY;
break;
case scope.mouseButtons.PAN:
if (!scope.enablePan) return;
handleMouseDownPan(event);
state = STATE.PAN;
break;
}
if (state != STATE.NONE) {
document.addEventListener('mousemove', onMouseMove, false);
document.addEventListener('mouseup', onMouseUp, false);
scope.dispatchEvent(startEvent);
}
}
function onMouseMove(event) {
if (!scope.enabled) return;
event.preventDefault();
switch (state) {
case STATE.ROTATE:
if (!scope.enableRotate) return;
handleMouseMoveRotate(event);
break;
case STATE.DOLLY:
if (!scope.enableZoom) return;
handleMouseMoveDolly(event);
break;
case STATE.PAN:
if (!scope.enablePan) return;
handleMouseMovePan(event);
break;
}
}
function onMouseUp(event) {
if (!scope.enabled) return;
handleMouseUp(event);
document.removeEventListener('mousemove', onMouseMove, false);
document.removeEventListener('mouseup', onMouseUp, false);
scope.dispatchEvent(endEvent);
state = STATE.NONE;
}
function onMouseWheel(event) {
if (!scope.enabled || !scope.enableZoom ||
(state != STATE.NONE && state != STATE.ROTATE)) return;
event.preventDefault();
event.stopPropagation();
scope.dispatchEvent(startEvent);
handleMouseWheel(event);
scope.dispatchEvent(endEvent);
}
function onKeyDown(event) {
if (!scope.enabled || !scope.enableKeys || !scope.enablePan) return;
handleKeyDown(event);
}
function onTouchStart(event) {
if (!scope.enabled) return;
event.preventDefault();
switch (event.touches.length) {
case 1: // one-fingered touch: rotate
if (!scope.enableRotate) return;
handleTouchStartRotate(event);
state = STATE.TOUCH_ROTATE;
break;
case 2: // two-fingered touch: dolly-pan
if (!scope.enableZoom && !scope.enablePan) return;
handleTouchStartDollyPan(event);
state = STATE.TOUCH_DOLLY_PAN;
break;
default: state = STATE.NONE;
}
if (state != STATE.NONE) scope.dispatchEvent(startEvent);
}
function onTouchMove(event) {
if (!scope.enabled) return;
event.preventDefault();
event.stopPropagation();
switch (event.touches.length) {
case 1: // one-fingered touch: rotate
if (!scope.enableRotate) return;
if (state != STATE.TOUCH_ROTATE) return; // is this needed?
handleTouchMoveRotate(event);
break;
case 2: // two-fingered touch: dolly-pan
if (!scope.enableZoom && !scope.enablePan) return;
if (state != STATE.TOUCH_DOLLY_PAN) return; // is this needed?
handleTouchMoveDollyPan(event);
break;
default: state = STATE.NONE;
}
}
function onTouchEnd(event) {
if (!scope.enabled) return;
handleTouchEnd(event);
scope.dispatchEvent(endEvent);
state = STATE.NONE;
}
function onContextMenu(event) {
if (!scope.enabled) return;
event.preventDefault();
}
scope.domElement.addEventListener('contextmenu', onContextMenu, false);
scope.domElement.addEventListener('mousedown', onMouseDown, false);
scope.domElement.addEventListener('wheel', onMouseWheel, false);
scope.domElement.addEventListener('touchstart', onTouchStart, false);
scope.domElement.addEventListener('touchend', onTouchEnd, false);
scope.domElement.addEventListener('touchmove', onTouchMove, false);
window .addEventListener('keydown', onKeyDown, false);
this.update(); // force an update at start
}
OrbitControls.prototype = Object.create(THREE.EventDispatcher.prototype);
OrbitControls.prototype.constructor = OrbitControls;
module.exports = OrbitControls;

759
src/js/path-viewer.js Normal file
View File

@@ -0,0 +1,759 @@
/******************************************************************************\
Copyright 2018. Buildbotics LLC
All Rights Reserved.
For information regarding this software email:
Joseph Coffland
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
as published by the Free Software Foundation, either version 2.1 of
the License, or (at your option) any later version.
This 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 C! library. If not, see
<http://www.gnu.org/licenses/>.
\******************************************************************************/
'use strict'
var orbit = require('./orbit');
var cookie = require('./cookie')('bbctrl-');
var api = require('./api');
var font = require('./helvetiker_regular.typeface.json')
function get(obj, name, defaultValue) {
return typeof obj[name] == 'undefined' ? defaultValue : obj[name];
}
var surfaceModes = ['cut', 'wire', 'solid', 'off'];
module.exports = {
template: '#path-viewer-template',
props: ['toolpath'],
data: function () {
return {
enabled: false,
loading: false,
dirty: true,
snapView: cookie.get('snap-view', 'isometric'),
small: cookie.get_bool('small-path-view', true),
surfaceMode: 'cut',
showPath: cookie.get_bool('show-path', true),
showTool: cookie.get_bool('show-tool', true),
showBBox: cookie.get_bool('show-bbox', true),
showAxes: cookie.get_bool('show-axes', true),
showIntensity: cookie.get_bool('show-intensity', false)
}
},
computed: {
target: function () {return $(this.$el).find('.path-viewer-content')[0]}
},
watch: {
toolpath: function () {Vue.nextTick(this.update)},
surfaceMode: function (mode) {this.update_surface_mode(mode)},
small: function (enable) {
cookie.set_bool('small-path-view', enable);
Vue.nextTick(this.update_view)
},
showPath: function (enable) {
cookie.set_bool('show-path', enable);
this.set_visible(this.pathView, enable)
},
showTool: function (enable) {
cookie.set_bool('show-tool', enable);
this.set_visible(this.toolView, enable)
},
showAxes: function (enable) {
cookie.set_bool('show-axes', enable);
this.set_visible(this.axesView, enable)
},
showIntensity: function (enable) {
cookie.set_bool('show-intensity', enable);
Vue.nextTick(this.update)
},
showBBox: function (enable) {
cookie.set_bool('show-bbox', enable);
this.set_visible(this.bboxView, enable);
this.set_visible(this.envelopeView, enable);
},
x: function () {this.axis_changed()},
y: function () {this.axis_changed()},
z: function () {this.axis_changed()}
},
ready: function () {
this.graphics();
Vue.nextTick(this.update);
},
methods: {
update: function () {
if (!this.state.selected) {
this.dirty = true;
this.scene = new THREE.Scene();
} else if (!this.toolpath.filename && !this.loading) {
this.loading = true;
this.dirty = true;
this.draw_loading();
}
if (!this.enabled || !this.toolpath.filename) return;
function get(url) {
var d = $.Deferred();
var xhr = new XMLHttpRequest();
xhr.open('GET', url + '?' + Math.random(), true);
xhr.responseType = 'arraybuffer';
xhr.onload = function (e) {
if (xhr.response) d.resolve(new Float32Array(xhr.response));
else d.reject();
};
xhr.send();
return d.promise();
}
var d1 = get('/api/path/' + this.toolpath.filename + '/positions');
var d2 = get('/api/path/' + this.toolpath.filename + '/speeds');
$.when(d1, d2).done(function (positions, speeds) {
this.positions = positions
this.speeds = speeds;
this.loading = false;
// Update scene
this.scene = new THREE.Scene();
this.draw(this.scene);
this.snap(this.snapView);
this.update_view();
}.bind(this))
},
update_surface_mode: function (mode) {
if (!this.enabled) return;
if (typeof this.surfaceMaterial != 'undefined') {
this.surfaceMaterial.wireframe = mode == 'wire';
this.surfaceMaterial.needsUpdate = true;
}
this.set_visible(this.surfaceMesh, mode == 'cut' || mode == 'wire');
this.set_visible(this.workpieceMesh, mode == 'solid');
},
load_surface: function (surface) {
if (typeof surface == 'undefined') {
this.vertices = undefined;
this.normals = undefined;
return;
}
this.vertices = surface.vertices;
// Expand normals
this.normals = [];
for (var i = 0; i < surface.normals.length / 3; i++)
for (var j = 0; j < 3; j++)
for (var k = 0; k < 3; k++)
this.normals.push(surface.normals[i * 3 + k]);
},
set_visible: function (target, visible) {
if (typeof target != 'undefined') target.visible = visible;
this.dirty = true;
},
get_dims: function () {
var t = $(this.target);
var width = t.innerWidth();
var height = t.innerHeight();
return {width: width, height: height};
},
update_view: function () {
if (!this.enabled) return;
var dims = this.get_dims();
this.camera.aspect = dims.width / dims.height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(dims.width, dims.height);
if (this.loading) {
this.controls.reset();
this.camera.position.copy(new THREE.Vector3(0, 0, 600));
this.camera.lookAt(new THREE.Vector3(0, 0, 0));
}
this.dirty = true;
},
update_tool: function (tool) {
if (!this.enabled) return;
if (typeof tool == 'undefined') tool = this.toolView;
if (typeof tool == 'undefined') return;
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.envelopeView;
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();
this.dirty = true;
},
graphics: function () {
try {
// Renderer
this.renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setClearColor(0, 0);
this.target.appendChild(this.renderer.domElement);
} catch (e) {
console.log('WebGL not supported: ', e);
return;
}
this.enabled = true;
// Camera
this.camera = new THREE.PerspectiveCamera(45, 4 / 3, 1, 10000);
// Lighting
this.ambient = new THREE.AmbientLight(0xffffff, 0.5);
var keyLight = new THREE.DirectionalLight
(new THREE.Color('hsl(30, 100%, 75%)'), 0.75);
keyLight.position.set(-100, 0, 100);
var fillLight = new THREE.DirectionalLight
(new THREE.Color('hsl(240, 100%, 75%)'), 0.25);
fillLight.position.set(100, 0, 100);
var backLight = new THREE.DirectionalLight(0xffffff, 0.5);
backLight.position.set(100, 0, -100).normalize();
this.lights = new THREE.Group();
this.lights.add(keyLight);
this.lights.add(fillLight);
this.lights.add(backLight);
// Surface material
this.surfaceMaterial = this.create_surface_material();
// Controls
this.controls = new orbit(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.2;
this.controls.rotateSpeed = 0.25;
this.controls.enableZoom = true;
// Move lights with scene
this.controls.addEventListener('change', function (scope) {
return function () {
keyLight.position.copy(scope.camera.position);
fillLight.position.copy(scope.camera.position);
backLight.position.copy(scope.camera.position);
keyLight.lookAt(scope.controls.target);
fillLight.lookAt(scope.controls.target);
backLight.lookAt(scope.controls.target);
scope.dirty = true;
}
}(this))
// Events
window.addEventListener('resize', this.update_view, false);
// Start it
this.render();
},
create_surface_material: function () {
return new THREE.MeshPhongMaterial({
specular: 0x111111,
shininess: 10,
side: THREE.FrontSide,
color: 0x0c2d53
});
},
draw_loading: function () {
this.scene = new THREE.Scene();
var geometry = new THREE.TextGeometry('Loading 3D View...', {
font: new THREE.Font(font),
size: 40,
height: 5,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 10,
bevelSize: 8,
bevelSegments: 5
});
geometry.computeBoundingBox();
var center = geometry.center();
var mesh = new THREE.Mesh(geometry, this.surfaceMaterial);
this.scene.add(mesh);
this.scene.add(this.ambient);
this.scene.add(this.lights);
this.update_view();
},
draw_workpiece: function (scene, material) {
if (typeof this.workpiece == 'undefined') return;
var min = this.workpiece.min;
var max = this.workpiece.max;
min = new THREE.Vector3(min[0], min[1], min[2]);
max = new THREE.Vector3(max[0], max[1], max[2]);
var dims = max.clone().sub(min);
var geometry = new THREE.BoxGeometry(dims.x, dims.y, dims.z)
var mesh = new THREE.Mesh(geometry, material);
var offset = dims.clone();
offset.divideScalar(2);
offset.add(min);
mesh.position.add(offset);
geometry.computeBoundingBox();
scene.add(mesh);
return mesh;
},
draw_surface: function (scene, material) {
if (typeof this.vertices == 'undefined') return;
var geometry = new THREE.BufferGeometry();
geometry.addAttribute
('position', new THREE.Float32BufferAttribute(this.vertices, 3));
geometry.addAttribute
('normal', new THREE.Float32BufferAttribute(this.normals, 3));
geometry.computeBoundingSphere();
geometry.computeBoundingBox();
return new THREE.Mesh(geometry, material);
},
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;
if (length < 1) length = 1;
var material = new THREE.MeshPhongMaterial({
transparent: true,
opacity: 0.75,
specular: 0x161616,
shininess: 10,
color: 0xffa500 // Orange
});
var geometry = new THREE.CylinderGeometry(length / 2, 0, length, 128);
geometry.translate(0, length / 2, 0);
geometry.rotateX(0.5 * Math.PI);
var mesh = new THREE.Mesh(geometry, material);
this.update_tool(mesh);
mesh.visible = this.showTool;
scene.add(mesh);
return mesh;
},
draw_axis: function (axis, up, length, radius) {
var color;
if (axis == 0) color = 0xff0000; // Red
else if (axis == 1) color = 0x00ff00; // Green
else if (axis == 2) color = 0x0000ff; // Blue
var group = new THREE.Group();
var material = new THREE.MeshPhongMaterial({
specular: 0x161616, shininess: 10, color: color
});
var geometry = new THREE.CylinderGeometry(radius, radius, length, 128);
geometry.translate(0, -length / 2, 0);
group.add(new THREE.Mesh(geometry, material));
geometry = new THREE.CylinderGeometry(1.5 * radius, 0, 2 * radius, 128);
geometry.translate(0, -length - radius, 0);
group.add(new THREE.Mesh(geometry, material));
if (axis == 0) group.rotateZ((up ? 0.5 : 1.5) * Math.PI);
else if (axis == 1) group.rotateX((up ? 0 : 1 ) * Math.PI);
else if (axis == 2) group.rotateX((up ? 1.5 : 0.5) * Math.PI);
return group;
},
draw_axes: function (scene, bbox) {
var size = bbox.getSize(new THREE.Vector3());
var length = (size.x + size.y + size.z) / 3;
length /= 10;
if (length < 1) length = 1;
var radius = length / 20;
var group = new THREE.Group();
for (var axis = 0; axis < 3; axis++)
for (var up = 0; up < 2; up++)
group.add(this.draw_axis(axis, up, length, radius));
group.visible = this.showAxes;
scene.add(group);
return group;
},
get_color: function (speed) {
if (isNaN(speed)) return [255, 0, 0]; // Rapid
var intensity = speed / this.toolpath.maxSpeed;
if (typeof speed == 'undefined' || !this.showIntensity) intensity = 1;
return [0, 255 * intensity, 127 * (1 - intensity)];
},
draw_path: function (scene) {
var geometry = new THREE.BufferGeometry();
var material =
new THREE.LineBasicMaterial({
vertexColors: THREE.VertexColors,
linewidth: 1.5
});
var positions = new THREE.Float32BufferAttribute(this.positions, 3);
geometry.addAttribute('position', positions);
var colors = [];
for (var i = 0; i < this.speeds.length; i++) {
var color = this.get_color(this.speeds[i]);
Array.prototype.push.apply(colors, color);
}
colors = new THREE.Uint8BufferAttribute(colors, 3, true);
geometry.addAttribute('color', colors);
geometry.computeBoundingSphere();
geometry.computeBoundingBox();
var line = new THREE.Line(geometry, material);
line.visible = this.showPath;
scene.add(line);
return line;
},
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 = [];
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();
geometry.addAttribute('position',
new THREE.Float32BufferAttribute(vertices, 3));
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;
scene.add(line);
return line;
},
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.pathView = 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.toolView = this.draw_tool(scene, bbox);
this.axesView = this.draw_axes(scene, bbox);
this.bboxView = this.draw_bbox(scene, bbox);
this.envelopeView = this.draw_envelope(scene);
},
render: function () {
window.requestAnimationFrame(this.render);
if (typeof this.scene == 'undefined') return;
if (this.controls.update() || this.dirty) {
this.dirty = false;
this.renderer.render(this.scene, this.camera);
}
},
get_model_bounds: function () {
var bbox = new THREE.Box3(new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0.00001, 0.00001, 0.00001));
function add(o) {
if (typeof o != 'undefined') {
var oBBox = new THREE.Box3();
oBBox.setFromObject(o);
bbox.union(oBBox);
}
}
add(this.pathView);
add(this.surfaceMesh);
add(this.workpieceMesh);
return bbox;
},
snap: function (view) {
if (this.loading) return;
if (view != this.snapView) {
this.snapView = view;
cookie.set('snap-view', view);
}
var bbox = this.get_model_bounds();
this.controls.reset();
bbox.getCenter(this.controls.target);
this.update_view();
// Compute new camera position
var center = bbox.getCenter(new THREE.Vector3());
var offset = new THREE.Vector3();
if (view == 'isometric') {offset.y -= 1; offset.z += 1;}
if (view == 'front') offset.y -= 1;
if (view == 'back') offset.y += 1;
if (view == 'left') offset.x -= 1;
if (view == 'right') offset.x += 1;
if (view == 'top') offset.z += 1;
if (view == 'bottom') offset.z -= 1;
offset.normalize();
// Initial camera position
var position = new THREE.Vector3().copy(center).add(offset);
this.camera.position.copy(position);
this.camera.lookAt(center); // Get correct camera orientation
var theta = this.camera.fov / 180 * Math.PI; // View angle
var cameraLine = new THREE.Line3(center, position);
var cameraUp = new THREE.Vector3().copy(this.camera.up)
.applyQuaternion(this.camera.quaternion);
var cameraLeft =
new THREE.Vector3().copy(offset).cross(cameraUp).normalize();
var corners = [
new THREE.Vector3(bbox.min.x, bbox.min.y, bbox.min.z),
new THREE.Vector3(bbox.min.x, bbox.min.y, bbox.max.z),
new THREE.Vector3(bbox.min.x, bbox.max.y, bbox.min.z),
new THREE.Vector3(bbox.min.x, bbox.max.y, bbox.max.z),
new THREE.Vector3(bbox.max.x, bbox.min.y, bbox.min.z),
new THREE.Vector3(bbox.max.x, bbox.min.y, bbox.max.z),
new THREE.Vector3(bbox.max.x, bbox.max.y, bbox.min.z),
new THREE.Vector3(bbox.max.x, bbox.max.y, bbox.max.z),
]
var dist = this.camera.near; // Min camera dist
for (var i = 0; i < corners.length; i++) {
// Project on to camera line
var p1 = cameraLine
.closestPointToPoint(corners[i], false, new THREE.Vector3());
// Compute distance from projection to center
var d = p1.distanceTo(center);
if (cameraLine.closestPointToPointParameter(p1, false) < 0) d = -d;
// Compute up line
var up =
new THREE.Line3(p1, new THREE.Vector3().copy(p1).add(cameraUp));
// Project on to up line
var p2 = up.closestPointToPoint(corners[i], false, new THREE.Vector3());
// Compute length
var l = p1.distanceTo(p2);
// Update min camera distance
dist = Math.max(dist, d + l / Math.tan(theta / 2));
// Compute left line
var left =
new THREE.Line3(p1, new THREE.Vector3().copy(p1).add(cameraLeft));
// Project on to left line
var p3 =
left.closestPointToPoint(corners[i], false, new THREE.Vector3());
// Compute length
l = p1.distanceTo(p3);
// Update min camera distance
dist = Math.max(dist, d + l / Math.tan(theta / 2) / this.camera.aspect);
}
this.camera.position.copy(offset.multiplyScalar(dist * 1.2).add(center));
}
},
mixins: [require('./axis-vars')]
}

42
src/js/settings-view.js Normal file
View File

@@ -0,0 +1,42 @@
/******************************************************************************\
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'
module.exports = {
template: '#settings-view-template',
props: ['config', 'template'],
events: {
'input-changed': function() {
this.$dispatch('config-changed');
return false;
}
}
}

127
src/js/sock.js Normal file
View File

@@ -0,0 +1,127 @@
/******************************************************************************\
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 Sock = function (url, retry, timeout) {
if (!(this instanceof Sock)) return new Sock(url, retry);
if (typeof retry == 'undefined') retry = 2000;
if (typeof timeout == 'undefined') timeout = 16000;
this.url = url;
this.retry = retry;
this.timeout = timeout;
this.divisions = 4;
this.count = 0;
this.connect();
}
Sock.prototype.onmessage = function () {}
Sock.prototype.onopen = function () {}
Sock.prototype.onclose = function () {}
Sock.prototype.connect = function () {
console.debug('connecting to', this.url);
this.close();
this._sock = new SockJS(this.url);
this._sock.onmessage = function (e) {
console.debug('msg:', e.data);
this.heartbeat('msg');
this.onmessage(e);
}.bind(this);
this._sock.onopen = function () {
console.debug('connected');
this.heartbeat('open');
this.onopen();
}.bind(this);
this._sock.onclose = function () {
console.debug('disconnected');
this._cancel_timeout();
this.onclose();
if (typeof this._sock != 'undefined')
setTimeout(this.connect.bind(this), this.retry);
}.bind(this);
}
Sock.prototype._timedout = function () {
// Divide timeout so slow browser doesn't trigger timeouts when the
// connection is good.
if (this.divisions <= ++this.count) {
console.debug('connection timedout');
this._timeout = undefined;
this._sock.close();
} else this._set_timeout();
}
Sock.prototype._cancel_timeout = function () {
clearTimeout(this._timeout);
this._timeout = undefined;
this.count = 0;
}
Sock.prototype._set_timeout = function () {
this._timeout = setTimeout(this._timedout.bind(this),
this.timeout / this.divisions);
}
Sock.prototype.heartbeat = function (msg) {
//console.debug('heartbeat ' + new Date().toLocaleTimeString() + ' ' + msg);
this._cancel_timeout();
this._set_timeout();
}
Sock.prototype.close = function () {
if (typeof this._sock != 'undefined') {
var sock = this._sock;
this._sock = undefined;
sock.close();
}
}
Sock.prototype.send = function (msg) {this._sock.send(msg)}
module.exports = Sock

89
src/js/templated-input.js Normal file
View File

@@ -0,0 +1,89 @@
/******************************************************************************\
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'
module.exports = {
replace: true,
template: '#templated-input-template',
props: ['name', 'model', 'template'],
data: function () {return {view: ''}},
computed: {
metric: function () {return this.$root.metric()},
_view: function () {
if (this.template.scale) {
if (this.metric) return 1 * this.model.toFixed(3);
return 1 * (this.model / this.template.scale).toFixed(4);
}
return this.model;
},
units: function () {
return (this.metric || !this.template.iunit) ?
this.template.unit : this.template.iunit;
},
title: function () {
var s = 'Default ' + this.template.default + ' ' +
(this.template.unit || '');
if (typeof this.template.help != 'undefined')
s = this.template.help + '\n' + s;
return s;
}
},
watch: {
_view: function () {this.view = this._view},
view: function () {
if (this.template.scale && !this.metric)
this.model = this.view * this.template.scale;
else this.model = this.view;
}
},
ready: function () {this.view = this._view},
methods: {
change: function () {this.$dispatch('input-changed')}
}
}

152
src/js/tool-view.js Normal file
View File

@@ -0,0 +1,152 @@
/******************************************************************************\
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 modbus = require('./modbus.js');
module.exports = {
template: '#tool-view-template',
props: ['config', 'template', 'state'],
data: function () {
return {
address: 0,
value: 0
}
},
components: {'modbus-reg': require('./modbus-reg.js')},
watch: {
'state.mr': function () {this.value = this.state.mr}
},
events: {
'input-changed': function() {
this.$dispatch('config-changed');
return false;
}
},
ready: function () {this.value = this.state.mr},
computed: {
regs_tmpl: function () {return this.template['modbus-spindle'].regs},
tool_type: function () {return this.config.tool['tool-type'].toUpperCase()},
is_modbus: function () {
return this.tool_type != 'DISABLED' && this.tool_type != 'PWM SPINDLE'
},
modbus_status: function () {return modbus.status_to_string(this.state.mx)}
},
methods: {
get_reg_type: function (reg) {
return this.regs_tmpl.template['reg-type'].values[this.state[reg + 'vt']]
},
get_reg_addr: function (reg) {return this.state[reg + 'va']},
get_reg_value: function (reg) {return this.state[reg + 'vv']},
get_reg_fails: function (reg) {
var fails = this.state[reg + 'vr']
return fails == 255 ? 'Max' : fails;
},
show_modbus_field: function (key) {
return key != 'regs' &&
(key != 'multi-write' || this.tool_type == 'CUSTOM MODBUS VFD');
},
read: function (e) {
e.preventDefault();
api.put('modbus/read', {address: this.address});
},
write: function (e) {
e.preventDefault();
api.put('modbus/write', {address: this.address, value: this.value});
},
customize: function (e) {
e.preventDefault();
this.config.tool['tool-type'] = 'Custom Modbus VFD';
var regs = this.config['modbus-spindle'].regs;
for (var i = 0; i < regs.length; i++) {
var reg = this.regs_tmpl.index[i];
regs[i]['reg-type'] = this.get_reg_type(reg);
regs[i]['reg-addr'] = this.get_reg_addr(reg);
regs[i]['reg-value'] = this.get_reg_value(reg);
}
this.$dispatch('config-changed');
},
clear: function (e) {
e.preventDefault();
this.config.tool['tool-type'] = 'Custom Modbus VFD';
var regs = this.config['modbus-spindle'].regs;
for (var i = 0; i < regs.length; i++) {
regs[i]['reg-type'] = 'disabled';
regs[i]['reg-addr'] = 0;
regs[i]['reg-value'] = 0;
}
this.$dispatch('config-changed');
},
reset_failures: function (e) {
e.preventDefault();
var regs = this.config['modbus-spindle'].regs;
for (var reg = 0; reg < regs.length; reg++)
this.$dispatch('send', '\$' + reg + 'vr=0');
}
}
}

58
src/js/unit-value.js Normal file
View File

@@ -0,0 +1,58 @@
/******************************************************************************\
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'
module.exports = {
replace: true,
template: '{{text}}<span class="unit">{{metric ? unit : iunit}}</span>',
props: ['value', 'precision', 'unit', 'iunit', 'scale'],
computed: {
metric: function () {return !this.$root.state.imperial},
text: function () {
var value = this.value;
if (typeof value == 'undefined') return '';
if (!this.metric) value /= this.scale;
return (1 * value.toFixed(this.precision)).toLocaleString();
}
},
ready: function () {
if (typeof this.precision == 'undefined') this.precision = 0;
if (typeof this.unit == 'undefined') this.unit = 'mm';
if (typeof this.iunit == 'undefined') this.iunit = 'in';
if (typeof this.scale == 'undefined') this.scale = 25.4;
}
}