From 82daebcdd0cfc60192a4c2a06610d04498c6dd4e Mon Sep 17 00:00:00 2001 From: Martin Fuchs Date: Sat, 5 Sep 2026 12:59:28 +0200 Subject: [PATCH] abort plot --- examples/netz/index.html | 2 +- examples/zickzack/index.html | 2 +- lib/README.md | 12 ++- lib/WallPlotter.js | 140 ++++++++++++++++++++++++++++++----- 4 files changed, 131 insertions(+), 25 deletions(-) diff --git a/examples/netz/index.html b/examples/netz/index.html index fabaebe..b456ba1 100644 --- a/examples/netz/index.html +++ b/examples/netz/index.html @@ -42,7 +42,7 @@ } - + diff --git a/examples/zickzack/index.html b/examples/zickzack/index.html index fabaebe..b456ba1 100644 --- a/examples/zickzack/index.html +++ b/examples/zickzack/index.html @@ -42,7 +42,7 @@ } - + diff --git a/lib/README.md b/lib/README.md index db95de1..6dcbe4a 100644 --- a/lib/README.md +++ b/lib/README.md @@ -48,6 +48,7 @@ Keyboard controls. Call from `setup()`. No arguments. |-----|--------| | `C` | Connect / disconnect | | `T` | Run Test (preview) | +| `A` | Abort / reset Test to start | | `H` | Set home here (`G92 X0 Y0`) — required before Plot | | `0` | Travel to `(0, 0)` | | `P` | Toggle pen | @@ -57,9 +58,9 @@ Keyboard controls. Call from `setup()`. No arguments. ### `enablePlotButtons(options?)` -Adds Connect, Plot, and Test buttons. Expects global `plot()` and `test()` in the sketch. +Adds Connect, Plot, Test, and Abort buttons. Expects global `plot()` and `test()` in the sketch. -**Connect** is shown only while disconnected (same as key **C**). While a path is running, Plot becomes **Pause**; while paused it becomes **Resume**. Test becomes **Stop** during a test. +**Connect** is shown only while disconnected (same as key **C**). While a path is running, Plot becomes **Pause**; while paused it becomes **Resume**. Test becomes **Stop** during a test. **Abort** stops and resets the Test preview to the start (key **A**). | Option | Default | What it is | |--------|---------|------------| @@ -70,6 +71,7 @@ Adds Connect, Plot, and Test buttons. Expects global `plot()` and `test()` in th | `resumeLabel` | `'Resume'` | Plot button while paused | | `testLabel` | `'Test'` | Test button text | | `stopLabel` | `'Stop'` | Test button while a test is running | +| `abortLabel` | `'Abort'` | Reset test/plot preview to the start | ### `showInfos()` @@ -155,9 +157,11 @@ Returns estimated duration in **milliseconds** for the current path buffer, or f `stopPlot()` first press is the same as **Pause**: finish the current stroke, then **Resume** continues. Second press stops the browser from sending more moves. The **in-flight** `G1` still runs — this firmware does not have `EMERGENCY_PARSER`, so `M410` cannot interrupt motion (and on a polargraph it desyncs belts). Unplug power for a hard stop. -### `pausePlot()` / `resumePlot()` +### `pausePlot()` / `resumePlot()` / `abortToStart()` -**Pause** / first **Stop** wait for the current segment to finish (`M400`), then lift the pen. Do not resume after emergency stop until you re-home. Elapsed / remaining HUD time freezes while paused. +**Pause** / first **Stop** wait for the current segment to finish (`M400`), then lift the pen. **Resume** continues from there. + +**Abort** (button or **A**) stops the run and resets the Test preview to the start. Live Plot only stops sending further moves; the machine stays where it is. --- diff --git a/lib/WallPlotter.js b/lib/WallPlotter.js index 482954a..6fab374 100644 --- a/lib/WallPlotter.js +++ b/lib/WallPlotter.js @@ -18,7 +18,7 @@ class WallPlotter { // Static constants // --------------------------------------------------------------------------- - static VERSION = '2.3.13'; + static VERSION = '2.3.15'; static MIN_FEED_MM_MIN = 1; static MAX_FEED_MM_MIN = 10000; /** Offline overlay size when not connected (mm) — 150×150 cm centred on origin */ @@ -133,14 +133,19 @@ class WallPlotter { this._connectBtn = null; this._plotBtn = null; this._testBtn = null; + this._abortBtn = null; this._connectLabel = 'Connect'; this._plotLabel = 'Plot'; this._pauseLabel = 'Pause'; this._resumeLabel = 'Resume'; this._testLabel = 'Test'; this._stopLabel = 'Stop'; + this._abortLabel = 'Abort'; this._lastTestBtnClick = 0; this._lastPlotBtnClick = 0; + this._pathStartX = 0; + this._pathStartY = 0; + this._wantPathStartReset = false; // --- Callbacks --- const userOnLog = options.onLog ?? (() => {}); @@ -222,7 +227,8 @@ class WallPlotter { /** * Add Connect / Plot / Test buttons. Expects global sketch functions plot() and test(). * Connect is shown only while disconnected. Plot becomes Pause while running, Resume while paused. - * @param {{parentId?: string, connectLabel?: string, plotLabel?: string, pauseLabel?: string, resumeLabel?: string, testLabel?: string, stopLabel?: string}} [options] + * Abort stops and resets the Test preview to the start. + * @param {{parentId?: string, connectLabel?: string, plotLabel?: string, pauseLabel?: string, resumeLabel?: string, testLabel?: string, stopLabel?: string, abortLabel?: string}} [options] */ enablePlotButtons(options = {}) { if (this._plotButtonsEnabled) return; @@ -244,6 +250,7 @@ class WallPlotter { this._resumeLabel = options.resumeLabel ?? 'Resume'; this._testLabel = options.testLabel ?? 'Test'; this._stopLabel = options.stopLabel ?? 'Stop'; + this._abortLabel = options.abortLabel ?? 'Abort'; const plotter = this; const connectBtn = createButton(this._connectLabel); @@ -260,6 +267,11 @@ class WallPlotter { testBtn.parent(parent); testBtn.mousePressed(() => plotter._onTestButton()); this._testBtn = testBtn; + + const abortBtn = createButton(this._abortLabel); + abortBtn.parent(parent); + abortBtn.mousePressed(() => plotter.abortToStart()); + this._abortBtn = abortBtn; this._updatePlotButtons(); } @@ -382,6 +394,34 @@ class WallPlotter { this.onLog('plot resumed'); } + /** + * Stop the current Test/Plot and reset the preview to the start. + * Test pose returns to where the path began. Live Plot only stops sending + * (the machine stays where it is). + */ + abortToStart() { + this._clearResumeState(); + this._wantPathStartReset = true; + this._pathRunId += 1; + if (this._job === 'running' || this._job === 'paused') this._abortJob(); + this._applyPathStartReset(); + this.onLog('drawing cleared'); + } + + _applyPathStartReset() { + this.x = this._pathStartX; + this.y = this._pathStartY; + this.isPenDown = false; + this.plotProgress = 0; + this.previewPath = []; + this._path = []; + this._wantPathStartReset = false; + this._plotTestOnly = false; + this._setJob('idle'); + this._redrawSketch(); + this._updatePlotButtons(); + } + /** * First press: pause after the current stroke (safe to Resume). * Second press: stop sending more moves. The in-flight G1 still finishes @@ -1008,6 +1048,10 @@ class WallPlotter { this._pathLoopActive = true; this._plotTestOnly = testOnly; this.previewPath = commands; + if (fromIndex === 0 && !timing) { + this._pathStartX = this.x; + this._pathStartY = this.y; + } const startAt = Math.max(0, Math.min(fromIndex, commands.length)); this.plotProgress = startAt; if (timing) { @@ -1117,9 +1161,13 @@ class WallPlotter { this.isPenDown = false; } this._plotTestOnly = false; - this._setJob('idle'); this._pathLoopActive = false; - if (this.plotProgress >= commands.length) this._clearResumeState(); + if (this._wantPathStartReset || this.previewPath.length === 0) { + this._applyPathStartReset(); + } else { + this._setJob('idle'); + if (this.plotProgress >= commands.length) this._clearResumeState(); + } } } @@ -1516,6 +1564,10 @@ class WallPlotter { await this.stopPlot(); return false; } + if (k === 'a') { + this.abortToStart(); + return false; + } if (k === 't') { this._onTestButton(); return false; @@ -1690,6 +1742,11 @@ class WallPlotter { this._job !== 'idle' && this._plotTestOnly ? this._stopLabel : this._testLabel ); } + if (this._abortBtn) { + const active = + this._job !== 'idle' || !!this._interrupted || this.previewPath.length > 0; + this._abortBtn.elt.disabled = !active; + } } _triggerSketchFn(name) { @@ -1750,22 +1807,47 @@ class WallPlotter { bottom: 0.75rem; z-index: 20; max-width: min(52rem, calc(100vw - 1.5rem)); - max-height: 42vh; + pointer-events: auto; + background: #141418; + border: none; + border-radius: 6px; + margin: 0; + overflow: hidden; + } + #wallplotter-log-toggle { + display: block; + width: 100%; + background: #141418; + border: none; + color: #aaa; + cursor: pointer; + font-family: system-ui, sans-serif; + font-size: 0.75rem; + letter-spacing: 0.04em; + text-align: left; + padding: 0.4rem 0.65rem; + } + #wallplotter-log-toggle:hover { + color: #eee; + } + #wallplotter-log-body { + max-height: 38vh; overflow: auto; user-select: text; -webkit-user-select: text; - pointer-events: auto; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.4; color: #c8c8d0; - background: rgba(18, 18, 22, 0.88); - border: 1px solid #3a3a42; - border-radius: 6px; - padding: 0.45rem 0.6rem; + background: #141418; + border: none; + margin: 0; + padding: 0 0.65rem 0.5rem; white-space: pre-wrap; word-break: break-word; - margin: 0; + } + #wallplotter-log.collapsed #wallplotter-log-body { + display: none; } `; document.head.appendChild(style); @@ -2003,6 +2085,7 @@ class WallPlotter { '0 go to origin (0, 0)', 'P toggle pen (test up/down angles)', 'T run test (preview)', + 'A abort / reset test to start', 'S stop sending (in-flight stroke still finishes)', 'C connect / disconnect', ]; @@ -2014,23 +2097,42 @@ class WallPlotter { _ensureLogOverlay() { this._injectPlotButtonStyles(); - let el = document.getElementById('wallplotter-log'); - if (el) return el; - el = document.createElement('pre'); - el.id = 'wallplotter-log'; - el.setAttribute('aria-label', 'Serial log'); - document.body.appendChild(el); - return el; + let wrap = document.getElementById('wallplotter-log'); + let body = document.getElementById('wallplotter-log-body'); + if (wrap && body) return body; + if (wrap) wrap.remove(); + + wrap = document.createElement('div'); + wrap.id = 'wallplotter-log'; + wrap.setAttribute('aria-label', 'Serial log'); + + const toggle = document.createElement('button'); + toggle.id = 'wallplotter-log-toggle'; + toggle.type = 'button'; + toggle.textContent = 'Serial'; + toggle.addEventListener('click', () => { + wrap.classList.toggle('collapsed'); + toggle.textContent = wrap.classList.contains('collapsed') ? 'Serial ▸' : 'Serial ▾'; + }); + + body = document.createElement('pre'); + body.id = 'wallplotter-log-body'; + wrap.appendChild(toggle); + wrap.appendChild(body); + document.body.appendChild(wrap); + toggle.textContent = 'Serial ▾'; + return body; } _syncLogOverlay() { const el = this._ensureLogOverlay(); + const wrap = document.getElementById('wallplotter-log'); const text = this.logLines.join('\n'); if (el.textContent !== text) { el.textContent = text; el.scrollTop = el.scrollHeight; } - el.style.display = this.logLines.length ? 'block' : 'none'; + if (wrap) wrap.style.display = this.logLines.length ? 'block' : 'none'; } // ===========================================================================