pen pressure

This commit is contained in:
Martin Fuchs
2026-09-10 10:39:01 +02:00
parent ce288bd35d
commit dc7211ad93
2 changed files with 165 additions and 55 deletions
+136 -47
View File
@@ -18,7 +18,7 @@ class WallPlotter {
// Static constants // Static constants
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static VERSION = '2.3.24'; static VERSION = '2.4.0';
static MIN_FEED_MM_MIN = 1; static MIN_FEED_MM_MIN = 1;
static MAX_FEED_MM_MIN = 10000; static MAX_FEED_MM_MIN = 10000;
/** Offline overlay size when not connected (mm) — 150×150 cm centred on origin */ /** Offline overlay size when not connected (mm) — 150×150 cm centred on origin */
@@ -91,6 +91,7 @@ class WallPlotter {
this._link = 'disconnected'; this._link = 'disconnected';
this.homeSet = false; this.homeSet = false;
this.isPenDown = false; this.isPenDown = false;
this.penPressure = 0;
this.x = 0; this.x = 0;
this.y = 0; this.y = 0;
this.statusText = 'Disconnected'; this.statusText = 'Disconnected';
@@ -363,15 +364,29 @@ class WallPlotter {
return this; return this;
} }
/** Queue pen-up travel to (x, y) mm. */ /**
move(x, y) { * Queue pen-up travel to (x, y) mm.
this._path.push({ type: 'move', x, y }); * @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; return this;
} }
/** Queue pen-down stroke to (x, y) mm. */ /**
draw(x, y) { * Queue pen-down stroke to (x, y) mm.
this._path.push({ type: 'draw', x, y }); * @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; return this;
} }
@@ -453,6 +468,7 @@ class WallPlotter {
this.x = this._pathStartX; this.x = this._pathStartX;
this.y = this._pathStartY; this.y = this._pathStartY;
this.isPenDown = false; this.isPenDown = false;
this.penPressure = 0;
this.plotProgress = 0; this.plotProgress = 0;
this.previewPath = []; this.previewPath = [];
this._path = []; 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] * @param {boolean} [force=false]
*/ */
async penUp(force = false) { async penUp(force = false) {
if (!force && !this.isPenDown) return; await this._applyPenPressure(0, { force, wait: true });
await this.send(this.penUpGcode);
await this._delay(this.penUpDelayMs);
this.isPenDown = false;
} }
/** /**
* Lower pen. Pass true to always send. * Lower pen (pressure 100). Pass true to always send.
* @param {boolean} [force=false] * @param {boolean} [force=false]
*/ */
async penDown(force = false) { async penDown(force = false) {
if (!force && this.isPenDown) return; await this._applyPenPressure(100, { force, wait: true });
await this.send(this.penDownGcode);
await this._delay(this.penDownDelayMs);
this.isPenDown = 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; if (!this._requireHome()) return;
const startX = this.x; const startX = this.x;
const startY = this.y; 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}`); await this.send(`G1 X${this._fmt(x)} Y${this._fmt(y)} F${this.feedRateTravel}`);
this.x = x; this.x = x;
this.y = y; 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; if (!this._requireHome()) return;
const startX = this.x; const startX = this.x;
const startY = this.y; 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}`); await this.send(`G1 X${this._fmt(x)} Y${this._fmt(y)} F${this.feedRateDraw}`);
this.x = x; this.x = x;
this.y = y; 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 // Internals — path execution & timing
// =========================================================================== // ===========================================================================
@@ -1227,6 +1316,7 @@ class WallPlotter {
} }
} else { } else {
this.isPenDown = false; this.isPenDown = false;
this.penPressure = 0;
} }
this._plotTestOnly = false; this._plotTestOnly = false;
this._pathLoopActive = false; this._pathLoopActive = false;
@@ -1245,9 +1335,11 @@ class WallPlotter {
const dist = Math.hypot(cmd.x - fromX, cmd.y - fromY); const dist = Math.hypot(cmd.x - fromX, cmd.y - fromY);
const rate = isMove ? this.feedRateTravel : this.feedRateDraw; const rate = isMove ? this.feedRateTravel : this.feedRateDraw;
const motionMs = dist > 0 && rate > 0 ? (dist / rate) * 60000 : 0; const motionMs = dist > 0 && rate > 0 ? (dist / rate) * 60000 : 0;
const nextPressure = this._cmdPressure(cmd);
let penMs = 0; let penMs = 0;
if (isMove && this.isPenDown) penMs = this.penUpDelayMs; if (nextPressure !== this.penPressure) {
if (!isMove && !this.isPenDown) penMs = this.penDownDelayMs; penMs = nextPressure === 0 ? this.penUpDelayMs : this.penDownDelayMs;
}
return Math.max(motionMs + penMs, this.previewMinDelayMs) * this.timingMultiplier; return Math.max(motionMs + penMs, this.previewMinDelayMs) * this.timingMultiplier;
} }
@@ -1255,6 +1347,7 @@ class WallPlotter {
const savedTravel = this.feedRateTravel; const savedTravel = this.feedRateTravel;
const savedDraw = this.feedRateDraw; const savedDraw = this.feedRateDraw;
const savedPen = this.isPenDown; const savedPen = this.isPenDown;
const savedPressure = this.penPressure;
let px = fromX; let px = fromX;
let py = fromY; let py = fromY;
let total = 0; let total = 0;
@@ -1272,7 +1365,8 @@ class WallPlotter {
} }
if (cmd.type !== 'move' && cmd.type !== 'draw') continue; if (cmd.type !== 'move' && cmd.type !== 'draw') continue;
total += this._segmentDurationMs(px, py, cmd); total += this._segmentDurationMs(px, py, cmd);
this.isPenDown = cmd.type === 'draw'; this.penPressure = this._cmdPressure(cmd);
this.isPenDown = this.penPressure > 0;
px = cmd.x; px = cmd.x;
py = cmd.y; py = cmd.y;
} }
@@ -1280,6 +1374,7 @@ class WallPlotter {
this.feedRateTravel = savedTravel; this.feedRateTravel = savedTravel;
this.feedRateDraw = savedDraw; this.feedRateDraw = savedDraw;
this.isPenDown = savedPen; this.isPenDown = savedPen;
this.penPressure = savedPressure;
} }
return total; return total;
} }
@@ -1304,7 +1399,10 @@ class WallPlotter {
} }
async _animatePosition(fromX, fromY, cmd, duration) { 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); const waitMs = Math.max(duration, 1);
let elapsed = 0; let elapsed = 0;
let last = performance.now(); let last = performance.now();
@@ -1360,24 +1458,10 @@ class WallPlotter {
async _sendSegment(cmd) { async _sendSegment(cmd) {
if (!this._requireHome()) return; if (!this._requireHome()) return;
if (cmd.type === 'move') { await this._applyPenPressure(this._cmdPressure(cmd), { wait: false });
if (this.isPenDown) { const feed = cmd.type === 'move' ? this.feedRateTravel : this.feedRateDraw;
await this.send(this.penUpGcode); // G1 not G0: Marlin G0_FEEDRATE overrides F unless VARIABLE_G0_FEEDRATE is on
this.isPenDown = false; await this.send(`G1 X${this._fmt(cmd.x)} Y${this._fmt(cmd.y)} F${feed}`);
}
// 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}`
);
}
} }
async _sendSegmentAndWait(cmd) { async _sendSegmentAndWait(cmd) {
@@ -2189,8 +2273,9 @@ class WallPlotter {
if (px !== null && py !== null) { if (px !== null && py !== null) {
if (cmd.type === 'draw') { if (cmd.type === 'draw') {
const pressure = this._cmdPressure(cmd);
stroke(255, 200, 60, isFuture ? 60 : 255); stroke(255, 200, 60, isFuture ? 60 : 255);
strokeWeight(2); strokeWeight(0.6 + (pressure / 100) * 1.8);
line(px, py, p.x, p.y); line(px, py, p.x, p.y);
} else { } else {
stroke(100, 100, 120, isFuture ? 60 : 200); stroke(100, 100, 120, isFuture ? 60 : 200);
@@ -2239,7 +2324,11 @@ class WallPlotter {
16, 16,
52 52
); );
text(`pen ${this.isPenDown ? 'down (drawing)' : 'up (travel)'}`, 16, 72); text(
`pen ${this.isPenDown ? 'down (drawing)' : 'up (travel)'} ${this.penPressure}%`,
16,
72
);
text( text(
`speed travel ${this.feedRateTravel} draw ${this.feedRateDraw} mm/min ×${this.timingMultiplier}`, `speed travel ${this.feedRateTravel} draw ${this.feedRateDraw} mm/min ×${this.timingMultiplier}`,
16, 16,
+29 -8
View File
@@ -105,14 +105,26 @@ Scales **Test** preview waits only. Plot is paced by Marlin (`ok` + `M400`).
Clear the path buffer. Returns `this`. Clear the path buffer. Returns `this`.
### `move(x, y)` ### `move(x, y, pressure?)`
Queue pen-up travel to `(x, y)` mm. Returns `this`. 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`. 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)` ### `wait(ms)`
Queue a pause. `ms` is milliseconds. On Plot, sends `G4 P<ms>`. Returns `this`. Queue a pause. `ms` is milliseconds. On Plot, sends `G4 P<ms>`. Returns `this`.
@@ -182,7 +194,11 @@ Write one G-code line and wait for Marlin `ok`.
### `penUp(force?)` / `penDown(force?)` ### `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?)` ### `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.clamp` (boolean, default `true`): clamp result to bounds.
- `options.useDisplayBounds` (boolean, default `false`): use current display bounds (preview when disconnected) instead of machine 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()` ### `goToZero()`
@@ -224,7 +244,8 @@ Relative move in mm.
| `connected` | Serial open | | `connected` | Serial open |
| `homeSet` | Home was set with `H` / `setHomeHere()` | | `homeSet` | Home was set with `H` / `setHomeHere()` |
| `x`, `y` | Current position, mm | | `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 | | `isPlotting` | `runPath` in progress |
| `isPaused` | `runPath` is paused | | `isPaused` | `runPath` is paused |
| `feedRateTravel` / `feedRateDraw` | Current speeds, mm/min | | `feedRateTravel` / `feedRateDraw` | Current speeds, mm/min |