Component demo · Instrument readouts and controls
Jog Panel
index.htm as it is written with the smart-industrial package
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Jog Panel Overview Demo</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.default.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-elements.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-dark.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.JogPanel is the manual motion panel of a machine pendant, with axis positions from the drive, continuous or incremental jogging, a speed override and a stop button.</h2>
<p>
In <em>Cont.</em> mode an axis moves while its jog button is held and stops when the button is released, when the pointer leaves the button, when focus moves away or when an interlock is set. With a step selected, each press requests one increment and there is no auto-repeat. Only one axis jogs at a time, a soft limit prevents jogging in the direction that would cross it, and the position readout shows the position reported by the drive.
</p>
</section>
<section>
<h2>An XYZ stage</h2>
<p>
The simulated drive moves at 20 mm/s multiplied by the speed override, has soft limits of ±150 mm in X and Y and 0–80 mm in Z, and takes a moment to home. Clear the <em>Manual mode</em> checkbox to interlock the panel through the machine mode.
</p>
<div class="stage">
<smart-jog-panel id="stage" label="Stage 1" unit="mm" speed="25"></smart-jog-panel>
<div class="plot">
<canvas id="plot" width="300" height="300" aria-label="Plan view of the stage"></canvas>
<div class="z-bar"><div id="zFill" class="z-fill"></div><span class="z-label">Z</span></div>
<ol id="log" class="log"></ol>
<div class="demo-actions">
<label><input type="checkbox" id="manual" checked /> Manual mode</label>
<label><input type="checkbox" id="touch" /> Touch density</label>
</div>
</div>
</div>
<p class="hint">
Keyboard: Tab to a jog button and hold Space to jog; Escape stops. The step and speed rows are radio groups where the arrow keys move and select. Everything shown on the panel comes from the drive.
</p>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.jogpanel.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* A simulated three-axis drive. The panel raises jogStart, jogStop, jogStep, homeRequest
and stop; the drive here moves at its own pace and reports positions back through
update(), which is all the panel ever shows. Replace `drive` with the motion controller. */
window.onload = function () {
const stage = document.getElementById('stage'),
plot = document.getElementById('plot'),
zFill = document.getElementById('zFill'),
log = document.getElementById('log'),
manual = document.getElementById('manual'),
touch = document.getElementById('touch');
const RATE = 20; /* mm/s at 100% */
const drive = {
axes: {
X: { position: 12.5, min: -150, max: 150, homed: true, velocity: 0, target: null, state: '' },
Y: { position: -40, min: -150, max: 150, homed: true, velocity: 0, target: null, state: '' },
Z: { position: 25, min: 0, max: 80, homed: false, velocity: 0, target: null, state: '' }
},
homing: null
};
function note(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
log.insertBefore(item, log.firstChild);
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
stage.axes = Object.keys(drive.axes).map(function (id) {
const axis = drive.axes[id];
return { id: id, position: axis.position, min: axis.min, max: axis.max, homed: axis.homed };
});
/* ── The panel's requests ───────────────────────────────────────────── */
stage.addEventListener('jogStart', function (event) {
const axis = drive.axes[event.detail.axis];
axis.velocity = event.detail.direction * RATE * event.detail.speed / 100;
axis.target = null;
axis.state = 'moving';
note('jogStart ' + event.detail.axis + (event.detail.direction > 0 ? '+' : '-') + ' at ' + event.detail.speed + '%');
});
stage.addEventListener('jogStop', function (event) {
const axis = drive.axes[event.detail.axis];
axis.velocity = 0;
axis.state = '';
note('jogStop ' + event.detail.axis + ' after ' + event.detail.duration + ' ms');
});
stage.addEventListener('jogStep', function (event) {
const axis = drive.axes[event.detail.axis],
target = Math.min(axis.max, Math.max(axis.min, axis.position + event.detail.direction * event.detail.step));
axis.target = target;
axis.velocity = Math.sign(target - axis.position) * RATE * event.detail.speed / 100;
axis.state = 'moving';
note('jogStep ' + event.detail.axis + (event.detail.direction > 0 ? '+' : '-') + event.detail.step + ' mm');
});
stage.addEventListener('homeRequest', function (event) {
const ids = event.detail.axis ? [event.detail.axis] : Object.keys(drive.axes);
ids.forEach(function (id) {
const axis = drive.axes[id];
axis.target = 0;
axis.velocity = Math.sign(-axis.position) * RATE * 0.5;
axis.state = 'homing';
axis.homed = false;
});
note('homeRequest ' + (event.detail.axis || 'all'));
});
stage.addEventListener('stop', function () {
Object.keys(drive.axes).forEach(function (id) {
drive.axes[id].velocity = 0;
drive.axes[id].target = null;
drive.axes[id].state = '';
});
note('stop');
});
stage.addEventListener('blocked', function (event) {
note('blocked: ' + event.detail.reason);
});
stage.addEventListener('stepChange', function (event) {
note('step ' + (event.detail.step || 'continuous'));
});
stage.addEventListener('speedChange', function (event) {
note('speed ' + event.detail.speed + '%');
});
/* ── The drive, reporting ───────────────────────────────────────────── */
let last = performance.now();
function tick(now) {
const dt = Math.min(0.1, (now - last) / 1000);
last = now;
Object.keys(drive.axes).forEach(function (id) {
const axis = drive.axes[id];
/* One report after the axis stops, so the panel sees the state clear. */
if (axis.velocity === 0) {
if (axis.reported) {
axis.reported = false;
stage.update(id, { position: axis.position, state: axis.state, homed: axis.homed });
}
return;
}
axis.reported = true;
let next = axis.position + axis.velocity * dt,
arrived = false;
if (axis.target !== null && (axis.velocity > 0 ? next >= axis.target : next <= axis.target)) {
next = axis.target;
arrived = true;
}
if (next >= axis.max) {
next = axis.max;
arrived = true;
}
else if (next <= axis.min) {
next = axis.min;
arrived = true;
}
axis.position = next;
if (arrived) {
const wasHoming = axis.state === 'homing';
axis.velocity = 0;
axis.target = null;
axis.state = '';
if (wasHoming) {
axis.homed = true;
note(id + ' homed');
}
}
stage.update(id, { position: axis.position, state: axis.state, homed: axis.homed });
});
draw();
requestAnimationFrame(tick);
}
/* ── The plan view ──────────────────────────────────────────────────── */
function draw() {
const ctx = plot.getContext('2d'),
w = plot.width,
h = plot.height,
scale = (w - 40) / 300,
x = w / 2 + drive.axes.X.position * scale,
y = h / 2 - drive.axes.Y.position * scale,
text = getComputedStyle(plot).color;
ctx.clearRect(0, 0, w, h);
ctx.save();
ctx.strokeStyle = text;
ctx.globalAlpha = 0.25;
ctx.lineWidth = 1;
ctx.strokeRect(20, 20, w - 40, h - 40);
ctx.beginPath();
ctx.moveTo(w / 2, 20);
ctx.lineTo(w / 2, h - 20);
ctx.moveTo(20, h / 2);
ctx.lineTo(w - 20, h / 2);
ctx.stroke();
ctx.globalAlpha = 1;
ctx.fillStyle = text;
ctx.font = '11px sans-serif';
ctx.fillText('-150', 22, h - 24);
ctx.fillText('+150', w - 48, h - 24);
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.moveTo(x - 12, y);
ctx.lineTo(x + 12, y);
ctx.moveTo(x, y - 12);
ctx.lineTo(x, y + 12);
ctx.stroke();
ctx.restore();
zFill.style.height = (drive.axes.Z.position / 80 * 100) + '%';
}
/* ── Chrome ─────────────────────────────────────────────────────────── */
manual.addEventListener('change', function () {
stage.interlocked = !manual.checked;
stage.interlockReason = manual.checked ? '' : 'machine not in Manual';
});
touch.addEventListener('change', function () {
stage.density = touch.checked ? 'touch' : 'normal';
});
draw();
requestAnimationFrame(tick);
};
styles.css
.stage {
display: grid;
grid-template-columns: auto minmax(260px, 1fr);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.stage {
grid-template-columns: 1fr;
}
}
.plot {
display: grid;
grid-template-columns: auto auto;
justify-content: start;
gap: 12px;
align-items: start;
}
#plot {
width: 300px;
height: 300px;
max-width: 100%;
color: var(--smart-text-color);
border: 1px solid var(--smart-border);
border-radius: 4px;
}
.z-bar {
position: relative;
width: 28px;
height: 300px;
border: 1px solid var(--smart-border);
border-radius: 4px;
overflow: hidden;
}
.z-fill {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 0;
background: var(--smart-primary);
opacity: 0.6;
transition: height 80ms linear;
}
.z-label {
position: absolute;
top: 4px;
left: 0;
right: 0;
text-align: center;
font-size: 11px;
font-weight: 600;
}
.log {
grid-column: 1 / -1;
margin: 0;
padding: 0;
list-style: none;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
}
.log li {
padding: 2px 0;
}
.demo-actions {
grid-column: 1 / -1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in Angular, generated from the HTML demo. The frameworks guide has the project setup.
angular/app.component.html
<div class="angular-root" theme="industrial">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.JogPanel is the manual motion panel of a machine pendant, with axis positions from the drive, continuous or incremental jogging, a speed override and a stop button.</h2>
<p>
In <em>Cont.</em> mode an axis moves while its jog button is held and stops when the button is released, when the pointer leaves the button, when focus moves away or when an interlock is set. With a step selected, each press requests one increment and there is no auto-repeat. Only one axis jogs at a time, a soft limit prevents jogging in the direction that would cross it, and the position readout shows the position reported by the drive.
</p>
</section>
<section>
<h2>An XYZ stage</h2>
<p>
The simulated drive moves at 20 mm/s multiplied by the speed override, has soft limits of ±150 mm in X and Y and 0–80 mm in Z, and takes a moment to home. Clear the <em>Manual mode</em> checkbox to interlock the panel through the machine mode.
</p>
<div class="stage">
<smart-jog-panel id="stage" label="Stage 1" unit="mm" [speed]="25"></smart-jog-panel>
<div class="plot">
<canvas id="plot" width="300" height="300" aria-label="Plan view of the stage"></canvas>
<div class="z-bar"><div id="zFill" class="z-fill"></div><span class="z-label">Z</span></div>
<ol id="log" class="log"></ol>
<div class="demo-actions">
<label><input type="checkbox" id="manual" checked /> Manual mode</label>
<label><input type="checkbox" id="touch" /> Touch density</label>
</div>
</div>
</div>
<p class="hint">
Keyboard: Tab to a jog button and hold Space to jog; Escape stops. The step and speed rows are radio groups where the arrow keys move and select. Everything shown on the panel comes from the drive.
</p>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { JogPanelModule } from 'smart-industrial/angular/jogpanel';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - JogPanel in Angular.
*
* The template is the HTML demo's markup: the elements are the wrappers, so what is
* written on them is checked against their inputs. demo.ts drives them the way the
* plant's data would, once every element in the view has rendered.
*/
@Component({
selector: 'app-root',
standalone: true,
imports: [JogPanelModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent implements AfterViewInit, OnDestroy {
private unmounted = false;
ngAfterViewInit(): void {
const root = document.querySelector('.angular-root') as HTMLElement,
pending: Promise<void>[] = [];
root.querySelectorAll('*').forEach((element: any) => {
if (typeof element.whenRendered === 'function' && !element.isRendered) {
pending.push(element.whenRendered());
}
});
Promise.all(pending).then(() => {
if (!this.unmounted) {
runDemo();
}
});
}
ngOnDestroy(): void {
this.unmounted = true;
}
}
angular/demo.ts
// @ts-nocheck
/* The HTML demo's script, as it was written: page code that feeds the elements, not a typed
API. The types are on the wrappers in app.component.ts. */
/* A simulated three-axis drive. The panel raises jogStart, jogStop, jogStep, homeRequest
and stop; the drive here moves at its own pace and reports positions back through
update(), which is all the panel ever shows. Replace `drive` with the motion controller. */
export function runDemo(): void {
const stage = document.getElementById('stage'),
plot = document.getElementById('plot'),
zFill = document.getElementById('zFill'),
log = document.getElementById('log'),
manual = document.getElementById('manual'),
touch = document.getElementById('touch');
const RATE = 20; /* mm/s at 100% */
const drive = {
axes: {
X: { position: 12.5, min: -150, max: 150, homed: true, velocity: 0, target: null, state: '' },
Y: { position: -40, min: -150, max: 150, homed: true, velocity: 0, target: null, state: '' },
Z: { position: 25, min: 0, max: 80, homed: false, velocity: 0, target: null, state: '' }
},
homing: null
};
function note(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
log.insertBefore(item, log.firstChild);
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
stage.axes = Object.keys(drive.axes).map(function (id) {
const axis = drive.axes[id];
return { id: id, position: axis.position, min: axis.min, max: axis.max, homed: axis.homed };
});
/* ── The panel's requests ───────────────────────────────────────────── */
stage.addEventListener('jogStart', function (event) {
const axis = drive.axes[event.detail.axis];
axis.velocity = event.detail.direction * RATE * event.detail.speed / 100;
axis.target = null;
axis.state = 'moving';
note('jogStart ' + event.detail.axis + (event.detail.direction > 0 ? '+' : '-') + ' at ' + event.detail.speed + '%');
});
stage.addEventListener('jogStop', function (event) {
const axis = drive.axes[event.detail.axis];
axis.velocity = 0;
axis.state = '';
note('jogStop ' + event.detail.axis + ' after ' + event.detail.duration + ' ms');
});
stage.addEventListener('jogStep', function (event) {
const axis = drive.axes[event.detail.axis],
target = Math.min(axis.max, Math.max(axis.min, axis.position + event.detail.direction * event.detail.step));
axis.target = target;
axis.velocity = Math.sign(target - axis.position) * RATE * event.detail.speed / 100;
axis.state = 'moving';
note('jogStep ' + event.detail.axis + (event.detail.direction > 0 ? '+' : '-') + event.detail.step + ' mm');
});
stage.addEventListener('homeRequest', function (event) {
const ids = event.detail.axis ? [event.detail.axis] : Object.keys(drive.axes);
ids.forEach(function (id) {
const axis = drive.axes[id];
axis.target = 0;
axis.velocity = Math.sign(-axis.position) * RATE * 0.5;
axis.state = 'homing';
axis.homed = false;
});
note('homeRequest ' + (event.detail.axis || 'all'));
});
stage.addEventListener('stop', function () {
Object.keys(drive.axes).forEach(function (id) {
drive.axes[id].velocity = 0;
drive.axes[id].target = null;
drive.axes[id].state = '';
});
note('stop');
});
stage.addEventListener('blocked', function (event) {
note('blocked: ' + event.detail.reason);
});
stage.addEventListener('stepChange', function (event) {
note('step ' + (event.detail.step || 'continuous'));
});
stage.addEventListener('speedChange', function (event) {
note('speed ' + event.detail.speed + '%');
});
/* ── The drive, reporting ───────────────────────────────────────────── */
let last = performance.now();
function tick(now) {
const dt = Math.min(0.1, (now - last) / 1000);
last = now;
Object.keys(drive.axes).forEach(function (id) {
const axis = drive.axes[id];
/* One report after the axis stops, so the panel sees the state clear. */
if (axis.velocity === 0) {
if (axis.reported) {
axis.reported = false;
stage.update(id, { position: axis.position, state: axis.state, homed: axis.homed });
}
return;
}
axis.reported = true;
let next = axis.position + axis.velocity * dt,
arrived = false;
if (axis.target !== null && (axis.velocity > 0 ? next >= axis.target : next <= axis.target)) {
next = axis.target;
arrived = true;
}
if (next >= axis.max) {
next = axis.max;
arrived = true;
}
else if (next <= axis.min) {
next = axis.min;
arrived = true;
}
axis.position = next;
if (arrived) {
const wasHoming = axis.state === 'homing';
axis.velocity = 0;
axis.target = null;
axis.state = '';
if (wasHoming) {
axis.homed = true;
note(id + ' homed');
}
}
stage.update(id, { position: axis.position, state: axis.state, homed: axis.homed });
});
draw();
requestAnimationFrame(tick);
}
/* ── The plan view ──────────────────────────────────────────────────── */
function draw() {
const ctx = plot.getContext('2d'),
w = plot.width,
h = plot.height,
scale = (w - 40) / 300,
x = w / 2 + drive.axes.X.position * scale,
y = h / 2 - drive.axes.Y.position * scale,
text = getComputedStyle(plot).color;
ctx.clearRect(0, 0, w, h);
ctx.save();
ctx.strokeStyle = text;
ctx.globalAlpha = 0.25;
ctx.lineWidth = 1;
ctx.strokeRect(20, 20, w - 40, h - 40);
ctx.beginPath();
ctx.moveTo(w / 2, 20);
ctx.lineTo(w / 2, h - 20);
ctx.moveTo(20, h / 2);
ctx.lineTo(w - 20, h / 2);
ctx.stroke();
ctx.globalAlpha = 1;
ctx.fillStyle = text;
ctx.font = '11px sans-serif';
ctx.fillText('-150', 22, h - 24);
ctx.fillText('+150', w - 48, h - 24);
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.moveTo(x - 12, y);
ctx.lineTo(x + 12, y);
ctx.moveTo(x, y - 12);
ctx.lineTo(x, y + 12);
ctx.stroke();
ctx.restore();
zFill.style.height = (drive.axes.Z.position / 80 * 100) + '%';
}
/* ── Chrome ─────────────────────────────────────────────────────────── */
manual.addEventListener('change', function () {
stage.interlocked = !manual.checked;
stage.interlockReason = manual.checked ? '' : 'machine not in Manual';
});
touch.addEventListener('change', function () {
stage.density = touch.checked ? 'touch' : 'normal';
});
draw();
requestAnimationFrame(tick);
}
angular/app.component.css
.stage {
display: grid;
grid-template-columns: auto minmax(260px, 1fr);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.stage {
grid-template-columns: 1fr;
}
}
.plot {
display: grid;
grid-template-columns: auto auto;
justify-content: start;
gap: 12px;
align-items: start;
}
#plot {
width: 300px;
height: 300px;
max-width: 100%;
color: var(--smart-text-color);
border: 1px solid var(--smart-border);
border-radius: 4px;
}
.z-bar {
position: relative;
width: 28px;
height: 300px;
border: 1px solid var(--smart-border);
border-radius: 4px;
overflow: hidden;
}
.z-fill {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 0;
background: var(--smart-primary);
opacity: 0.6;
transition: height 80ms linear;
}
.z-label {
position: absolute;
top: 4px;
left: 0;
right: 0;
text-align: center;
font-size: 11px;
font-weight: 600;
}
.log {
grid-column: 1 / -1;
margin: 0;
padding: 0;
list-style: none;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
}
.log li {
padding: 2px 0;
}
.demo-actions {
grid-column: 1 / -1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in React, generated from the HTML demo. The frameworks guide has the project setup.
react/App.jsx
import 'smart-industrial/source/styles/smart.default.css';
import 'smart-industrial/source/styles/smart.industrial-elements.css';
import 'smart-industrial/source/styles/smart.industrial.css';
import 'smart-industrial/source/styles/smart.industrial-dark.css';
import './App.css';
import React from 'react';
import { JogPanel } from 'smart-industrial/react/jogpanel';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* A simulated three-axis drive. The panel raises jogStart, jogStop, jogStep, homeRequest
and stop; the drive here moves at its own pace and reports positions back through
update(), which is all the panel ever shows. Replace `drive` with the motion controller. */
const App = () => {
const root = React.useRef(null);
React.useEffect(() => {
/* The elements render after React has mounted them, and the demo's logic - written
for a page's load event - reads their parts, so it waits for every element in the
tree to have rendered first. */
const pending = Array.from(root.current.querySelectorAll('*'))
.filter((element) => typeof element.whenRendered === 'function' && !element.isRendered)
.map((element) => element.whenRendered());
let unmounted = false;
Promise.all(pending).then(() => {
if (unmounted) {
return;
}
const stage = document.getElementById('stage'),
plot = document.getElementById('plot'),
zFill = document.getElementById('zFill'),
log = document.getElementById('log'),
manual = document.getElementById('manual'),
touch = document.getElementById('touch');
const RATE = 20; /* mm/s at 100% */
const drive = {
axes: {
X: { position: 12.5, min: -150, max: 150, homed: true, velocity: 0, target: null, state: '' },
Y: { position: -40, min: -150, max: 150, homed: true, velocity: 0, target: null, state: '' },
Z: { position: 25, min: 0, max: 80, homed: false, velocity: 0, target: null, state: '' }
},
homing: null
};
function note(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
log.insertBefore(item, log.firstChild);
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
stage.axes = Object.keys(drive.axes).map(function (id) {
const axis = drive.axes[id];
return { id: id, position: axis.position, min: axis.min, max: axis.max, homed: axis.homed };
});
/* ── The panel's requests ───────────────────────────────────────────── */
stage.addEventListener('jogStart', function (event) {
const axis = drive.axes[event.detail.axis];
axis.velocity = event.detail.direction * RATE * event.detail.speed / 100;
axis.target = null;
axis.state = 'moving';
note('jogStart ' + event.detail.axis + (event.detail.direction > 0 ? '+' : '-') + ' at ' + event.detail.speed + '%');
});
stage.addEventListener('jogStop', function (event) {
const axis = drive.axes[event.detail.axis];
axis.velocity = 0;
axis.state = '';
note('jogStop ' + event.detail.axis + ' after ' + event.detail.duration + ' ms');
});
stage.addEventListener('jogStep', function (event) {
const axis = drive.axes[event.detail.axis],
target = Math.min(axis.max, Math.max(axis.min, axis.position + event.detail.direction * event.detail.step));
axis.target = target;
axis.velocity = Math.sign(target - axis.position) * RATE * event.detail.speed / 100;
axis.state = 'moving';
note('jogStep ' + event.detail.axis + (event.detail.direction > 0 ? '+' : '-') + event.detail.step + ' mm');
});
stage.addEventListener('homeRequest', function (event) {
const ids = event.detail.axis ? [event.detail.axis] : Object.keys(drive.axes);
ids.forEach(function (id) {
const axis = drive.axes[id];
axis.target = 0;
axis.velocity = Math.sign(-axis.position) * RATE * 0.5;
axis.state = 'homing';
axis.homed = false;
});
note('homeRequest ' + (event.detail.axis || 'all'));
});
stage.addEventListener('stop', function () {
Object.keys(drive.axes).forEach(function (id) {
drive.axes[id].velocity = 0;
drive.axes[id].target = null;
drive.axes[id].state = '';
});
note('stop');
});
stage.addEventListener('blocked', function (event) {
note('blocked: ' + event.detail.reason);
});
stage.addEventListener('stepChange', function (event) {
note('step ' + (event.detail.step || 'continuous'));
});
stage.addEventListener('speedChange', function (event) {
note('speed ' + event.detail.speed + '%');
});
/* ── The drive, reporting ───────────────────────────────────────────── */
let last = performance.now();
function tick(now) {
const dt = Math.min(0.1, (now - last) / 1000);
last = now;
Object.keys(drive.axes).forEach(function (id) {
const axis = drive.axes[id];
/* One report after the axis stops, so the panel sees the state clear. */
if (axis.velocity === 0) {
if (axis.reported) {
axis.reported = false;
stage.update(id, { position: axis.position, state: axis.state, homed: axis.homed });
}
return;
}
axis.reported = true;
let next = axis.position + axis.velocity * dt,
arrived = false;
if (axis.target !== null && (axis.velocity > 0 ? next >= axis.target : next <= axis.target)) {
next = axis.target;
arrived = true;
}
if (next >= axis.max) {
next = axis.max;
arrived = true;
}
else if (next <= axis.min) {
next = axis.min;
arrived = true;
}
axis.position = next;
if (arrived) {
const wasHoming = axis.state === 'homing';
axis.velocity = 0;
axis.target = null;
axis.state = '';
if (wasHoming) {
axis.homed = true;
note(id + ' homed');
}
}
stage.update(id, { position: axis.position, state: axis.state, homed: axis.homed });
});
draw();
requestAnimationFrame(tick);
}
/* ── The plan view ──────────────────────────────────────────────────── */
function draw() {
const ctx = plot.getContext('2d'),
w = plot.width,
h = plot.height,
scale = (w - 40) / 300,
x = w / 2 + drive.axes.X.position * scale,
y = h / 2 - drive.axes.Y.position * scale,
text = getComputedStyle(plot).color;
ctx.clearRect(0, 0, w, h);
ctx.save();
ctx.strokeStyle = text;
ctx.globalAlpha = 0.25;
ctx.lineWidth = 1;
ctx.strokeRect(20, 20, w - 40, h - 40);
ctx.beginPath();
ctx.moveTo(w / 2, 20);
ctx.lineTo(w / 2, h - 20);
ctx.moveTo(20, h / 2);
ctx.lineTo(w - 20, h / 2);
ctx.stroke();
ctx.globalAlpha = 1;
ctx.fillStyle = text;
ctx.font = '11px sans-serif';
ctx.fillText('-150', 22, h - 24);
ctx.fillText('+150', w - 48, h - 24);
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.moveTo(x - 12, y);
ctx.lineTo(x + 12, y);
ctx.moveTo(x, y - 12);
ctx.lineTo(x, y + 12);
ctx.stroke();
ctx.restore();
zFill.style.height = (drive.axes.Z.position / 80 * 100) + '%';
}
/* ── Chrome ─────────────────────────────────────────────────────────── */
manual.addEventListener('change', function () {
stage.interlocked = !manual.checked;
stage.interlockReason = manual.checked ? '' : 'machine not in Manual';
});
touch.addEventListener('change', function () {
stage.density = touch.checked ? 'touch' : 'normal';
});
draw();
requestAnimationFrame(tick);
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.JogPanel is the manual motion panel of a machine pendant, with axis positions from the drive, continuous or incremental jogging, a speed override and a stop button.</h2>
<p>In <em>Cont.</em> mode an axis moves while its jog button is held and stops when the button is released, when the pointer leaves the button, when focus moves away or when an interlock is set. With a step selected, each press requests one increment and there is no auto-repeat. Only one axis jogs at a time, a soft limit prevents jogging in the direction that would cross it, and the position readout shows the position reported by the drive.</p>
</section>
<section>
<h2>An XYZ stage</h2>
<p>The simulated drive moves at 20 mm/s multiplied by the speed override, has soft limits of ±150 mm in X and Y and 0–80 mm in Z, and takes a moment to home. Clear the <em>Manual mode</em> checkbox to interlock the panel through the machine mode.</p>
<div className="stage">
<JogPanel id="stage" label="Stage 1" unit="mm" speed={25}></JogPanel>
<div className="plot">
<canvas id="plot" width="300" height="300" aria-label="Plan view of the stage"></canvas>
<div className="z-bar">
<div id="zFill" className="z-fill"></div>
<span className="z-label">Z</span>
</div>
<ol id="log" className="log"></ol>
<div className="demo-actions">
<label><input type="checkbox" id="manual" defaultChecked /> Manual mode</label>
<label><input type="checkbox" id="touch" /> Touch density</label>
</div>
</div>
</div>
<p className="hint">Keyboard: Tab to a jog button and hold Space to jog; Escape stops. The step and speed rows are radio groups where the arrow keys move and select. Everything shown on the panel comes from the drive.</p>
</section>
</div>
</div>
);
};
export default App;
react/App.css
.stage {
display: grid;
grid-template-columns: auto minmax(260px, 1fr);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.stage {
grid-template-columns: 1fr;
}
}
.plot {
display: grid;
grid-template-columns: auto auto;
justify-content: start;
gap: 12px;
align-items: start;
}
#plot {
width: 300px;
height: 300px;
max-width: 100%;
color: var(--smart-text-color);
border: 1px solid var(--smart-border);
border-radius: 4px;
}
.z-bar {
position: relative;
width: 28px;
height: 300px;
border: 1px solid var(--smart-border);
border-radius: 4px;
overflow: hidden;
}
.z-fill {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 0;
background: var(--smart-primary);
opacity: 0.6;
transition: height 80ms linear;
}
.z-label {
position: absolute;
top: 4px;
left: 0;
right: 0;
text-align: center;
font-size: 11px;
font-weight: 600;
}
.log {
grid-column: 1 / -1;
margin: 0;
padding: 0;
list-style: none;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
}
.log li {
padding: 2px 0;
}
.demo-actions {
grid-column: 1 / -1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in Vue, generated from the HTML demo. The frameworks guide has the project setup.
vue/App.vue
<template>
<div class="vue-root" theme="industrial" v-pre>
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.JogPanel is the manual motion panel of a machine pendant, with axis positions from the drive, continuous or incremental jogging, a speed override and a stop button.</h2>
<p>
In <em>Cont.</em> mode an axis moves while its jog button is held and stops when the button is released, when the pointer leaves the button, when focus moves away or when an interlock is set. With a step selected, each press requests one increment and there is no auto-repeat. Only one axis jogs at a time, a soft limit prevents jogging in the direction that would cross it, and the position readout shows the position reported by the drive.
</p>
</section>
<section>
<h2>An XYZ stage</h2>
<p>
The simulated drive moves at 20 mm/s multiplied by the speed override, has soft limits of ±150 mm in X and Y and 0–80 mm in Z, and takes a moment to home. Clear the <em>Manual mode</em> checkbox to interlock the panel through the machine mode.
</p>
<div class="stage">
<smart-jog-panel id="stage" label="Stage 1" unit="mm" speed="25"></smart-jog-panel>
<div class="plot">
<canvas id="plot" width="300" height="300" aria-label="Plan view of the stage"></canvas>
<div class="z-bar"><div id="zFill" class="z-fill"></div><span class="z-label">Z</span></div>
<ol id="log" class="log"></ol>
<div class="demo-actions">
<label><input type="checkbox" id="manual" checked /> Manual mode</label>
<label><input type="checkbox" id="touch" /> Touch density</label>
</div>
</div>
</div>
<p class="hint">
Keyboard: Tab to a jog button and hold Space to jog; Escape stops. The step and speed rows are radio groups where the arrow keys move and select. Everything shown on the panel comes from the drive.
</p>
</section>
</div>
</div>
</template>
<script>
import { onMounted } from "vue";
import "smart-industrial/source/styles/smart.default.css";
import "smart-industrial/source/styles/smart.industrial-elements.css";
import "smart-industrial/source/styles/smart.industrial.css";
import "smart-industrial/source/styles/smart.industrial-dark.css";
import "smart-industrial/source/modules/smart.jogpanel.js";
/* A simulated three-axis drive. The panel raises jogStart, jogStop, jogStep, homeRequest
and stop; the drive here moves at its own pace and reports positions back through
update(), which is all the panel ever shows. Replace `drive` with the motion controller. */
export default {
name: "app",
setup() {
onMounted(() => {
/* The elements render after Vue has mounted them, and the demo's logic - written for
a page's load event - reads their parts, so it waits for every element in the tree
to have rendered first. */
const pending = Array.from(document.querySelector(".vue-root").querySelectorAll("*"))
.filter((element) => typeof element.whenRendered === "function" && !element.isRendered)
.map((element) => element.whenRendered());
Promise.all(pending).then(() => {
const stage = document.getElementById('stage'),
plot = document.getElementById('plot'),
zFill = document.getElementById('zFill'),
log = document.getElementById('log'),
manual = document.getElementById('manual'),
touch = document.getElementById('touch');
const RATE = 20; /* mm/s at 100% */
const drive = {
axes: {
X: { position: 12.5, min: -150, max: 150, homed: true, velocity: 0, target: null, state: '' },
Y: { position: -40, min: -150, max: 150, homed: true, velocity: 0, target: null, state: '' },
Z: { position: 25, min: 0, max: 80, homed: false, velocity: 0, target: null, state: '' }
},
homing: null
};
function note(text) {
const item = document.createElement('li');
item.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }) + ' ' + text;
log.insertBefore(item, log.firstChild);
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
stage.axes = Object.keys(drive.axes).map(function (id) {
const axis = drive.axes[id];
return { id: id, position: axis.position, min: axis.min, max: axis.max, homed: axis.homed };
});
/* ── The panel's requests ───────────────────────────────────────────── */
stage.addEventListener('jogStart', function (event) {
const axis = drive.axes[event.detail.axis];
axis.velocity = event.detail.direction * RATE * event.detail.speed / 100;
axis.target = null;
axis.state = 'moving';
note('jogStart ' + event.detail.axis + (event.detail.direction > 0 ? '+' : '-') + ' at ' + event.detail.speed + '%');
});
stage.addEventListener('jogStop', function (event) {
const axis = drive.axes[event.detail.axis];
axis.velocity = 0;
axis.state = '';
note('jogStop ' + event.detail.axis + ' after ' + event.detail.duration + ' ms');
});
stage.addEventListener('jogStep', function (event) {
const axis = drive.axes[event.detail.axis],
target = Math.min(axis.max, Math.max(axis.min, axis.position + event.detail.direction * event.detail.step));
axis.target = target;
axis.velocity = Math.sign(target - axis.position) * RATE * event.detail.speed / 100;
axis.state = 'moving';
note('jogStep ' + event.detail.axis + (event.detail.direction > 0 ? '+' : '-') + event.detail.step + ' mm');
});
stage.addEventListener('homeRequest', function (event) {
const ids = event.detail.axis ? [event.detail.axis] : Object.keys(drive.axes);
ids.forEach(function (id) {
const axis = drive.axes[id];
axis.target = 0;
axis.velocity = Math.sign(-axis.position) * RATE * 0.5;
axis.state = 'homing';
axis.homed = false;
});
note('homeRequest ' + (event.detail.axis || 'all'));
});
stage.addEventListener('stop', function () {
Object.keys(drive.axes).forEach(function (id) {
drive.axes[id].velocity = 0;
drive.axes[id].target = null;
drive.axes[id].state = '';
});
note('stop');
});
stage.addEventListener('blocked', function (event) {
note('blocked: ' + event.detail.reason);
});
stage.addEventListener('stepChange', function (event) {
note('step ' + (event.detail.step || 'continuous'));
});
stage.addEventListener('speedChange', function (event) {
note('speed ' + event.detail.speed + '%');
});
/* ── The drive, reporting ───────────────────────────────────────────── */
let last = performance.now();
function tick(now) {
const dt = Math.min(0.1, (now - last) / 1000);
last = now;
Object.keys(drive.axes).forEach(function (id) {
const axis = drive.axes[id];
/* One report after the axis stops, so the panel sees the state clear. */
if (axis.velocity === 0) {
if (axis.reported) {
axis.reported = false;
stage.update(id, { position: axis.position, state: axis.state, homed: axis.homed });
}
return;
}
axis.reported = true;
let next = axis.position + axis.velocity * dt,
arrived = false;
if (axis.target !== null && (axis.velocity > 0 ? next >= axis.target : next <= axis.target)) {
next = axis.target;
arrived = true;
}
if (next >= axis.max) {
next = axis.max;
arrived = true;
}
else if (next <= axis.min) {
next = axis.min;
arrived = true;
}
axis.position = next;
if (arrived) {
const wasHoming = axis.state === 'homing';
axis.velocity = 0;
axis.target = null;
axis.state = '';
if (wasHoming) {
axis.homed = true;
note(id + ' homed');
}
}
stage.update(id, { position: axis.position, state: axis.state, homed: axis.homed });
});
draw();
requestAnimationFrame(tick);
}
/* ── The plan view ──────────────────────────────────────────────────── */
function draw() {
const ctx = plot.getContext('2d'),
w = plot.width,
h = plot.height,
scale = (w - 40) / 300,
x = w / 2 + drive.axes.X.position * scale,
y = h / 2 - drive.axes.Y.position * scale,
text = getComputedStyle(plot).color;
ctx.clearRect(0, 0, w, h);
ctx.save();
ctx.strokeStyle = text;
ctx.globalAlpha = 0.25;
ctx.lineWidth = 1;
ctx.strokeRect(20, 20, w - 40, h - 40);
ctx.beginPath();
ctx.moveTo(w / 2, 20);
ctx.lineTo(w / 2, h - 20);
ctx.moveTo(20, h / 2);
ctx.lineTo(w - 20, h / 2);
ctx.stroke();
ctx.globalAlpha = 1;
ctx.fillStyle = text;
ctx.font = '11px sans-serif';
ctx.fillText('-150', 22, h - 24);
ctx.fillText('+150', w - 48, h - 24);
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.moveTo(x - 12, y);
ctx.lineTo(x + 12, y);
ctx.moveTo(x, y - 12);
ctx.lineTo(x, y + 12);
ctx.stroke();
ctx.restore();
zFill.style.height = (drive.axes.Z.position / 80 * 100) + '%';
}
/* ── Chrome ─────────────────────────────────────────────────────────── */
manual.addEventListener('change', function () {
stage.interlocked = !manual.checked;
stage.interlockReason = manual.checked ? '' : 'machine not in Manual';
});
touch.addEventListener('change', function () {
stage.density = touch.checked ? 'touch' : 'normal';
});
draw();
requestAnimationFrame(tick);
});
});
}
};
</script>
<style>
.stage {
display: grid;
grid-template-columns: auto minmax(260px, 1fr);
gap: 24px;
align-items: start;
}
@media (max-width: 900px) {
.stage {
grid-template-columns: 1fr;
}
}
.plot {
display: grid;
grid-template-columns: auto auto;
justify-content: start;
gap: 12px;
align-items: start;
}
#plot {
width: 300px;
height: 300px;
max-width: 100%;
color: var(--smart-text-color);
border: 1px solid var(--smart-border);
border-radius: 4px;
}
.z-bar {
position: relative;
width: 28px;
height: 300px;
border: 1px solid var(--smart-border);
border-radius: 4px;
overflow: hidden;
}
.z-fill {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 0;
background: var(--smart-primary);
opacity: 0.6;
transition: height 80ms linear;
}
.z-label {
position: absolute;
top: 4px;
left: 0;
right: 0;
text-align: center;
font-size: 11px;
font-weight: 600;
}
.log {
grid-column: 1 / -1;
margin: 0;
padding: 0;
list-style: none;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
}
.log li {
padding: 2px 0;
}
.demo-actions {
grid-column: 1 / -1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.hint {
margin-top: 10px;
font-size: 12px;
opacity: 0.75;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
</style>
What the demo shows
- An XYZ stage
Also in instrument readouts and controls
Bar GraphBit FieldKnobMomentary ButtonNumeric KeypadPanel MeterSelector SwitchSeven SegmentThermometerTouch Keyboard