diff --git a/examples/zickzack/index.html b/examples/zickzack/index.html index 15ffd0a..a6f5a38 100644 --- a/examples/zickzack/index.html +++ b/examples/zickzack/index.html @@ -44,7 +44,7 @@ } - +
diff --git a/examples/zickzack/sketch.js b/examples/zickzack/sketch.js index 96a2308..3dfc2b9 100644 --- a/examples/zickzack/sketch.js +++ b/examples/zickzack/sketch.js @@ -6,7 +6,12 @@ function setup() { const canvas = createCanvas(windowWidth, windowHeight); canvas.parent('sketch-host'); - wp = new WallPlotter(); + wp = new WallPlotter({ + penUpGcode: 'M280 P0 S120', + penDownGcode: 'M280 P0 S30', + penUpDelayMs: 250, + penDownDelayMs: 150, + }); wp.setSpeed(10000, 10000); wp.setTimingMultiplier(1); // tune if machine is slower than speed estimate, the higher the slower wp.enableKeyInputs(); @@ -35,16 +40,16 @@ async function drawArt(testOnly) { wp.move(-area, -area); - wp.wait(1000 * 10); + //wp.wait(1000 * 10); - let step = area / 20; + let step = area / 4.001; for (let y = -area; y < area; y+=step){ for (let x = -area; x < area; x+=step){ - if(random(1) < 0.8){ - wp.move(x, y); + //if(random(1) < 0.8){ + wp.draw(x, y); wp.draw(x, y+step); - } + //} } } diff --git a/lib/README.md b/lib/README.md index d2959d8..1c4f874 100644 --- a/lib/README.md +++ b/lib/README.md @@ -26,8 +26,8 @@ All options are optional. | `penDownGcode` | `M280 P0 S50` | Command to lower pen | | `penUpDelayMs` | `150` | Pause after pen up, ms | | `penDownDelayMs` | `50` | Pause after pen down, ms | -| `machineXMin` / `machineXMax` | `-850` / `850` | Overlay X extents, mm | -| `machineYMin` / `machineYMax` | `-1000` / `1000` | Overlay Y extents, mm | +| `machineXMin` / `machineXMax` | `-830` / `830` | Overlay X extents, mm (idler spacing 1660) | +| `machineYMin` / `machineYMax` | `-1200` / `800` | Overlay Y; idlers at Y=800 (80 cm above work centre) | | `jogIntervalMs` | `150` | Repeat delay while holding arrows, ms | | `maxLogLines` | `8` | Serial lines shown on the HUD | | `previewMinDelayMs` | `30` | Floor for preview segment wait, ms | @@ -144,9 +144,9 @@ Sketch: `plot()` → `runPath(false)`, `test()` → `runPath(true)`. Returns estimated duration in **milliseconds** for the current path buffer, or for `commands` if given. Uses `setSpeed`, queued `speed()`, `wait()`, and `timingMultiplier`. The HUD shows elapsed / est / remain while plotting. -### `stopPath()` +### `stopPath()` / `stopPlot()` -Abort the current `runPath`. No arguments. +`stopPath()` aborts the current `runPath`. `stopPlot()` also sends Marlin `M410` (quick stop) during a live plot. Key **S** calls `stopPlot()` while plotting. --- @@ -156,7 +156,7 @@ All **async**. Coordinate moves on Plot require home (`H`) first. ### `connect()` -Web Serial port picker, enable steppers (`M17`), pen up, `M114`. +Web Serial port picker, enable steppers (`M17`), pen up, query polargraph area (`M665` → machine overlay bounds), `M114`. ### `disconnect()` @@ -171,9 +171,9 @@ Write one G-code line and wait for Marlin `ok`. - `options.waitOk` (boolean): wait for `ok`. Default `true`. - `options.timeoutMs` (number): `ok` timeout, default `300000`. -### `penUp()` / `penDown()` +### `penUp(force?)` / `penDown(force?)` -Send pen G-code and wait the pen delay. No-op if already in that state. +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. ### `moveTo(x, y)` diff --git a/lib/WallPlotter.js b/lib/WallPlotter.js index 9d8ddb8..09d97c3 100644 --- a/lib/WallPlotter.js +++ b/lib/WallPlotter.js @@ -9,6 +9,8 @@ class WallPlotter { static VERSION = 'wait-ok+M400'; static MAX_FEED_MM_MIN = 10000; static MIN_FEED_MM_MIN = 1; + /** Offline overlay size (mm) — 150×150 cm centred on origin */ + static PREVIEW_SIZE_MM = 1500; constructor(options = {}) { this.baudRate = options.baudRate ?? 115200; @@ -19,10 +21,11 @@ class WallPlotter { this.penDownGcode = options.penDownGcode ?? 'M280 P0 S50'; this.penUpDelayMs = options.penUpDelayMs ?? 150; this.penDownDelayMs = options.penDownDelayMs ?? 50; - this.machineXMin = options.machineXMin ?? -850; - this.machineXMax = options.machineXMax ?? 850; - this.machineYMin = options.machineYMin ?? -1000; - this.machineYMax = options.machineYMax ?? 1000; + this.machineXMin = options.machineXMin ?? -830; + this.machineXMax = options.machineXMax ?? 830; + this.machineYMin = options.machineYMin ?? -1200; + this.machineYMax = options.machineYMax ?? 800; + this.previewSizeMm = options.previewSizeMm ?? WallPlotter.PREVIEW_SIZE_MM; this.jogIntervalMs = options.jogIntervalMs ?? 150; this.maxLogLines = options.maxLogLines ?? 8; @@ -143,6 +146,21 @@ class WallPlotter { this._pathAbort = true; } + /** Abort path; if a live plot is running, also quick-stop Marlin (M410). */ + async stopPlot() { + if (!this.isPlotting) return; + this.stopPath(); + if (this.connected && !this._plotTestOnly) { + try { + await this.send('M410', { waitOk: false, allowWithoutHome: true }); + } catch (_) { + /* port may be busy */ + } + } + this.statusText = 'Stopped'; + this.onLog('plot stopped'); + } + _restartPathAfterAbort(mode) { this._pendingAfterAbort = mode; this._pathAbort = true; @@ -526,7 +544,8 @@ class WallPlotter { this.onStatusChange('connected'); await this.send('M17'); - await this.penUp(); + await this.penUp(true); + await this.send('M665'); // polargraph draw area → updates machine bounds await this.send('M114'); } @@ -632,15 +651,15 @@ class WallPlotter { return false; } - async penUp() { - if (!this.isPenDown) return; + async penUp(force = false) { + if (!force && !this.isPenDown) return; await this.send(this.penUpGcode); await this._delay(this.penUpDelayMs); this.isPenDown = false; } - async penDown() { - if (this.isPenDown) return; + async penDown(force = false) { + if (!force && this.isPenDown) return; await this.send(this.penDownGcode); await this._delay(this.penDownDelayMs); this.isPenDown = true; @@ -715,6 +734,11 @@ class WallPlotter { return false; } + if (k === 's') { + if (this.isPlotting) await this.stopPlot(); + return false; + } + if (!this._keysEnabled || !this.connected) return; if (k === 'h') { @@ -728,8 +752,8 @@ class WallPlotter { } if (k === 'p') { - if (this.isPenDown) await this.penUp(); - else await this.penDown(); + if (this.isPenDown) await this.penUp(true); + else await this.penDown(true); return false; } @@ -806,18 +830,40 @@ class WallPlotter { } } + _getDisplayBounds() { + if (this.connected) { + return { + xMin: this.machineXMin, + xMax: this.machineXMax, + yMin: this.machineYMin, + yMax: this.machineYMax, + preview: false, + }; + } + const half = this.previewSizeMm / 2; + return { + xMin: -half, + xMax: half, + yMin: -half, + yMax: half, + preview: true, + }; + } + _getPlotLayout() { const margin = 60; const size = min(width, height) - margin * 2; - const worldW = this.machineXMax - this.machineXMin; - const worldH = this.machineYMax - this.machineYMin; + const b = this._getDisplayBounds(); + const worldW = Math.max(1, b.xMax - b.xMin); + const worldH = Math.max(1, b.yMax - b.yMin); + // Fit full display rectangle; centre the view on work origin (0,0) const scale = min(size / worldW, size / worldH); return { cx: width / 2, cy: height / 2 - 20, scale, - worldCx: (this.machineXMin + this.machineXMax) / 2, - worldCy: (this.machineYMin + this.machineYMax) / 2, + worldCx: 0, + worldCy: 0, }; } @@ -856,22 +902,54 @@ class WallPlotter { _drawPlotArea() { const layout = this._getPlotLayout(); + const bounds = this._getDisplayBounds(); push(); translate(layout.cx, layout.cy); - stroke(50); - strokeWeight(1); - const tl = this._worldToScreen(this.machineXMin, this.machineYMax, layout); - const br = this._worldToScreen(this.machineXMax, this.machineYMin, layout); + const tl = this._worldToScreen(bounds.xMin, bounds.yMax, layout); + const br = this._worldToScreen(bounds.xMax, bounds.yMin, layout); + const w = br.x - tl.x; + const h = br.y - tl.y; + noFill(); rectMode(CORNER); - rect(tl.x, tl.y, br.x - tl.x, br.y - tl.y); + strokeWeight(1); + if (bounds.preview) { + stroke(90, 90, 110); + drawingContext.setLineDash([8, 6]); + } else { + stroke(70); + drawingContext.setLineDash([]); + } + rect(tl.x, tl.y, w, h); + drawingContext.setLineDash([]); stroke(60); line(tl.x, 0, br.x, 0); line(0, tl.y, 0, br.y); + // Size labels in cm along bottom and right edges + const widthCm = (bounds.xMax - bounds.xMin) / 10; + const heightCm = (bounds.yMax - bounds.yMin) / 10; + const widthLabel = Number.isInteger(widthCm) + ? `${widthCm} cm` + : `${widthCm.toFixed(1)} cm`; + const heightLabel = Number.isInteger(heightCm) + ? `${heightCm} cm` + : `${heightCm.toFixed(1)} cm`; + noStroke(); + fill(bounds.preview ? 140 : 160); + textFont('monospace', 12); + textAlign(CENTER, TOP); + text(widthLabel, (tl.x + br.x) / 2, br.y + 8); + push(); + translate(br.x + 8, (tl.y + br.y) / 2); + rotate(HALF_PI); + textAlign(CENTER, BOTTOM); + text(heightLabel, 0, 0); + pop(); + const rectBounds = this._getEffectiveDrawBounds(); if (rectBounds) { const dtl = this._worldToScreen(rectBounds.left, rectBounds.top, layout); @@ -955,43 +1033,53 @@ class WallPlotter { 92 ); text(`home ${this.homeSet ? 'set' : 'not set'}`, 16, 112); + if (this.connected) { + text( + `machine X ${this.machineXMin.toFixed(0)}…${this.machineXMax.toFixed(0)} Y ${this.machineYMin.toFixed(0)}…${this.machineYMax.toFixed(0)} mm`, + 16, + 132 + ); + } else { + const cm = this.previewSizeMm / 10; + text(`preview ${cm} × ${cm} cm (connect for machine bounds)`, 16, 132); + } if (this.isPlotting) { fill(180, 220, 255); text( `plotting ${this.plotProgress}/${this.previewPath.length}`, 16, - 132 + 152 ); const elapsedMs = performance.now() - this._plotStartedAt; const remainMs = Math.max(0, this._plotEstimateMs - elapsedMs); text( `elapsed ${this._formatDuration(elapsedMs)} est ${this._formatDuration(this._plotEstimateMs)} remain ${this._formatDuration(remainMs)}`, 16, - 152 + 172 ); text( `seg wait ${this._lastSegmentMs.toFixed(0)} ms ×${this.timingMultiplier}`, 16, - 172 + 192 ); fill(220); } if (this.connected && !this.homeSet && this._homeWarning) { fill(255, 180, 80); - text(`! ${this._homeWarning}`, 16, this.isPlotting ? 192 : 132); + text(`! ${this._homeWarning}`, 16, this.isPlotting ? 212 : 152); fill(220); } text( `bounds L ${this._formatBound(this.drawBounds.left)} R ${this._formatBound(this.drawBounds.right)} T ${this._formatBound(this.drawBounds.top)} B ${this._formatBound(this.drawBounds.bottom)}`, 16, - this.isPlotting ? 212 : 152 + this.isPlotting ? 232 : 172 ); const size = this._getDrawSize(); - const areaY = this.isPlotting ? 232 : 172; + const areaY = this.isPlotting ? 252 : 192; if (size) { text(`area ${size.w.toFixed(1)} × ${size.h.toFixed(1)} mm`, 16, areaY); } else if ( @@ -1012,7 +1100,8 @@ class WallPlotter { '+ / - step size', 'H set current pos as home (G92) — required first', '0 go to origin (0, 0)', - 'P toggle pen', + 'P toggle pen (test up/down angles)', + 'S stop plot / test', 'C connect / disconnect', ]; for (let i = 0; i < help.length; i++) { @@ -1074,6 +1163,7 @@ class WallPlotter { const next = this._okResolvers.shift(); if (next) next.resolve(); } + this._parseMachineBounds(line); if (line.startsWith('X:')) { const match = line.match(/X:([-\d.]+)\s+Y:([-\d.]+)/); if (match) { @@ -1082,4 +1172,21 @@ class WallPlotter { } } } + + /** Parse Marlin polargraph M665 report: M665 S… L… R… T… B… H… */ + _parseMachineBounds(line) { + if (!/\bM665\b/i.test(line)) return; + const l = line.match(/\bL(-?[\d.]+)/i); + const r = line.match(/\bR(-?[\d.]+)/i); + const t = line.match(/\bT(-?[\d.]+)/i); + const b = line.match(/\bB(-?[\d.]+)/i); + if (!l || !r || !t || !b) return; + this.machineXMin = parseFloat(l[1]); + this.machineXMax = parseFloat(r[1]); + this.machineYMax = parseFloat(t[1]); + this.machineYMin = parseFloat(b[1]); + this.onLog( + `machine bounds X ${this.machineXMin}…${this.machineXMax} Y ${this.machineYMin}…${this.machineYMax}` + ); + } }