From c129222b78a9c4cef32598b9cdf5be05a8943062 Mon Sep 17 00:00:00 2001 From: Martin Fuchs Date: Sun, 6 Sep 2026 22:51:29 +0200 Subject: [PATCH] bounding box plot --- examples/rechteck/index.html | 2 +- examples/zickzack/index.html | 2 +- examples/zickzack/sketch.js | 6 +- lib/README.md | 2 +- lib/WallPlotter.js | 325 +++++++++++++++++++++++++++-------- lib/doc.md | 18 +- tutorial.md | 51 ++++-- 7 files changed, 301 insertions(+), 105 deletions(-) diff --git a/examples/rechteck/index.html b/examples/rechteck/index.html index f113928..ddc556b 100644 --- a/examples/rechteck/index.html +++ b/examples/rechteck/index.html @@ -42,7 +42,7 @@ } - + diff --git a/examples/zickzack/index.html b/examples/zickzack/index.html index b456ba1..1647fee 100644 --- a/examples/zickzack/index.html +++ b/examples/zickzack/index.html @@ -42,7 +42,7 @@ } - + diff --git a/examples/zickzack/sketch.js b/examples/zickzack/sketch.js index 37ea3ea..55e7283 100644 --- a/examples/zickzack/sketch.js +++ b/examples/zickzack/sketch.js @@ -46,10 +46,8 @@ async function drawPlot(testOnly) { for (let y = -area; y <= area; y+=step){ for (let x = -area; x <= area; x+=step){ - //if(random(1) < 0.8){ - wp.draw(x, y); - wp.draw(x+(random(1)<0.9), y+random(step*1.5)); - //} + wp.move(x, y); + wp.draw(x+step, y+step); } } await wp.runPath(testOnly); diff --git a/lib/README.md b/lib/README.md index b97b8bc..fee319d 100644 --- a/lib/README.md +++ b/lib/README.md @@ -40,6 +40,6 @@ async function drawPlot(testOnly) { } ``` -Press **C** to connect, **H** to set home, then **Test** or **Plot**. +Press **C** to connect, **H** to set home, then **Test**. **Plot** and **Bounding box** appear after a preview exists. Example sketches: `examples/zickzack/`, `examples/rechteck/`. Full API: [doc.md](doc.md). diff --git a/lib/WallPlotter.js b/lib/WallPlotter.js index 6fab374..2ef8c3b 100644 --- a/lib/WallPlotter.js +++ b/lib/WallPlotter.js @@ -18,7 +18,7 @@ class WallPlotter { // Static constants // --------------------------------------------------------------------------- - static VERSION = '2.3.15'; + static VERSION = '2.3.24'; 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,16 @@ class WallPlotter { this._connectBtn = null; this._plotBtn = null; this._testBtn = null; + this._bboxBtn = null; this._abortBtn = null; this._connectLabel = 'Connect'; this._plotLabel = 'Plot'; this._pauseLabel = 'Pause'; this._resumeLabel = 'Resume'; this._testLabel = 'Test'; - this._stopLabel = 'Stop'; + this._bboxLabel = 'Bounding box'; this._abortLabel = 'Abort'; + this._bboxRunning = false; this._lastTestBtnClick = 0; this._lastPlotBtnClick = 0; this._pathStartX = 0; @@ -225,10 +227,12 @@ 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. - * 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] + * Add Connect / Test / Plot / Bounding box / Abort. + * No sketch yet: Test (+ Connect if disconnected). After Test, Bounding box + * appears; Plot appears once connected. Plot sends that path (does not regenerate) and + * becomes Pause / Resume while plotting. Abort only while plotting or when a plot is saved. + * Bounding box travels pen-up around the drawing extents. + * @param {{parentId?: string, connectLabel?: string, plotLabel?: string, pauseLabel?: string, resumeLabel?: string, testLabel?: string, bboxLabel?: string, abortLabel?: string}} [options] */ enablePlotButtons(options = {}) { if (this._plotButtonsEnabled) return; @@ -249,7 +253,7 @@ class WallPlotter { this._pauseLabel = options.pauseLabel ?? 'Pause'; this._resumeLabel = options.resumeLabel ?? 'Resume'; this._testLabel = options.testLabel ?? 'Test'; - this._stopLabel = options.stopLabel ?? 'Stop'; + this._bboxLabel = options.bboxLabel ?? 'Bounding box'; this._abortLabel = options.abortLabel ?? 'Abort'; const plotter = this; @@ -258,19 +262,24 @@ class WallPlotter { connectBtn.mousePressed(() => plotter._onConnectButton()); this._connectBtn = connectBtn; - const plotBtn = createButton(this._plotLabel); - plotBtn.parent(parent); - plotBtn.mousePressed(() => plotter._onPlotButton()); - this._plotBtn = plotBtn; - const testBtn = createButton(this._testLabel); testBtn.parent(parent); testBtn.mousePressed(() => plotter._onTestButton()); this._testBtn = testBtn; + const plotBtn = createButton(this._plotLabel); + plotBtn.parent(parent); + plotBtn.mousePressed(() => plotter._onPlotButton()); + this._plotBtn = plotBtn; + + const bboxBtn = createButton(this._bboxLabel); + bboxBtn.parent(parent); + bboxBtn.mousePressed(() => plotter._onBoundsButton()); + this._bboxBtn = bboxBtn; + const abortBtn = createButton(this._abortLabel); abortBtn.parent(parent); - abortBtn.mousePressed(() => plotter.abortToStart()); + abortBtn.mousePressed(() => plotter._abortLivePlot()); this._abortBtn = abortBtn; this._updatePlotButtons(); } @@ -954,6 +963,10 @@ class WallPlotter { /** G92 X0 Y0 — set current pose as home. Refused while plotting or waiting for Marlin. */ async setHomeHere() { + if (this._bboxRunning) { + this.onLog('! cannot set home while travelling the bounding box'); + return; + } if (this._job !== 'idle') { this.onLog('! cannot set home while plotting — resume or abort first'); this.statusText = 'Finish or abort plot before H'; @@ -1530,7 +1543,7 @@ class WallPlotter { } _isMachineIdle() { - return this._job === 'idle' && !this._okWaiter; + return this._job === 'idle' && !this._okWaiter && !this._bboxRunning; } _showHomeRequiredWarning() { @@ -1561,17 +1574,26 @@ class WallPlotter { return false; } if (k === 's') { - await this.stopPlot(); + if (!this._isLivePlot()) return false; + if (this._job === 'paused') this.resumePlot(); + else if (this._job === 'running') this.pausePlot(); return false; } if (k === 'a') { - this.abortToStart(); + if (!this._isLivePlot() && !this._hasSavedPlot()) return false; + this._abortLivePlot(); return false; } if (k === 't') { + if (this._isLivePlot() || this._bboxRunning) return false; this._onTestButton(); return false; } + if (k === 'b') { + if (this._isLivePlot() || this._bboxRunning) return false; + this._onBoundsButton(); + return false; + } if (!this._keysEnabled || !this.connected) return; @@ -1676,41 +1698,136 @@ class WallPlotter { this._toggleConnection(); } + _isLivePlot() { + return this._job !== 'idle' && !this._plotTestOnly; + } + + _hasSavedPlot() { + return !!(this._interrupted && !this._interrupted.testOnly); + } + + _hasDrawnSketch() { + return this.previewPath.length > 0; + } + + _abortLivePlot() { + if (this._hasSavedPlot() && this._job === 'idle') { + this._clearResumeState(); + this.previewPath = []; + this._path = []; + this.plotProgress = 0; + this.onLog('saved plot discarded — Test this sketch before Plot'); + this._syncStatus(); + this._updatePlotButtons(); + this._redrawSketch(); + return; + } + if (!this._isLivePlot()) return; + this._clearResumeState(); + this._abortJob(); + this.onLog( + 'plot aborted — motors finish the in-flight move (unplug power for a hard stop)' + ); + } + + async _plotExistingPath() { + const commands = this.previewPath; + if (!commands.length || !this.connected) return; + if (this._pathLoopActive) this._abortJob(); + while (this._pathLoopActive) await this._delay(16); + await this._executePath(commands, false); + } + _onPlotButton() { - if (this._job === 'paused') { + if (this._bboxRunning) return; + if (this._isLivePlot() && this._job === 'paused') { this.resumePlot(); return; } - if (this._job === 'running') { + if (this._isLivePlot() && this._job === 'running') { this.pausePlot(); return; } + if (this._job === 'aborting') return; + if (this._job !== 'idle' && this._plotTestOnly) { + this._plotExistingPath(); + return; + } if (this._job !== 'idle' || !this.connected) return; - const now = typeof millis === 'function' ? millis() : Date.now(); if (this._interrupted && !this._interrupted.testOnly) { - if (now - this._lastPlotBtnClick < 450) { - this._lastPlotBtnClick = now; - this._clearResumeState(); - this._triggerSketchFn('plot'); - return; - } - this._lastPlotBtnClick = now; this._resumeInterruptedPath(); return; } - this._lastPlotBtnClick = now; - this._triggerSketchFn('plot'); + this._plotExistingPath(); + } + + _previewBounds() { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + let n = 0; + for (const cmd of this.previewPath) { + if (cmd.type !== 'move' && cmd.type !== 'draw') continue; + minX = Math.min(minX, cmd.x); + maxX = Math.max(maxX, cmd.x); + minY = Math.min(minY, cmd.y); + maxY = Math.max(maxY, cmd.y); + n += 1; + } + if (!n) return null; + return { minX, minY, maxX, maxY }; + } + + async _onBoundsButton() { + if (this._isLivePlot() || this._bboxRunning) return; + if (!this.connected || !this._requireHome()) return; + const box = this._previewBounds(); + if (!box) return; + if (this._job !== 'idle' && this._plotTestOnly) { + this._abortJob(); + while (this._pathLoopActive) await this._delay(16); + } + if (this._job !== 'idle') return; + this._bboxRunning = true; + this._updatePlotButtons(); + const { minX, minY, maxX, maxY } = box; + this.onLog( + `bounding box ${minX.toFixed(0)},${minY.toFixed(0)} … ${maxX.toFixed(0)},${maxY.toFixed(0)} mm (pen up)` + ); + try { + await this.penUp(); + const corners = [ + { type: 'move', x: minX, y: minY }, + { type: 'move', x: maxX, y: minY }, + { type: 'move', x: maxX, y: maxY }, + { type: 'move', x: minX, y: maxY }, + { type: 'move', x: minX, y: minY }, + ]; + let px = this.x; + let py = this.y; + this.statusText = 'Bounding box (pen up)'; + for (const cmd of corners) { + const duration = this._segmentDurationMs(px, py, cmd); + this._lastSegmentMs = duration; + await Promise.all([ + this._sendSegmentAndWait(cmd), + this._animatePosition(px, py, cmd, duration), + ]); + px = cmd.x; + py = cmd.y; + } + } catch (err) { + if (err.name !== 'AbortError') this.onLog('! ' + err.message); + } finally { + this._bboxRunning = false; + this._syncStatus(); + this._updatePlotButtons(); + } } _onTestButton() { - const now = millis(); - if (this._job === 'running' && this._plotTestOnly) { - if (now - this._lastTestBtnClick < 450) this._restartPath('test'); - else this.stopPlot(); - this._lastTestBtnClick = now; - return; - } - this._lastTestBtnClick = now; + if (this._isLivePlot() || this._bboxRunning) return; this._restartPath('test'); } @@ -1721,31 +1838,44 @@ class WallPlotter { } _updatePlotButtons() { + const livePlot = this._isLivePlot(); + const savedPlot = this._hasSavedPlot(); + const sketchVisible = this._hasDrawnSketch() || livePlot || savedPlot; + if (this._connectBtn) { if (this.connected) this._connectBtn.hide(); else this._connectBtn.show(); this._connectBtn.elt.disabled = this._link === 'connecting'; } + if (this._testBtn) { + if (livePlot) this._testBtn.hide(); + else this._testBtn.show(); + this._testBtn.html(this._testLabel); + this._testBtn.elt.disabled = this._bboxRunning; + } if (this._plotBtn) { + if (this.connected && sketchVisible) this._plotBtn.show(); + else this._plotBtn.hide(); let label = this._plotLabel; - if (this._job === 'paused') label = this._resumeLabel; - else if (this._interrupted && this._job === 'idle') label = this._resumeLabel; - else if (this._job === 'running' || this._job === 'aborting') { + if (this._job === 'paused' && livePlot) label = this._resumeLabel; + else if (savedPlot && this._job === 'idle') label = this._resumeLabel; + else if (livePlot && (this._job === 'running' || this._job === 'aborting')) { label = this._pauseLabel; } this._plotBtn.html(label); this._plotBtn.elt.disabled = - this._job === 'aborting' || (!this.connected && this._job === 'idle'); + this._job === 'aborting' || this._bboxRunning; } - if (this._testBtn) { - this._testBtn.html( - this._job !== 'idle' && this._plotTestOnly ? this._stopLabel : this._testLabel - ); + if (this._bboxBtn) { + if (sketchVisible && !livePlot) this._bboxBtn.show(); + else this._bboxBtn.hide(); + this._bboxBtn.elt.disabled = + this._bboxRunning || !this.connected || !this.homeSet; } if (this._abortBtn) { - const active = - this._job !== 'idle' || !!this._interrupted || this.previewPath.length > 0; - this._abortBtn.elt.disabled = !active; + if (livePlot || savedPlot) this._abortBtn.show(); + else this._abortBtn.hide(); + this._abortBtn.elt.disabled = this._job === 'aborting'; } } @@ -1762,9 +1892,12 @@ class WallPlotter { } _injectPlotButtonStyles() { - if (document.getElementById('wallplotter-btn-styles')) return; - const style = document.createElement('style'); - style.id = 'wallplotter-btn-styles'; + let style = document.getElementById('wallplotter-btn-styles'); + if (!style) { + style = document.createElement('style'); + style.id = 'wallplotter-btn-styles'; + document.head.appendChild(style); + } style.textContent = ` #wallplotter-controls { position: absolute; @@ -1772,10 +1905,14 @@ class WallPlotter { right: 0.75rem; z-index: 20; display: flex; + flex-wrap: wrap; + justify-content: flex-end; gap: 0.5rem; margin: 0; + max-width: calc(100vw - 1.5rem); } - #wallplotter-controls button { + #wallplotter-controls button, + #wallplotter-log-toggle { background: rgba(42, 42, 48, 0.9); border: 1px solid #444; border-radius: 6px; @@ -1786,7 +1923,8 @@ class WallPlotter { line-height: 1; padding: 0.4rem 1rem; } - #wallplotter-controls button:hover { + #wallplotter-controls button:hover, + #wallplotter-log-toggle:hover { background: #35353c; border-color: #666; color: #eee; @@ -1806,29 +1944,38 @@ class WallPlotter { right: 0.75rem; bottom: 0.75rem; z-index: 20; - max-width: min(52rem, calc(100vw - 1.5rem)); + box-sizing: border-box; + width: min(40rem, calc(100vw - 1.5rem)); pointer-events: auto; background: #141418; - border: none; + border: 1px solid #333; border-radius: 6px; margin: 0; overflow: hidden; } - #wallplotter-log-toggle { - display: block; - width: 100%; - background: #141418; + #wallplotter-log.collapsed { + width: auto; + background: transparent; border: none; - color: #aaa; - cursor: pointer; + } + #wallplotter-log-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding: 0.35rem 0.4rem 0.35rem 0.65rem; + } + #wallplotter-log.collapsed #wallplotter-log-header { + padding: 0; + } + #wallplotter-log-title { + color: #ccc; 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.collapsed #wallplotter-log-title { + display: none; } #wallplotter-log-body { max-height: 38vh; @@ -1844,13 +1991,13 @@ class WallPlotter { margin: 0; padding: 0 0.65rem 0.5rem; white-space: pre-wrap; + overflow-wrap: anywhere; word-break: break-word; } #wallplotter-log.collapsed #wallplotter-log-body { display: none; } `; - document.head.appendChild(style); } // =========================================================================== @@ -1932,6 +2079,7 @@ class WallPlotter { this._drawBoundSizeLabels(bounds, tl, br, w, h); this._drawPreviewPath(layout); + this._drawSketchBounds(layout); this._drawGondola(layout); pop(); @@ -1958,6 +2106,20 @@ class WallPlotter { pop(); } + _drawSketchBounds(layout) { + const box = this._previewBounds(); + if (!box) return; + const tl = this._worldToScreen(box.minX, box.maxY, layout); + const br = this._worldToScreen(box.maxX, box.minY, layout); + noFill(); + stroke(90, 200, 255, 180); + strokeWeight(1); + drawingContext.setLineDash([6, 5]); + rectMode(CORNER); + rect(tl.x, tl.y, br.x - tl.x, br.y - tl.y); + drawingContext.setLineDash([]); + } + _drawPreviewPath(layout) { if (!this.previewPath.length) return; @@ -1990,6 +2152,7 @@ class WallPlotter { _drawGondola(layout) { const pos = this._worldToScreen(this.x, this.y, layout); + const moving = this.isPlotting || this._bboxRunning; if (this.isPenDown) { stroke(255, 90, 90); fill(255, 90, 90); @@ -1998,8 +2161,8 @@ class WallPlotter { fill(90, 200, 255); } strokeWeight(2); - circle(pos.x, pos.y, this.isPlotting ? 18 : 12); - if (this.isPlotting) { + circle(pos.x, pos.y, moving ? 18 : 12); + if (moving) { noFill(); stroke(255, 255, 255, 120); strokeWeight(1); @@ -2085,8 +2248,9 @@ 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)', + 'B travel drawing bounds (pen up)', + 'A abort plot (in-flight stroke still finishes)', + 'S pause / resume plot', 'C connect / disconnect', ]; for (let i = 0; i < help.length; i++) { @@ -2099,28 +2263,39 @@ class WallPlotter { this._injectPlotButtonStyles(); let wrap = document.getElementById('wallplotter-log'); let body = document.getElementById('wallplotter-log-body'); - if (wrap && body) return body; + const header = document.getElementById('wallplotter-log-header'); + if (wrap && body && header) return body; if (wrap) wrap.remove(); wrap = document.createElement('div'); wrap.id = 'wallplotter-log'; wrap.setAttribute('aria-label', 'Serial log'); + const headerEl = document.createElement('div'); + headerEl.id = 'wallplotter-log-header'; + + const title = document.createElement('span'); + title.id = 'wallplotter-log-title'; + title.textContent = 'Serial'; + const toggle = document.createElement('button'); toggle.id = 'wallplotter-log-toggle'; toggle.type = 'button'; - toggle.textContent = 'Serial'; + toggle.textContent = 'Hide'; + toggle.setAttribute('aria-expanded', 'true'); toggle.addEventListener('click', () => { - wrap.classList.toggle('collapsed'); - toggle.textContent = wrap.classList.contains('collapsed') ? 'Serial ▸' : 'Serial ▾'; + const collapsed = wrap.classList.toggle('collapsed'); + toggle.textContent = collapsed ? 'serial-log' : 'Hide'; + toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true'); }); body = document.createElement('pre'); body.id = 'wallplotter-log-body'; - wrap.appendChild(toggle); + headerEl.appendChild(title); + headerEl.appendChild(toggle); + wrap.appendChild(headerEl); wrap.appendChild(body); document.body.appendChild(wrap); - toggle.textContent = 'Serial ▾'; return body; } diff --git a/lib/doc.md b/lib/doc.md index fa40a5f..09e6ddb 100644 --- a/lib/doc.md +++ b/lib/doc.md @@ -39,8 +39,10 @@ Keyboard controls. Call from `setup()`. No arguments. | Key | Action | |-----|--------| | `C` | Connect / disconnect | -| `T` | Run Test (preview) | -| `A` | Abort / reset Test to start | +| `T` | Run Test (preview). Press again to redraw | +| `B` | Travel the drawing bounding box, pen up | +| `A` | Abort live Plot, or discard a restored plot after reload | +| `S` | Pause / resume live Plot | | `H` | Set home here (`G92 X0 Y0`) — required before Plot | | `0` | Travel to `(0, 0)` | | `P` | Toggle pen | @@ -50,9 +52,9 @@ Keyboard controls. Call from `setup()`. No arguments. ### `enablePlotButtons(options?)` -Adds Connect, Plot, Test, and Abort buttons. Expects global `plot()` and `test()` in the sketch. +Adds Connect, Test, Plot, Bounding box, and Abort. 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. **Abort** stops and resets the Test preview to the start (key **A**). +**Connect** is shown only while disconnected (same as key **C**). With no sketch yet, only **Test** is shown. After Test, **Bounding box** appears. **Plot** appears once connected (and a preview exists). **Plot** sends the existing preview path (it does not call `plot()` again). While plotting, Plot becomes **Pause**, then **Resume**. **Abort** is shown during a live Plot, and after a reload if a plot can be resumed (so you can drop it after switching sketches). **Bounding box** (key **B**) is shown when a sketch is visible and nothing is plotting: the gondola travels the drawing extents with the pen up, without replacing the preview. | Option | Default | What it is | |--------|---------|------------| @@ -62,8 +64,8 @@ Adds Connect, Plot, Test, and Abort buttons. Expects global `plot()` and `test() | `pauseLabel` | `'Pause'` | Plot button while a path is running | | `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 | +| `bboxLabel` | `'Bounding box'` | Travel drawing extents, pen up | +| `abortLabel` | `'Abort'` | Stop sending a live Plot | ### `showInfos()` @@ -151,9 +153,9 @@ Returns estimated duration in **milliseconds** for the current path buffer, or f ### `pausePlot()` / `resumePlot()` / `abortToStart()` -**Pause** / first **Stop** wait for the current segment to finish (`M400`), then lift the pen. **Resume** continues from there. +**Pause** waits for the current segment to finish (`M400`), then lifts 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. +**Abort** is shown during a live Plot, and after reload if a plot was saved to resume. Button or **A**: while plotting it stops sending further moves (the machine finishes the in-flight `G1`). For a restored plot it discards that saved path, so Plot will not continue a previous sketch. During Test, press **Test** again to redraw. --- diff --git a/tutorial.md b/tutorial.md index 0d54bee..20737e2 100644 --- a/tutorial.md +++ b/tutorial.md @@ -91,7 +91,7 @@ p5-wallplotter/ Dann öffne darin *sketch.js*. Den Rest (HTML, Buttons, Verbinden) kannst du lassen. Ändere die Inhalte in der Funktion *drawPlot*: dort steht, was gezeichnet wird. -Chrome, gleicher Server: [http://localhost:8080/sketches/mein-sketch/](http://localhost:8080/sketches/mein-sketch/) (Ordnername in der URL anpassen). Nach dem Speichern die Seite neu laden (*F5*). Der Python-Server muss nicht neu starten. +Chrome, gleicher Server: [http://localhost:8080/sketches/mein-sketch/](http://localhost:8080/sketches/mein-sketch/) (Ordnername in der URL anpassen). Wie du die Vorschau startest und nach Änderungen neu lädst: Abschnitt 3. ### Was *sketch.js* macht @@ -102,7 +102,7 @@ Chrome, gleicher Server: [http://localhost:8080/sketches/mein-sketch/](http://lo - *new WallPlotter({ … })* — Verbindung zum Plotter. *penUpGcode* / *penDownGcode* sind die Servo-Winkel für Stift hoch und runter - *setSpeed(3000, 1500)* — Tempo in mm/min: zuerst leer fahren (Travel), dann zeichnen (Draw) - *enableKeyInputs* — Tastatur: C, H, Pfeile, … -- *enablePlotButtons* — Buttons Connect, Plot, Test, Abort +- *enablePlotButtons* — Buttons Connect, Test, Plot, Bounding box, Abort *plot* und *test* rufen dieselbe Zeichnung auf. *false* = G-code an den Plotter, *true* = nur Vorschau im Browser. @@ -145,7 +145,24 @@ async function drawPlot(testOnly) { Mehr Befehle: [lib/doc.md](https://git.collectivemedia.space/martin/p5-wallplotter/src/branch/main/lib/doc.md). -## 3.1 Plotter anschliessen +## 3. Testen + +Der Plotter bleibt aus. **Test** zeigt die Zeichnung nur im Browser — kein G-code, keine Motoren. + +1. Sketch in Chrome öffnen, z. B. [http://localhost:8080/sketches/mein-sketch/](http://localhost:8080/sketches/mein-sketch/) (oder *examples/zickzack/*). +2. Button **Test** oder Taste **T**. Die Gondel-Linie läuft als Vorschau über den Bildschirm. +3. Nochmals **Test** oder **T** zeichnet die Vorschau neu (kein Abort während des Tests). + +Nach jeder Änderung in *sketch.js*: in VS Code speichern, dann in Chrome **neu laden**, sonst siehst du die alte Version (Cache). + +- Hart neu laden: *Ctrl+Shift+R* (Windows/Linux) bzw. *Cmd+Shift+R* (macOS). +- Oder die Entwicklertools öffnen (*F12* bzw. *Cmd+Option+I*), oben **Netzwerk**, Häkchen **Cache deaktivieren**. Die Tools müssen **offen bleiben**. Dann reicht ein normales Neuladen (*F5* / *Cmd+R*). + +Danach wieder **Test** oder **T** — jetzt gilt der geänderte Sketch. + +## 4. Plotten + +## 4.1 Plotter anschliessen ### Hardware-Reihenfolge @@ -156,7 +173,7 @@ Mehr Befehle: [lib/doc.md](https://git.collectivemedia.space/martin/p5-wallplott 5. **Connect** klicken oder Taste **C**. 6. Im Dialog den Serial-Port der Steuerung wählen. -## 3.2 Home setzen (wichtig) +## 4.2 Home setzen (wichtig) Marlin kennt nach dem Einschalten **keine** absolute Wand-Position. Du sagst ihm, wo „jetzt“ der Ursprung (Blattmitte = Koordinate 0/0) ist. Die Pfeiltasten funktionieren erst nach dem ersten Home: @@ -171,7 +188,7 @@ Ohne Home bleibt Plot gesperrt. Diese Firmware hat keinen *EMERGENCY_PARSER*. Ein Stopp im Browser bricht nur das *weitere Senden* ab; die bereits laufende *G1*-Fahrt läuft weiter. Für einen harten Stopp: **Strom trennen**. -## 3.3 Bedienung +## 4.3 Bedienung ### Tasten, Buttons, HUD @@ -179,18 +196,21 @@ Shortcuts: - **C** — Verbinden / trennen (Port-Dialog) - **H** — Hier ist Home (*G92 X0 Y0*): zuerst an der Gondel, nach dem Joggen nochmals in der Blattmitte -- **T** — Test (Vorschau) -- **A** — Abort: Test auf Start zurück +- **T** — Test (Vorschau). Nochmals **T** zeichnet neu +- **B** — Bounding box: Gondel fährt Stift oben um die Ausdehnung der Zeichnung +- **A** — Abort: während eines Plots (laufendes *G1* fährt zu Ende), oder nach einem Reload den gespeicherten Plot verwerfen - **P** — Stift umschalten - **0** — Nach (0, 0) fahren (Stift oben) - **← → ↑ ↓** — Joggen, Stift oben - **Shift + Pfeile** — Joggen, Stift unten - **+** / **−** — Schrittweite 1–100 mm -- **S** — Plot stoppen / pausieren +- **S** — Plot pausieren / fortsetzen + +Nach **Test** erscheint **Bounding box**. **Plot** nur wenn verbunden. **Plot** schickt genau diese Vorschau an den Plotter — die Zeichnung wird nicht neu gewürfelt. **Bounding box** fährt Stift oben um das Rechteck der Zeichnung, damit du siehst, ob alles aufs Blatt passt. Während eines Plots wird **Plot** zu **Pause** / **Resume**, und **Abort** erscheint. Nach einem Reload mit gespeichertem Plot bleibt **Abort** sichtbar, falls du einen anderen Sketch geladen hast und den alten Pfad nicht fortsetzen willst. **Test** bleibt während des Tests **Test** (nochmals drücken = neu zeichnen). Das HUD (unten und als Overlay) zeigt Verbindung, Home, Position, Stift, Serial-Log. -## 4. Polargraph und Marlin +## 5. Polargraph und Marlin ### Der Plotter und seine Teile @@ -250,7 +270,7 @@ Befehlsliste: [gcode.md](https://git.collectivemedia.space/martin/p5-wallplotter Marlin ist keine Zeichen-App. Es weiss nichts von p5 oder deinem Sketch. Es bekommt nur Koordinaten und Servo-Winkel. Pfadplanung, Stiftlogik und die Vorschau liegen in *WallPlotter.js*. Die Firmware ist der Bewegungscontroller: parsen, in Riemenlängen umrechnen, Schritte erzeugen, *ok* sagen. -## 5. Fehlerbehebung +## 6. Fehlerbehebung - **Kein Port-Dialog, oder „Serial not supported“** — **Chrome**, nicht Firefox oder Safari. Seite über *http://localhost:…* öffnen, nicht als Datei. - **Port erscheint nicht** — Strom der Steuerung an? Daten-USB-Kabel? Linux: Gruppe *dialout* oder *uucp*, neu einloggen; im Terminal nach *ttyACM* oder *ttyUSB* schauen. Windows: Geräte-Manager → COM-Port. macOS: Tab schliessen und neu öffnen (Port oft blockiert). @@ -266,15 +286,16 @@ Marlin ist keine Zeichen-App. Es weiss nichts von p5 oder deinem Sketch. Es beko Serial-Log im HUD lesen: Marlin-Fehler stehen dort als Text, nicht als Browser-Alert. -## 6. Mini-Ablauf zum Merken +## 7. Mini-Ablauf zum Merken 1. ZIP herunterladen, in VS Code den Ordner *p5-wallplotter* öffnen. 2. Terminal in VS Code öffnen, *python3 -m http.server 8080* (Windows: *py -m http.server 8080*). 3. Chrome: [http://localhost:8080/examples/zickzack/](http://localhost:8080/examples/zickzack/). 4. Eigenen Ordner unter *sketches/* anlegen, *drawPlot* anpassen. -5. Plotter mit Strom und USB verbinden. -6. **C** → Port wählen. -7. **H** (aktuelle Position), zur Blattmitte joggen, nochmals **H**. -8. **Test**, dann **Plot**. +5. **Test** oder **T**. Nach Änderungen speichern, hart neu laden (*Ctrl+Shift+R*), nochmals **T**. +6. Plotter mit Strom und USB verbinden. +7. **C** → Port wählen. +8. **H** (aktuelle Position), zur Blattmitte joggen, nochmals **H**. +9. **Plot**. API-Referenz: [lib/doc.md](https://git.collectivemedia.space/martin/p5-wallplotter/src/branch/main/lib/doc.md). G-code: [gcode.md](https://git.collectivemedia.space/martin/p5-wallplotter/src/branch/main/gcode.md).