diff --git a/lib/WallPlotter.js b/lib/WallPlotter.js index 74ce8fa..b18b0eb 100644 --- a/lib/WallPlotter.js +++ b/lib/WallPlotter.js @@ -18,7 +18,7 @@ class WallPlotter { // Static constants // --------------------------------------------------------------------------- - static VERSION = '2.3.24'; + static VERSION = '2.4.0'; 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 */ @@ -91,6 +91,7 @@ class WallPlotter { this._link = 'disconnected'; this.homeSet = false; this.isPenDown = false; + this.penPressure = 0; this.x = 0; this.y = 0; this.statusText = 'Disconnected'; @@ -363,15 +364,29 @@ class WallPlotter { return this; } - /** Queue pen-up travel to (x, y) mm. */ - move(x, y) { - this._path.push({ type: 'move', x, y }); + /** + * Queue pen-up travel to (x, y) mm. + * @param {number} x + * @param {number} y + * @param {number} [pressure] 0–100, mapped from penUpGcode S to penDownGcode S. Default 0 (up). + */ + move(x, y, pressure) { + const cmd = { type: 'move', x, y }; + if (pressure != null) cmd.pressure = this._clampPressure(pressure); + this._path.push(cmd); return this; } - /** Queue pen-down stroke to (x, y) mm. */ - draw(x, y) { - this._path.push({ type: 'draw', x, y }); + /** + * Queue pen-down stroke to (x, y) mm. + * @param {number} x + * @param {number} y + * @param {number} [pressure] 0–100, mapped from penUpGcode S to penDownGcode S. Default 100 (down). + */ + draw(x, y, pressure) { + const cmd = { type: 'draw', x, y }; + if (pressure != null) cmd.pressure = this._clampPressure(pressure); + this._path.push(cmd); return this; } @@ -453,6 +468,7 @@ class WallPlotter { this.x = this._pathStartX; this.y = this._pathStartY; this.isPenDown = false; + this.penPressure = 0; this.plotProgress = 0; this.previewPath = []; this._path = []; @@ -937,33 +953,42 @@ class WallPlotter { // =========================================================================== /** - * Lift pen. Pass true to always send (used by P and connect). + * Lift pen (pressure 0). Pass true to always send (used by P and connect). * @param {boolean} [force=false] */ async penUp(force = false) { - if (!force && !this.isPenDown) return; - await this.send(this.penUpGcode); - await this._delay(this.penUpDelayMs); - this.isPenDown = false; + await this._applyPenPressure(0, { force, wait: true }); } /** - * Lower pen. Pass true to always send. + * Lower pen (pressure 100). Pass true to always send. * @param {boolean} [force=false] */ async penDown(force = false) { - if (!force && this.isPenDown) return; - await this.send(this.penDownGcode); - await this._delay(this.penDownDelayMs); - this.isPenDown = true; + await this._applyPenPressure(100, { force, wait: true }); } - /** Pen up and travel to (x, y) mm immediately. Requires home. */ - async moveTo(x, y) { + /** + * Set servo pressure immediately (0 = penUpGcode S, 100 = penDownGcode S). + * @param {number} pressure + */ + async setPressure(pressure) { + await this._applyPenPressure(this._clampPressure(pressure), { force: true, wait: true }); + } + + /** + * Pen up (or optional pressure) and travel to (x, y) mm immediately. Requires home. + * @param {number} x + * @param {number} y + * @param {number} [pressure] 0–100. Default 0 (up). + */ + async moveTo(x, y, pressure) { if (!this._requireHome()) return; const startX = this.x; const startY = this.y; - await this.penUp(); + await this._applyPenPressure(pressure != null ? this._clampPressure(pressure) : 0, { + wait: true, + }); await this.send(`G1 X${this._fmt(x)} Y${this._fmt(y)} F${this.feedRateTravel}`); this.x = x; this.y = y; @@ -973,12 +998,19 @@ class WallPlotter { ); } - /** Pen down and draw to (x, y) mm immediately. Requires home. */ - async drawTo(x, y) { + /** + * Pen down (or optional pressure) and draw to (x, y) mm immediately. Requires home. + * @param {number} x + * @param {number} y + * @param {number} [pressure] 0–100. Default 100 (down). + */ + async drawTo(x, y, pressure) { if (!this._requireHome()) return; const startX = this.x; const startY = this.y; - await this.penDown(); + await this._applyPenPressure(pressure != null ? this._clampPressure(pressure) : 100, { + wait: true, + }); await this.send(`G1 X${this._fmt(x)} Y${this._fmt(y)} F${this.feedRateDraw}`); this.x = x; this.y = y; @@ -1097,6 +1129,63 @@ class WallPlotter { ); } + _clampPressure(value) { + const n = Number(value); + if (!Number.isFinite(n)) return 0; + return Math.round(Math.min(100, Math.max(0, n))); + } + + /** Effective 0–100 pressure for a path command (move default 0, draw default 100). */ + _cmdPressure(cmd) { + if (cmd.pressure != null) return this._clampPressure(cmd.pressure); + return cmd.type === 'draw' ? 100 : 0; + } + + _parsePenS(gcode) { + const m = String(gcode ?? '').match(/S(-?\d+(?:\.\d+)?)/i); + return m ? Number(m[1]) : null; + } + + _penSForPressure(pressure) { + const t = this._clampPressure(pressure) / 100; + const upS = this._parsePenS(this.penUpGcode) ?? 90; + const downS = this._parsePenS(this.penDownGcode) ?? 50; + return upS + (downS - upS) * t; + } + + _penGcodeForPressure(pressure) { + const p = this._clampPressure(pressure); + if (p <= 0) return this.penUpGcode; + if (p >= 100) return this.penDownGcode; + const s = Math.round(this._penSForPressure(p)); + const template = this.penDownGcode || this.penUpGcode || 'M280 P0 S0'; + if (/S-?\d+(?:\.\d+)?/i.test(template)) { + return template.replace(/S-?\d+(?:\.\d+)?/i, `S${s}`); + } + return `${template} S${s}`.trim(); + } + + /** + * Send interpolated servo G-code. 0 = penUpGcode S, 100 = penDownGcode S. + * @param {number} pressure + * @param {{force?: boolean, wait?: boolean}} [options] + */ + async _applyPenPressure(pressure, options = {}) { + const force = options.force === true; + const wait = options.wait !== false; + const next = this._clampPressure(pressure); + if (!force && this.penPressure === next) { + this.isPenDown = next > 0; + return; + } + await this.send(this._penGcodeForPressure(next)); + if (wait) { + await this._delay(next === 0 ? this.penUpDelayMs : this.penDownDelayMs); + } + this.penPressure = next; + this.isPenDown = next > 0; + } + // =========================================================================== // Internals — path execution & timing // =========================================================================== @@ -1227,6 +1316,7 @@ class WallPlotter { } } else { this.isPenDown = false; + this.penPressure = 0; } this._plotTestOnly = false; this._pathLoopActive = false; @@ -1245,9 +1335,11 @@ class WallPlotter { const dist = Math.hypot(cmd.x - fromX, cmd.y - fromY); const rate = isMove ? this.feedRateTravel : this.feedRateDraw; const motionMs = dist > 0 && rate > 0 ? (dist / rate) * 60000 : 0; + const nextPressure = this._cmdPressure(cmd); let penMs = 0; - if (isMove && this.isPenDown) penMs = this.penUpDelayMs; - if (!isMove && !this.isPenDown) penMs = this.penDownDelayMs; + if (nextPressure !== this.penPressure) { + penMs = nextPressure === 0 ? this.penUpDelayMs : this.penDownDelayMs; + } return Math.max(motionMs + penMs, this.previewMinDelayMs) * this.timingMultiplier; } @@ -1255,6 +1347,7 @@ class WallPlotter { const savedTravel = this.feedRateTravel; const savedDraw = this.feedRateDraw; const savedPen = this.isPenDown; + const savedPressure = this.penPressure; let px = fromX; let py = fromY; let total = 0; @@ -1272,7 +1365,8 @@ class WallPlotter { } if (cmd.type !== 'move' && cmd.type !== 'draw') continue; total += this._segmentDurationMs(px, py, cmd); - this.isPenDown = cmd.type === 'draw'; + this.penPressure = this._cmdPressure(cmd); + this.isPenDown = this.penPressure > 0; px = cmd.x; py = cmd.y; } @@ -1280,6 +1374,7 @@ class WallPlotter { this.feedRateTravel = savedTravel; this.feedRateDraw = savedDraw; this.isPenDown = savedPen; + this.penPressure = savedPressure; } return total; } @@ -1304,7 +1399,10 @@ class WallPlotter { } async _animatePosition(fromX, fromY, cmd, duration) { - if (this._plotTestOnly) this.isPenDown = cmd.type === 'draw'; + if (this._plotTestOnly) { + this.penPressure = this._cmdPressure(cmd); + this.isPenDown = this.penPressure > 0; + } const waitMs = Math.max(duration, 1); let elapsed = 0; let last = performance.now(); @@ -1360,24 +1458,10 @@ class WallPlotter { async _sendSegment(cmd) { if (!this._requireHome()) return; - if (cmd.type === 'move') { - if (this.isPenDown) { - await this.send(this.penUpGcode); - this.isPenDown = false; - } - // G1 not G0: Marlin G0_FEEDRATE overrides F unless VARIABLE_G0_FEEDRATE is on - await this.send( - `G1 X${this._fmt(cmd.x)} Y${this._fmt(cmd.y)} F${this.feedRateTravel}` - ); - } else { - if (!this.isPenDown) { - await this.send(this.penDownGcode); - this.isPenDown = true; - } - await this.send( - `G1 X${this._fmt(cmd.x)} Y${this._fmt(cmd.y)} F${this.feedRateDraw}` - ); - } + await this._applyPenPressure(this._cmdPressure(cmd), { wait: false }); + const feed = cmd.type === 'move' ? this.feedRateTravel : this.feedRateDraw; + // G1 not G0: Marlin G0_FEEDRATE overrides F unless VARIABLE_G0_FEEDRATE is on + await this.send(`G1 X${this._fmt(cmd.x)} Y${this._fmt(cmd.y)} F${feed}`); } async _sendSegmentAndWait(cmd) { @@ -2189,8 +2273,9 @@ class WallPlotter { if (px !== null && py !== null) { if (cmd.type === 'draw') { + const pressure = this._cmdPressure(cmd); stroke(255, 200, 60, isFuture ? 60 : 255); - strokeWeight(2); + strokeWeight(0.6 + (pressure / 100) * 1.8); line(px, py, p.x, p.y); } else { stroke(100, 100, 120, isFuture ? 60 : 200); @@ -2239,7 +2324,11 @@ class WallPlotter { 16, 52 ); - text(`pen ${this.isPenDown ? 'down (drawing)' : 'up (travel)'}`, 16, 72); + text( + `pen ${this.isPenDown ? 'down (drawing)' : 'up (travel)'} ${this.penPressure}%`, + 16, + 72 + ); text( `speed travel ${this.feedRateTravel} draw ${this.feedRateDraw} mm/min ×${this.timingMultiplier}`, 16, diff --git a/lib/doc.md b/lib/doc.md index 7cad72e..4cb9eb4 100644 --- a/lib/doc.md +++ b/lib/doc.md @@ -105,14 +105,26 @@ Scales **Test** preview waits only. Plot is paced by Marlin (`ok` + `M400`). Clear the path buffer. Returns `this`. -### `move(x, y)` +### `move(x, y, pressure?)` Queue pen-up travel to `(x, y)` mm. Returns `this`. -### `draw(x, y)` +- `pressure` (number, optional): **0–100**. Mapped linearly onto the `S` values of `penUpGcode` (0) and `penDownGcode` (100). Default **0** (pen fully up). + +### `draw(x, y, pressure?)` Queue pen-down stroke to `(x, y)` mm. Returns `this`. +- `pressure` (number, optional): **0–100**, same mapping as `move`. Default **100** (pen fully down). Use values in between for lighter contact. + +```javascript +wp.startPath(); +wp.move(-200, -200); +wp.draw(200, -200, 40); // light +wp.draw(200, 200, 100); // full down +await wp.runPath(false); +``` + ### `wait(ms)` Queue a pause. `ms` is milliseconds. On Plot, sends `G4 P`. Returns `this`. @@ -182,7 +194,11 @@ Write one G-code line and wait for Marlin `ok`. ### `penUp(force?)` / `penDown(force?)` -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. +Send pen G-code and wait the pen delay. By default, no-op if already at that pressure. Pass `true` to always send (used by `P` and on connect) so you can test the configured angles. `penUp` is pressure **0**, `penDown` is pressure **100**. + +### `setPressure(pressure)` + +Set servo pressure immediately (**0–100**). Same mapping as `move`/`draw`: **0** = `penUpGcode` `S`, **100** = `penDownGcode` `S`. Waits the pen delay. ### `screenToPlot(px, py, options?)` @@ -192,13 +208,17 @@ Convert canvas pixel coordinates to plot coordinates in mm. - `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)` +### `moveTo(x, y, pressure?)` -Pen up, travel to `(x, y)` mm immediately (not queued), then wait for motion completion (`M400`). Requires home. +Pen up (or optional pressure), travel to `(x, y)` mm immediately (not queued), then wait for motion completion (`M400`). Requires home. -### `drawTo(x, y)` +- `pressure` (number, optional): **0–100**. Default **0**. -Pen down, draw to `(x, y)` mm immediately, then wait for motion completion (`M400`). Requires home. +### `drawTo(x, y, pressure?)` + +Pen down (or optional pressure), draw to `(x, y)` mm immediately, then wait for motion completion (`M400`). Requires home. + +- `pressure` (number, optional): **0–100**. Default **100**. ### `goToZero()` @@ -224,7 +244,8 @@ Relative move in mm. | `connected` | Serial open | | `homeSet` | Home was set with `H` / `setHomeHere()` | | `x`, `y` | Current position, mm | -| `isPenDown` | Pen state | +| `isPenDown` | Pen is below fully-up (pressure > 0) | +| `penPressure` | Last servo pressure, **0–100** | | `isPlotting` | `runPath` in progress | | `isPaused` | `runPath` is paused | | `feedRateTravel` / `feedRateDraw` | Current speeds, mm/min |