pen pressure
This commit is contained in:
+136
-47
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user