plotter drawing size and stop with key

This commit is contained in:
Martin Fuchs
2026-08-15 16:40:42 +02:00
parent e293b2f434
commit 5ca9b99df8
4 changed files with 153 additions and 41 deletions
+1 -1
View File
@@ -44,7 +44,7 @@
}
</style>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<script src="../../lib/WallPlotter.js"></script>
<script src="../../lib/WallPlotter.js?v=stop-s"></script>
<script src="sketch.js"></script>
</head>
<body>
+11 -6
View File
@@ -6,7 +6,12 @@ function setup() {
const canvas = createCanvas(windowWidth, windowHeight);
canvas.parent('sketch-host');
wp = new WallPlotter();
wp = new WallPlotter({
penUpGcode: 'M280 P0 S120',
penDownGcode: 'M280 P0 S30',
penUpDelayMs: 250,
penDownDelayMs: 150,
});
wp.setSpeed(10000, 10000);
wp.setTimingMultiplier(1); // tune if machine is slower than speed estimate, the higher the slower
wp.enableKeyInputs();
@@ -35,16 +40,16 @@ async function drawArt(testOnly) {
wp.move(-area, -area);
wp.wait(1000 * 10);
//wp.wait(1000 * 10);
let step = area / 20;
let step = area / 4.001;
for (let y = -area; y < area; y+=step){
for (let x = -area; x < area; x+=step){
if(random(1) < 0.8){
wp.move(x, y);
//if(random(1) < 0.8){
wp.draw(x, y);
wp.draw(x, y+step);
}
//}
}
}
+7 -7
View File
@@ -26,8 +26,8 @@ All options are optional.
| `penDownGcode` | `M280 P0 S50` | Command to lower pen |
| `penUpDelayMs` | `150` | Pause after pen up, ms |
| `penDownDelayMs` | `50` | Pause after pen down, ms |
| `machineXMin` / `machineXMax` | `-850` / `850` | Overlay X extents, mm |
| `machineYMin` / `machineYMax` | `-1000` / `1000` | Overlay Y extents, mm |
| `machineXMin` / `machineXMax` | `-830` / `830` | Overlay X extents, mm (idler spacing 1660) |
| `machineYMin` / `machineYMax` | `-1200` / `800` | Overlay Y; idlers at Y=800 (80 cm above work centre) |
| `jogIntervalMs` | `150` | Repeat delay while holding arrows, ms |
| `maxLogLines` | `8` | Serial lines shown on the HUD |
| `previewMinDelayMs` | `30` | Floor for preview segment wait, ms |
@@ -144,9 +144,9 @@ Sketch: `plot()` → `runPath(false)`, `test()` → `runPath(true)`.
Returns estimated duration in **milliseconds** for the current path buffer, or for `commands` if given. Uses `setSpeed`, queued `speed()`, `wait()`, and `timingMultiplier`. The HUD shows elapsed / est / remain while plotting.
### `stopPath()`
### `stopPath()` / `stopPlot()`
Abort the current `runPath`. No arguments.
`stopPath()` aborts the current `runPath`. `stopPlot()` also sends Marlin `M410` (quick stop) during a live plot. Key **S** calls `stopPlot()` while plotting.
---
@@ -156,7 +156,7 @@ All **async**. Coordinate moves on Plot require home (`H`) first.
### `connect()`
Web Serial port picker, enable steppers (`M17`), pen up, `M114`.
Web Serial port picker, enable steppers (`M17`), pen up, query polargraph area (`M665` → machine overlay bounds), `M114`.
### `disconnect()`
@@ -171,9 +171,9 @@ Write one G-code line and wait for Marlin `ok`.
- `options.waitOk` (boolean): wait for `ok`. Default `true`.
- `options.timeoutMs` (number): `ok` timeout, default `300000`.
### `penUp()` / `penDown()`
### `penUp(force?)` / `penDown(force?)`
Send pen G-code and wait the pen delay. No-op if already in that state.
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.
### `moveTo(x, y)`
+134 -27
View File
@@ -9,6 +9,8 @@ class WallPlotter {
static VERSION = 'wait-ok+M400';
static MAX_FEED_MM_MIN = 10000;
static MIN_FEED_MM_MIN = 1;
/** Offline overlay size (mm) — 150×150 cm centred on origin */
static PREVIEW_SIZE_MM = 1500;
constructor(options = {}) {
this.baudRate = options.baudRate ?? 115200;
@@ -19,10 +21,11 @@ class WallPlotter {
this.penDownGcode = options.penDownGcode ?? 'M280 P0 S50';
this.penUpDelayMs = options.penUpDelayMs ?? 150;
this.penDownDelayMs = options.penDownDelayMs ?? 50;
this.machineXMin = options.machineXMin ?? -850;
this.machineXMax = options.machineXMax ?? 850;
this.machineYMin = options.machineYMin ?? -1000;
this.machineYMax = options.machineYMax ?? 1000;
this.machineXMin = options.machineXMin ?? -830;
this.machineXMax = options.machineXMax ?? 830;
this.machineYMin = options.machineYMin ?? -1200;
this.machineYMax = options.machineYMax ?? 800;
this.previewSizeMm = options.previewSizeMm ?? WallPlotter.PREVIEW_SIZE_MM;
this.jogIntervalMs = options.jogIntervalMs ?? 150;
this.maxLogLines = options.maxLogLines ?? 8;
@@ -143,6 +146,21 @@ class WallPlotter {
this._pathAbort = true;
}
/** Abort path; if a live plot is running, also quick-stop Marlin (M410). */
async stopPlot() {
if (!this.isPlotting) return;
this.stopPath();
if (this.connected && !this._plotTestOnly) {
try {
await this.send('M410', { waitOk: false, allowWithoutHome: true });
} catch (_) {
/* port may be busy */
}
}
this.statusText = 'Stopped';
this.onLog('plot stopped');
}
_restartPathAfterAbort(mode) {
this._pendingAfterAbort = mode;
this._pathAbort = true;
@@ -526,7 +544,8 @@ class WallPlotter {
this.onStatusChange('connected');
await this.send('M17');
await this.penUp();
await this.penUp(true);
await this.send('M665'); // polargraph draw area → updates machine bounds
await this.send('M114');
}
@@ -632,15 +651,15 @@ class WallPlotter {
return false;
}
async penUp() {
if (!this.isPenDown) return;
async penUp(force = false) {
if (!force && !this.isPenDown) return;
await this.send(this.penUpGcode);
await this._delay(this.penUpDelayMs);
this.isPenDown = false;
}
async penDown() {
if (this.isPenDown) return;
async penDown(force = false) {
if (!force && this.isPenDown) return;
await this.send(this.penDownGcode);
await this._delay(this.penDownDelayMs);
this.isPenDown = true;
@@ -715,6 +734,11 @@ class WallPlotter {
return false;
}
if (k === 's') {
if (this.isPlotting) await this.stopPlot();
return false;
}
if (!this._keysEnabled || !this.connected) return;
if (k === 'h') {
@@ -728,8 +752,8 @@ class WallPlotter {
}
if (k === 'p') {
if (this.isPenDown) await this.penUp();
else await this.penDown();
if (this.isPenDown) await this.penUp(true);
else await this.penDown(true);
return false;
}
@@ -806,18 +830,40 @@ class WallPlotter {
}
}
_getDisplayBounds() {
if (this.connected) {
return {
xMin: this.machineXMin,
xMax: this.machineXMax,
yMin: this.machineYMin,
yMax: this.machineYMax,
preview: false,
};
}
const half = this.previewSizeMm / 2;
return {
xMin: -half,
xMax: half,
yMin: -half,
yMax: half,
preview: true,
};
}
_getPlotLayout() {
const margin = 60;
const size = min(width, height) - margin * 2;
const worldW = this.machineXMax - this.machineXMin;
const worldH = this.machineYMax - this.machineYMin;
const b = this._getDisplayBounds();
const worldW = Math.max(1, b.xMax - b.xMin);
const worldH = Math.max(1, b.yMax - b.yMin);
// Fit full display rectangle; centre the view on work origin (0,0)
const scale = min(size / worldW, size / worldH);
return {
cx: width / 2,
cy: height / 2 - 20,
scale,
worldCx: (this.machineXMin + this.machineXMax) / 2,
worldCy: (this.machineYMin + this.machineYMax) / 2,
worldCx: 0,
worldCy: 0,
};
}
@@ -856,22 +902,54 @@ class WallPlotter {
_drawPlotArea() {
const layout = this._getPlotLayout();
const bounds = this._getDisplayBounds();
push();
translate(layout.cx, layout.cy);
stroke(50);
strokeWeight(1);
const tl = this._worldToScreen(this.machineXMin, this.machineYMax, layout);
const br = this._worldToScreen(this.machineXMax, this.machineYMin, layout);
const tl = this._worldToScreen(bounds.xMin, bounds.yMax, layout);
const br = this._worldToScreen(bounds.xMax, bounds.yMin, layout);
const w = br.x - tl.x;
const h = br.y - tl.y;
noFill();
rectMode(CORNER);
rect(tl.x, tl.y, br.x - tl.x, br.y - tl.y);
strokeWeight(1);
if (bounds.preview) {
stroke(90, 90, 110);
drawingContext.setLineDash([8, 6]);
} else {
stroke(70);
drawingContext.setLineDash([]);
}
rect(tl.x, tl.y, w, h);
drawingContext.setLineDash([]);
stroke(60);
line(tl.x, 0, br.x, 0);
line(0, tl.y, 0, br.y);
// Size labels in cm along bottom and right edges
const widthCm = (bounds.xMax - bounds.xMin) / 10;
const heightCm = (bounds.yMax - bounds.yMin) / 10;
const widthLabel = Number.isInteger(widthCm)
? `${widthCm} cm`
: `${widthCm.toFixed(1)} cm`;
const heightLabel = Number.isInteger(heightCm)
? `${heightCm} cm`
: `${heightCm.toFixed(1)} cm`;
noStroke();
fill(bounds.preview ? 140 : 160);
textFont('monospace', 12);
textAlign(CENTER, TOP);
text(widthLabel, (tl.x + br.x) / 2, br.y + 8);
push();
translate(br.x + 8, (tl.y + br.y) / 2);
rotate(HALF_PI);
textAlign(CENTER, BOTTOM);
text(heightLabel, 0, 0);
pop();
const rectBounds = this._getEffectiveDrawBounds();
if (rectBounds) {
const dtl = this._worldToScreen(rectBounds.left, rectBounds.top, layout);
@@ -955,43 +1033,53 @@ class WallPlotter {
92
);
text(`home ${this.homeSet ? 'set' : 'not set'}`, 16, 112);
if (this.connected) {
text(
`machine X ${this.machineXMin.toFixed(0)}…${this.machineXMax.toFixed(0)} Y ${this.machineYMin.toFixed(0)}…${this.machineYMax.toFixed(0)} mm`,
16,
132
);
} else {
const cm = this.previewSizeMm / 10;
text(`preview ${cm} × ${cm} cm (connect for machine bounds)`, 16, 132);
}
if (this.isPlotting) {
fill(180, 220, 255);
text(
`plotting ${this.plotProgress}/${this.previewPath.length}`,
16,
132
152
);
const elapsedMs = performance.now() - this._plotStartedAt;
const remainMs = Math.max(0, this._plotEstimateMs - elapsedMs);
text(
`elapsed ${this._formatDuration(elapsedMs)} est ${this._formatDuration(this._plotEstimateMs)} remain ${this._formatDuration(remainMs)}`,
16,
152
172
);
text(
`seg wait ${this._lastSegmentMs.toFixed(0)} ms ×${this.timingMultiplier}`,
16,
172
192
);
fill(220);
}
if (this.connected && !this.homeSet && this._homeWarning) {
fill(255, 180, 80);
text(`! ${this._homeWarning}`, 16, this.isPlotting ? 192 : 132);
text(`! ${this._homeWarning}`, 16, this.isPlotting ? 212 : 152);
fill(220);
}
text(
`bounds L ${this._formatBound(this.drawBounds.left)} R ${this._formatBound(this.drawBounds.right)} T ${this._formatBound(this.drawBounds.top)} B ${this._formatBound(this.drawBounds.bottom)}`,
16,
this.isPlotting ? 212 : 152
this.isPlotting ? 232 : 172
);
const size = this._getDrawSize();
const areaY = this.isPlotting ? 232 : 172;
const areaY = this.isPlotting ? 252 : 192;
if (size) {
text(`area ${size.w.toFixed(1)} × ${size.h.toFixed(1)} mm`, 16, areaY);
} else if (
@@ -1012,7 +1100,8 @@ class WallPlotter {
'+ / - step size',
'H set current pos as home (G92) — required first',
'0 go to origin (0, 0)',
'P toggle pen',
'P toggle pen (test up/down angles)',
'S stop plot / test',
'C connect / disconnect',
];
for (let i = 0; i < help.length; i++) {
@@ -1074,6 +1163,7 @@ class WallPlotter {
const next = this._okResolvers.shift();
if (next) next.resolve();
}
this._parseMachineBounds(line);
if (line.startsWith('X:')) {
const match = line.match(/X:([-\d.]+)\s+Y:([-\d.]+)/);
if (match) {
@@ -1082,4 +1172,21 @@ class WallPlotter {
}
}
}
/** Parse Marlin polargraph M665 report: M665 S… L… R… T… B… H… */
_parseMachineBounds(line) {
if (!/\bM665\b/i.test(line)) return;
const l = line.match(/\bL(-?[\d.]+)/i);
const r = line.match(/\bR(-?[\d.]+)/i);
const t = line.match(/\bT(-?[\d.]+)/i);
const b = line.match(/\bB(-?[\d.]+)/i);
if (!l || !r || !t || !b) return;
this.machineXMin = parseFloat(l[1]);
this.machineXMax = parseFloat(r[1]);
this.machineYMax = parseFloat(t[1]);
this.machineYMin = parseFloat(b[1]);
this.onLog(
`machine bounds X ${this.machineXMin}…${this.machineXMax} Y ${this.machineYMin}…${this.machineYMax}`
);
}
}