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 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,