ui: assorted polish - Vue async fix, OrbitControls passive listeners, path-viewer + motor-view + indicators
- main.js: disable Vue async batching so reactive writes from
hashchange listeners propagate synchronously (matches Vue 1's older
default; avoids dropped DRO updates).
- orbit.js: pass {passive:false} to wheel/touch listeners so
OrbitControls.preventDefault() actually suppresses page panning.
- path-viewer: opaque dark canvas (no flash from page background),
zero-size guard, ResizeObserver cleanup on destroy.
- motor-view: stop clobbering user edits with controller state.
- estop/indicators/tool-view/path-viewer pug: rename FA4 icons to FA6,
add viewBox to estop SVG, fix tool-view trailing newline.
This commit is contained in:
@@ -44,6 +44,16 @@ window.onload = function() {
|
|||||||
cookie_set("client-id", uuid(), 10000);
|
cookie_set("client-id", uuid(), 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vue 1's async queue can drop dependent watcher updates when
|
||||||
|
// data props are mutated outside the normal event flow (e.g. from
|
||||||
|
// a `hashchange` listener that fires before Vue's tick scheduler
|
||||||
|
// has caught up). Disable async batching so every reactive write
|
||||||
|
// synchronously re-evaluates dependents — this matches Vue 1's
|
||||||
|
// older default and is what the legacy UI implicitly relied on.
|
||||||
|
if (Vue.config) {
|
||||||
|
Vue.config.async = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Register global components
|
// Register global components
|
||||||
Vue.component("templated-input", require("./templated-input"));
|
Vue.component("templated-input", require("./templated-input"));
|
||||||
Vue.component("message", require("./message"));
|
Vue.component("message", require("./message"));
|
||||||
|
|||||||
@@ -87,100 +87,16 @@ module.exports = {
|
|||||||
return this.stallRPM * this.stepsPerRev * ustep / 60;
|
return this.stallRPM * this.stepsPerRev * ustep / 60;
|
||||||
},
|
},
|
||||||
|
|
||||||
current_axis: function() {
|
// NOTE: do not add `current_xxx` computed props that mirror
|
||||||
return this.state[this.index + 'an'];
|
// controller state vars (`<idx>vm`, `<idx>am`, …) and pair
|
||||||
},
|
// them with watchers that copy state -> motor config. The
|
||||||
|
// controller streams those vars continuously over the WS;
|
||||||
current_max_velocity: function() {
|
// any watcher that writes them back into
|
||||||
return this.state[this.index + 'vm'];
|
// `config.motors[index]` will clobber whatever the user is
|
||||||
},
|
// typing into the form between websocket ticks. The form
|
||||||
|
// edits config directly; Save (app.js) PUTs it to the
|
||||||
current_max_soft_limit: function() {
|
// server. The server-side rotary toggle is handled by
|
||||||
return this.state[this.index + 'tm'];
|
// refetching config after the PUT, not by watching state.
|
||||||
},
|
|
||||||
|
|
||||||
current_min_soft_limit: function() {
|
|
||||||
return this.state[this.index + 'tn'];
|
|
||||||
},
|
|
||||||
current_max_accel: function() {
|
|
||||||
return this.state[this.index + 'am'];
|
|
||||||
},
|
|
||||||
current_max_jerk: function() {
|
|
||||||
return this.state[this.index + 'jm'];
|
|
||||||
},
|
|
||||||
current_step_angle: function() {
|
|
||||||
return this.state[this.index + 'sa'];
|
|
||||||
},
|
|
||||||
current_travel_per_rev: function() {
|
|
||||||
return this.state[this.index + 'tr'];
|
|
||||||
},
|
|
||||||
current_microsteps: function() {
|
|
||||||
return this.state[this.index + 'mi'];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
attached: function() {
|
|
||||||
// Sync all state values with motor config when component is ready
|
|
||||||
// This ensures UI shows correct values when component is first loaded
|
|
||||||
console.log("Syncing state to motor config for motor index ",this.index);
|
|
||||||
this.syncStateToConfig();
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
current_axis(new_value) {
|
|
||||||
const motor_axes = ["X", "Y", "Z", "A", "B", "C"]
|
|
||||||
if(motor_axes[new_value] != this.motor['axis']){
|
|
||||||
this.motor['axis'] = motor_axes[new_value];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
current_max_velocity(new_value) {
|
|
||||||
if(new_value != this.motor['max-velocity']) {
|
|
||||||
this.motor['max-velocity'] = new_value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
current_max_soft_limit(new_value) {
|
|
||||||
if(new_value != this.motor['max-soft-limit']) {
|
|
||||||
this.motor['max-soft-limit'] = new_value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
current_min_soft_limit(new_value) {
|
|
||||||
if(new_value != this.motor['min-soft-limit']) {
|
|
||||||
this.motor['min-soft-limit'] = new_value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
current_max_accel(new_value) {
|
|
||||||
if(new_value != this.motor['max-accel']) {
|
|
||||||
this.motor['max-accel'] = new_value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
current_max_jerk(new_value) {
|
|
||||||
if(new_value != this.motor['max-jerk']) {
|
|
||||||
this.motor['max-jerk'] = new_value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
current_step_angle(new_value) {
|
|
||||||
if(new_value != this.motor['step-angle']) {
|
|
||||||
this.motor['step-angle'] = new_value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
current_travel_per_rev(new_value) {
|
|
||||||
if(new_value != this.motor['travel-per-rev']) {
|
|
||||||
this.motor['travel-per-rev'] = new_value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
current_microsteps(new_value) {
|
|
||||||
if(new_value != this.motor['microsteps']) {
|
|
||||||
this.motor['microsteps'] = new_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
@@ -210,45 +126,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return templ.hmodes.indexOf(this.motor["homing-mode"]) != -1;
|
return templ.hmodes.indexOf(this.motor["homing-mode"]) != -1;
|
||||||
},
|
|
||||||
|
|
||||||
syncStateToConfig: function() {
|
|
||||||
// Force sync all state values to motor config
|
|
||||||
// This ensures the UI reflects the current state even if changes happened while component was unmounted
|
|
||||||
|
|
||||||
if(this.state == undefined) {
|
|
||||||
console.log("State is undefined");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state[this.index + 'an'] != this.motor['axis']) {
|
|
||||||
const motor_axes = ["X", "Y", "Z", "A", "B", "C"];
|
|
||||||
this.$set('motor["axis"]', motor_axes[this.state[this.index + 'an']]);
|
|
||||||
}
|
|
||||||
if (this.state[this.index + 'vm'] != this.motor['max-velocity']) {
|
|
||||||
this.$set('motor["max-velocity"]', this.state[this.index + 'vm']);
|
|
||||||
}
|
|
||||||
if (this.state[this.index + 'tm'] != this.motor['max-soft-limit']) {
|
|
||||||
this.$set('motor["max-soft-limit"]', this.state[this.index + 'tm']);
|
|
||||||
}
|
|
||||||
if (this.state[this.index + 'tn'] != this.motor['min-soft-limit']) {
|
|
||||||
this.$set('motor["min-soft-limit"]', this.state[this.index + 'tn']);
|
|
||||||
}
|
|
||||||
if (this.state[this.index + 'am'] != this.motor['max-accel']) {
|
|
||||||
this.$set('motor["max-accel"]', this.state[this.index + 'am']);
|
|
||||||
}
|
|
||||||
if (this.state[this.index + 'jm'] != this.motor['max-jerk']) {
|
|
||||||
this.$set('motor["max-jerk"]', this.state[this.index + 'jm']);
|
|
||||||
}
|
|
||||||
if (this.state[this.index + 'sa'] != this.motor['step-angle']) {
|
|
||||||
this.$set('motor["step-angle"]', this.state[this.index + 'sa']);
|
|
||||||
}
|
|
||||||
if (this.state[this.index + 'tr'] != this.motor['travel-per-rev']) {
|
|
||||||
this.$set('motor["travel-per-rev"]', this.state[this.index + 'tr']);
|
|
||||||
}
|
|
||||||
if (this.state[this.index + 'mi'] != this.motor['microsteps']) {
|
|
||||||
this.$set('motor["microsteps"]', this.state[this.index + 'mi']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -683,12 +683,16 @@ const OrbitControls = function(object, domElement) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chrome treats touch/wheel listeners as passive by default,
|
||||||
|
// which prevents OrbitControls.preventDefault() from suppressing
|
||||||
|
// page panning while interacting with the 3D viewer. Pass
|
||||||
|
// {passive: false} on the events that need to call preventDefault.
|
||||||
scope.domElement.addEventListener("contextmenu", onContextMenu, false);
|
scope.domElement.addEventListener("contextmenu", onContextMenu, false);
|
||||||
scope.domElement.addEventListener("mousedown", onMouseDown, false);
|
scope.domElement.addEventListener("mousedown", onMouseDown, false);
|
||||||
scope.domElement.addEventListener("wheel", onMouseWheel, false);
|
scope.domElement.addEventListener("wheel", onMouseWheel, { passive: false });
|
||||||
scope.domElement.addEventListener("touchstart", onTouchStart, false);
|
scope.domElement.addEventListener("touchstart", onTouchStart, { passive: false });
|
||||||
scope.domElement.addEventListener("touchend", onTouchEnd, false);
|
scope.domElement.addEventListener("touchend", onTouchEnd, false);
|
||||||
scope.domElement.addEventListener("touchmove", onTouchMove, false);
|
scope.domElement.addEventListener("touchmove", onTouchMove, { passive: false });
|
||||||
window.addEventListener("keydown", onKeyDown, false);
|
window.addEventListener("keydown", onKeyDown, false);
|
||||||
|
|
||||||
this.update(); // force an update at start
|
this.update(); // force an update at start
|
||||||
|
|||||||
@@ -101,6 +101,13 @@ module.exports = {
|
|||||||
Vue.nextTick(this.update);
|
Vue.nextTick(this.update);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
beforeDestroy: function() {
|
||||||
|
if (this._sizeWatcher) {
|
||||||
|
this._sizeWatcher.disconnect();
|
||||||
|
this._sizeWatcher = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
update: async function() {
|
update: async function() {
|
||||||
if (!this.webglAvailable) {
|
if (!this.webglAvailable) {
|
||||||
@@ -201,6 +208,12 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dims = this.get_dims();
|
const dims = this.get_dims();
|
||||||
|
// Skip layouts where the target has no measurable size.
|
||||||
|
// The render loop guard below will not draw frames until
|
||||||
|
// a real size has been observed at least once.
|
||||||
|
if (!(dims.width > 0 && dims.height > 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.camera.aspect = dims.width / dims.height;
|
this.camera.aspect = dims.width / dims.height;
|
||||||
this.camera.updateProjectionMatrix();
|
this.camera.updateProjectionMatrix();
|
||||||
@@ -274,12 +287,23 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Renderer
|
// Renderer. Use an opaque canvas with a clear color
|
||||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
// that matches the page-side gradient so the moment
|
||||||
|
// the canvas is appended (and before the first 3D
|
||||||
|
// frame is drawn) the user does not see a flash from
|
||||||
|
// the page background through transparency.
|
||||||
|
this.renderer = new THREE.WebGLRenderer({
|
||||||
|
antialias: true,
|
||||||
|
alpha: false,
|
||||||
|
});
|
||||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
this.renderer.setClearColor(0, 0);
|
this.renderer.setClearColor(0x222222, 1);
|
||||||
|
// Same color on the DOM element itself so the very
|
||||||
|
// first paint (before the WebGL context has cleared)
|
||||||
|
// is dark too.
|
||||||
|
this.renderer.domElement.style.background = "#222222";
|
||||||
|
this.renderer.domElement.style.display = "block";
|
||||||
this.target.appendChild(this.renderer.domElement);
|
this.target.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("WebGL not supported: ", e);
|
console.log("WebGL not supported: ", e);
|
||||||
return;
|
return;
|
||||||
@@ -333,8 +357,46 @@ module.exports = {
|
|||||||
// Events
|
// Events
|
||||||
window.addEventListener("resize", this.update_view, false);
|
window.addEventListener("resize", this.update_view, false);
|
||||||
|
|
||||||
// Start it
|
// Start the render loop only after the target has a real,
|
||||||
|
// stable size. Without this, the first frame paints into
|
||||||
|
// a 0×0 / collapsed-flex canvas and a second frame paints
|
||||||
|
// again at the right size — visible as a flash on the
|
||||||
|
// very first mount of the Program tab.
|
||||||
|
const startRendering = () => {
|
||||||
|
if (this._rendering) return;
|
||||||
|
this._rendering = true;
|
||||||
|
this.update_view();
|
||||||
this.render();
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const dims = this.get_dims();
|
||||||
|
if (dims.width > 0 && dims.height > 0) {
|
||||||
|
startRendering();
|
||||||
|
} else if (typeof ResizeObserver !== "undefined") {
|
||||||
|
this._sizeWatcher = new ResizeObserver(entries => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const r = entry.contentRect;
|
||||||
|
if (r.width > 0 && r.height > 0) {
|
||||||
|
this._sizeWatcher.disconnect();
|
||||||
|
this._sizeWatcher = null;
|
||||||
|
startRendering();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._sizeWatcher.observe(this.target);
|
||||||
|
} else {
|
||||||
|
// Old browser fallback: poll for a non-zero size.
|
||||||
|
const tick = () => {
|
||||||
|
const d = this.get_dims();
|
||||||
|
if (d.width > 0 && d.height > 0) {
|
||||||
|
startRendering();
|
||||||
|
} else {
|
||||||
|
requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
create_surface_material: function() {
|
create_surface_material: function() {
|
||||||
@@ -646,6 +708,14 @@ module.exports = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't paint frames while the target has no size; this
|
||||||
|
// prevents an initial single-frame clear from painting
|
||||||
|
// before the layout has settled (visible as a dark flash).
|
||||||
|
const dims = this.get_dims();
|
||||||
|
if (!(dims.width > 0 && dims.height > 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.controls.update() || this.dirty) {
|
if (this.controls.update() || this.dirty) {
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ script#estop-template(type="text/x-template")
|
|||||||
svg(version="1.1", xmlns:svg="http://www.w3.org/2000/svg",
|
svg(version="1.1", xmlns:svg="http://www.w3.org/2000/svg",
|
||||||
xmlns="http://www.w3.org/2000/svg",
|
xmlns="http://www.w3.org/2000/svg",
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink",
|
xmlns:xlink="http://www.w3.org/1999/xlink",
|
||||||
|
viewBox="0 0 130 130",
|
||||||
|
preserveAspectRatio="xMidYMid meet",
|
||||||
width="130", height="130")
|
width="130", height="130")
|
||||||
defs
|
defs
|
||||||
path#text-path-1(d="m 73.735,673.129 c 0,55.107 44.673,99.780 99.780,99.780 55.107,0 99.780,-44.673 99.780,-99.780 0,-55.107 -44.673,-99.780 -99.780,-99.780 -55.107,0 -99.780,44.673 -99.780,99.780 z")
|
path#text-path-1(d="m 73.735,673.129 c 0,55.107 44.673,99.780 99.780,99.780 55.107,0 99.780,-44.673 99.780,-99.780 0,-55.107 -44.673,-99.780 -99.780,-99.780 -55.107,0 -99.780,44.673 -99.780,99.780 z")
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ script#indicators-template(type="text/x-template")
|
|||||||
|
|
||||||
tr
|
tr
|
||||||
td
|
td
|
||||||
.fa.fa-plus-circle.io
|
.fa.fa-circle-plus.io
|
||||||
th Hi/+3.3v
|
th Hi/+3.3v
|
||||||
th.separator
|
th.separator
|
||||||
td
|
td
|
||||||
.fa.fa-minus-circle.io
|
.fa.fa-circle-minus.io
|
||||||
th Lo/Gnd
|
th Lo/Gnd
|
||||||
th.separator
|
th.separator
|
||||||
td
|
td
|
||||||
@@ -22,7 +22,7 @@ script#indicators-template(type="text/x-template")
|
|||||||
th Inactive
|
th Inactive
|
||||||
th.separator
|
th.separator
|
||||||
td
|
td
|
||||||
.fa.fa-circle-o.io
|
.far.fa-circle.io
|
||||||
th Tristated/Disabled
|
th Tristated/Disabled
|
||||||
|
|
||||||
table.inputs
|
table.inputs
|
||||||
@@ -169,14 +169,14 @@ script#indicators-template(type="text/x-template")
|
|||||||
|
|
||||||
tr
|
tr
|
||||||
th Motor
|
th Motor
|
||||||
th(title="Overtemperature fault"): .fa.fa-thermometer-full
|
th(title="Overtemperature fault"): .fa.fa-temperature-full
|
||||||
th(title="Overcurrent motor channel A") A #[.fa.fa-bolt]
|
th(title="Overcurrent motor channel A") A #[.fa.fa-bolt]
|
||||||
th(title="Predriver fault motor channel A")
|
th(title="Predriver fault motor channel A")
|
||||||
| A #[.fa.fa-exclamation-triangle]
|
| A #[.fa.fa-triangle-exclamation]
|
||||||
th(title="Overcurrent motor channel B") B #[.fa.fa-bolt]
|
th(title="Overcurrent motor channel B") B #[.fa.fa-bolt]
|
||||||
th(title="Predriver fault motor channel B")
|
th(title="Predriver fault motor channel B")
|
||||||
| B #[.fa.fa-exclamation-triangle]
|
| B #[.fa.fa-triangle-exclamation]
|
||||||
th(title="Driver communication failure"): .fa.fa-handshake-o
|
th(title="Driver communication failure"): .fa.fa-handshake
|
||||||
th(title="Reset all motor flags")
|
th(title="Reset all motor flags")
|
||||||
.fa.fa-eraser(@click="motor_reset()")
|
.fa.fa-eraser(@click="motor_reset()")
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ script#path-viewer-template(type="text/x-template")
|
|||||||
.path-viewer-toolbar
|
.path-viewer-toolbar
|
||||||
.tool-button(title="Toggle path view size.",
|
.tool-button(title="Toggle path view size.",
|
||||||
@click="small = !small", :class="{active: !small}")
|
@click="small = !small", :class="{active: !small}")
|
||||||
.fa.fa-arrows-alt
|
.fa.fa-up-down-left-right
|
||||||
|
|
||||||
.tool-button(@click="showTool = !showTool", :class="{active: showTool}",
|
.tool-button(@click="showTool = !showTool", :class="{active: showTool}",
|
||||||
title="Show/hide tool.")
|
title="Show/hide tool.")
|
||||||
|
|||||||
Reference in New Issue
Block a user