pen pressure
This commit is contained in:
+135
-46
@@ -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;
|
||||
}
|
||||
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${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.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,
|
||||
|
||||
+29
-8
@@ -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<ms>`. 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 |
|
||||
|
||||
Reference in New Issue
Block a user