From ce288bd35d38584f7296f6e849e7e7cd87b20b00 Mon Sep 17 00:00:00 2001 From: Martin Fuchs Date: Wed, 9 Sep 2026 12:06:56 +0200 Subject: [PATCH] live plot example and screenToPlot helper --- .gitignore | 1 + README.md | 1 + examples/live/index.html | 52 ++++++++++++++++++++++++++ examples/live/sketch.js | 79 ++++++++++++++++++++++++++++++++++++++++ lib/WallPlotter.js | 55 ++++++++++++++++++++++++++++ lib/doc.md | 12 +++++- 6 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 examples/live/index.html create mode 100644 examples/live/sketch.js diff --git a/.gitignore b/.gitignore index 9751508..8b41f66 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Thumbs.db *.log .idea/ .vscode/ +unterricht diff --git a/README.md b/README.md index 21be2c1..9e2e2da 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ p5.js library for a Marlin polargraph (wall plotter) over Web Serial, plus a zig ``` p5-wallplotter/ ├── tutorial.md # setup + plotter + G-code +├── unterricht/ # three workshop assignments ├── hardware.md # motors, board, geometry ├── gcode.md # G-code used by this setup ├── plotter.svg # devices + signal path diff --git a/examples/live/index.html b/examples/live/index.html new file mode 100644 index 0000000..6f90a94 --- /dev/null +++ b/examples/live/index.html @@ -0,0 +1,52 @@ + + + + + + Wall Plotter — mouse + + + + + + +

Wall Plotter — mouse

+
+ + diff --git a/examples/live/sketch.js b/examples/live/sketch.js new file mode 100644 index 0000000..92bd25f --- /dev/null +++ b/examples/live/sketch.js @@ -0,0 +1,79 @@ +// Live-Maus: Jeder Klick schickt eine Zielposition. Der Plotter +// fährt erst dorthin (moveTo wartet auf Bewegungsende). Der nächste Klick wird +// erst danach ausgeführt. +// waitUntilArrived: true = Klicks während der Fahrt ignorieren. +// false = Klicks dürfen sich stapeln (send-Kette in der Library). + +let wp; +let live = false; +let busy = false; +const waitUntilArrived = true; + +function setup() { + const canvas = createCanvas(windowWidth, windowHeight); + canvas.parent('sketch-host'); + + wp = new WallPlotter({ + penUpGcode: 'M280 P0 S120', + penDownGcode: 'M280 P0 S30', + penUpDelayMs: 250, + penDownDelayMs: 150, + }); + wp.setSpeed(3000, 3000); + wp.enableKeyInputs(); + wp.enablePlotButtons(); +} + +// Jeden Frame: Overlay zeichnen und die Maus umranden. +function draw() { + background(28, 28, 32); + wp.showInfos(); + + noFill(); + stroke(live ? color(230, 50, 50) : color(220)); + strokeWeight(2); + circle(mouseX, mouseY, 16); + + if (live) { + noStroke(); + fill(230, 50, 50); + textSize(12); + textAlign(LEFT, CENTER); + text(busy ? 'LIVE BUSY' : 'LIVE', mouseX + 14, mouseY); + } +} + +// Fenstergrösse geändert: Canvas anpassen. +function windowResized() { + resizeCanvas(windowWidth, windowHeight); +} + +// Taste L: Live-Modus ein- oder ausschalten. +function keyPressed() { + if (key !== 'l' && key !== 'L') return; + live = !live; +} + +// Klick (nur im Live-Modus): Gondel fährt zu dieser Millimeter-Position, Stift oben.n +async function mousePressed() { + if (!live || !wp.connected || !wp.homeSet) return; + if (waitUntilArrived && busy) return; + + busy = true; + try { + // Maus-Pixel auf dem Overlay -> Plotter-Koordinaten + // Die Library nutzt den aktuellen Overlay-Layout-Massstab und begrenzt + // standardmässig auf die Maschinen-Grenzen (inkl. korrekter Y-Ausrichtung). + const p = wp.screenToPlot(mouseX, mouseY); + await wp.moveTo(p.x, p.y); + + console.log('moveTo', p.x, p.y); + } finally { + busy = false; + } +} + +// Von den Plot-Buttons erwartet; hier ungenutzt (kein gepufferter Pfad). +async function plot() {} + +async function test() {} diff --git a/lib/WallPlotter.js b/lib/WallPlotter.js index 2ef8c3b..74ce8fa 100644 --- a/lib/WallPlotter.js +++ b/lib/WallPlotter.js @@ -320,6 +320,38 @@ class WallPlotter { this.timingMultiplier = Math.max(0.1, Number(mult) || 1); } + /** + * Convert canvas pixel coordinates to plot coordinates in mm. + * By default this uses machine bounds so live-click targeting stays stable. + * @param {number} px + * @param {number} py + * @param {{clamp?: boolean, useDisplayBounds?: boolean}} [options] + */ + screenToPlot(px, py, options = {}) { + const layout = this._getPlotLayout(); + const bounds = + options.useDisplayBounds === true + ? this._getDisplayBounds() + : { + xMin: this.machineXMin, + xMax: this.machineXMax, + yMin: this.machineYMin, + yMax: this.machineYMax, + }; + + const scale = Math.max(layout.scale || 0, 1e-9); + const worldX = (px - layout.cx) / scale + layout.worldCx; + const worldY = -(py - layout.cy) / scale + layout.worldCy; + const clamp = options.clamp !== false; + + if (!clamp) return { x: worldX, y: worldY }; + + return { + x: Math.min(bounds.xMax, Math.max(bounds.xMin, worldX)), + y: Math.min(bounds.yMax, Math.max(bounds.yMin, worldY)), + }; + } + // =========================================================================== // Public API — path buffer (queue only; nothing is sent until runPath) // =========================================================================== @@ -929,19 +961,42 @@ class WallPlotter { /** Pen up and travel to (x, y) mm immediately. Requires home. */ async moveTo(x, y) { if (!this._requireHome()) return; + const startX = this.x; + const startY = this.y; await this.penUp(); await this.send(`G1 X${this._fmt(x)} Y${this._fmt(y)} F${this.feedRateTravel}`); this.x = x; this.y = y; + await this._waitForMotionComplete( + Math.hypot((x ?? 0) - (startX ?? 0), (y ?? 0) - (startY ?? 0)), + this.feedRateTravel + ); } /** Pen down and draw to (x, y) mm immediately. Requires home. */ async drawTo(x, y) { if (!this._requireHome()) return; + const startX = this.x; + const startY = this.y; await this.penDown(); await this.send(`G1 X${this._fmt(x)} Y${this._fmt(y)} F${this.feedRateDraw}`); this.x = x; this.y = y; + await this._waitForMotionComplete( + Math.hypot((x ?? 0) - (startX ?? 0), (y ?? 0) - (startY ?? 0)), + this.feedRateDraw + ); + } + + async _waitForMotionComplete(distanceMm, feedRateMmPerMin) { + const distance = Number.isFinite(distanceMm) ? Math.max(0, distanceMm) : 0; + const speedMmPerS = Math.max(1, Number(feedRateMmPerMin) || 1) / 60; + const etaMs = Math.round((distance / speedMmPerS) * 1000); + await this.send('M400', { + timeoutMs: Math.max(WallPlotter.OK_TIMEOUT_MS, etaMs + 15000), + // M400 legitimately blocks until mechanics are done moving. + waitLogAfterMs: Math.max(WallPlotter.OK_WAIT_LOG_MS, etaMs + 1500), + }); } /** Travel to (0, 0). */ diff --git a/lib/doc.md b/lib/doc.md index 09e6ddb..7cad72e 100644 --- a/lib/doc.md +++ b/lib/doc.md @@ -184,13 +184,21 @@ Write one G-code line and wait for Marlin `ok`. Send pen G-code and wait the pen delay. By default, no-op if already in that state. Pass `true` to always send (used by `P` and on connect) so you can test the configured angles. +### `screenToPlot(px, py, options?)` + +Convert canvas pixel coordinates to plot coordinates in mm. + +- `px`, `py` (number): screen/canvas pixel coordinates. +- `options.clamp` (boolean, default `true`): clamp result to bounds. +- `options.useDisplayBounds` (boolean, default `false`): use current display bounds (preview when disconnected) instead of machine bounds. + ### `moveTo(x, y)` -Pen up, travel to `(x, y)` mm immediately (not queued). Requires home. +Pen up, travel to `(x, y)` mm immediately (not queued), then wait for motion completion (`M400`). Requires home. ### `drawTo(x, y)` -Pen down, draw to `(x, y)` mm immediately. Requires home. +Pen down, draw to `(x, y)` mm immediately, then wait for motion completion (`M400`). Requires home. ### `goToZero()`