live plot example and screenToPlot helper
This commit is contained in:
@@ -3,3 +3,4 @@ Thumbs.db
|
|||||||
*.log
|
*.log
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
unterricht
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ p5.js library for a Marlin polargraph (wall plotter) over Web Serial, plus a zig
|
|||||||
```
|
```
|
||||||
p5-wallplotter/
|
p5-wallplotter/
|
||||||
├── tutorial.md # setup + plotter + G-code
|
├── tutorial.md # setup + plotter + G-code
|
||||||
|
├── unterricht/ # three workshop assignments
|
||||||
├── hardware.md # motors, board, geometry
|
├── hardware.md # motors, board, geometry
|
||||||
├── gcode.md # G-code used by this setup
|
├── gcode.md # G-code used by this setup
|
||||||
├── plotter.svg # devices + signal path
|
├── plotter.svg # devices + signal path
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Wall Plotter — mouse</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #1c1c20;
|
||||||
|
color: #ddd;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.6rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
z-index: 20;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
opacity: 0.85;
|
||||||
|
pointer-events: none;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
#sketch-host {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
#sketch-host canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
|
||||||
|
<script src="../../lib/WallPlotter.js?v=2.3.24"></script>
|
||||||
|
<script src="sketch.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Wall Plotter — mouse</h1>
|
||||||
|
<main id="sketch-host"></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Live-Maus: Jeder Klick schickt eine Zielposition. Der Plotter
|
||||||
|
// fährt erst dorthin (moveTo wartet auf Bewegungsende). Der nächste Klick wird
|
||||||
|
// erst danach ausgeführt.
|
||||||
|
// waitUntilArrived: true = Klicks während der Fahrt ignorieren.
|
||||||
|
// false = Klicks dürfen sich stapeln (send-Kette in der Library).
|
||||||
|
|
||||||
|
let wp;
|
||||||
|
let live = false;
|
||||||
|
let busy = false;
|
||||||
|
const waitUntilArrived = true;
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const canvas = createCanvas(windowWidth, windowHeight);
|
||||||
|
canvas.parent('sketch-host');
|
||||||
|
|
||||||
|
wp = new WallPlotter({
|
||||||
|
penUpGcode: 'M280 P0 S120',
|
||||||
|
penDownGcode: 'M280 P0 S30',
|
||||||
|
penUpDelayMs: 250,
|
||||||
|
penDownDelayMs: 150,
|
||||||
|
});
|
||||||
|
wp.setSpeed(3000, 3000);
|
||||||
|
wp.enableKeyInputs();
|
||||||
|
wp.enablePlotButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jeden Frame: Overlay zeichnen und die Maus umranden.
|
||||||
|
function draw() {
|
||||||
|
background(28, 28, 32);
|
||||||
|
wp.showInfos();
|
||||||
|
|
||||||
|
noFill();
|
||||||
|
stroke(live ? color(230, 50, 50) : color(220));
|
||||||
|
strokeWeight(2);
|
||||||
|
circle(mouseX, mouseY, 16);
|
||||||
|
|
||||||
|
if (live) {
|
||||||
|
noStroke();
|
||||||
|
fill(230, 50, 50);
|
||||||
|
textSize(12);
|
||||||
|
textAlign(LEFT, CENTER);
|
||||||
|
text(busy ? 'LIVE BUSY' : 'LIVE', mouseX + 14, mouseY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fenstergrösse geändert: Canvas anpassen.
|
||||||
|
function windowResized() {
|
||||||
|
resizeCanvas(windowWidth, windowHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taste L: Live-Modus ein- oder ausschalten.
|
||||||
|
function keyPressed() {
|
||||||
|
if (key !== 'l' && key !== 'L') return;
|
||||||
|
live = !live;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Klick (nur im Live-Modus): Gondel fährt zu dieser Millimeter-Position, Stift oben.n
|
||||||
|
async function mousePressed() {
|
||||||
|
if (!live || !wp.connected || !wp.homeSet) return;
|
||||||
|
if (waitUntilArrived && busy) return;
|
||||||
|
|
||||||
|
busy = true;
|
||||||
|
try {
|
||||||
|
// Maus-Pixel auf dem Overlay -> Plotter-Koordinaten
|
||||||
|
// Die Library nutzt den aktuellen Overlay-Layout-Massstab und begrenzt
|
||||||
|
// standardmässig auf die Maschinen-Grenzen (inkl. korrekter Y-Ausrichtung).
|
||||||
|
const p = wp.screenToPlot(mouseX, mouseY);
|
||||||
|
await wp.moveTo(p.x, p.y);
|
||||||
|
|
||||||
|
console.log('moveTo', p.x, p.y);
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Von den Plot-Buttons erwartet; hier ungenutzt (kein gepufferter Pfad).
|
||||||
|
async function plot() {}
|
||||||
|
|
||||||
|
async function test() {}
|
||||||
@@ -320,6 +320,38 @@ class WallPlotter {
|
|||||||
this.timingMultiplier = Math.max(0.1, Number(mult) || 1);
|
this.timingMultiplier = Math.max(0.1, Number(mult) || 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert canvas pixel coordinates to plot coordinates in mm.
|
||||||
|
* By default this uses machine bounds so live-click targeting stays stable.
|
||||||
|
* @param {number} px
|
||||||
|
* @param {number} py
|
||||||
|
* @param {{clamp?: boolean, useDisplayBounds?: boolean}} [options]
|
||||||
|
*/
|
||||||
|
screenToPlot(px, py, options = {}) {
|
||||||
|
const layout = this._getPlotLayout();
|
||||||
|
const bounds =
|
||||||
|
options.useDisplayBounds === true
|
||||||
|
? this._getDisplayBounds()
|
||||||
|
: {
|
||||||
|
xMin: this.machineXMin,
|
||||||
|
xMax: this.machineXMax,
|
||||||
|
yMin: this.machineYMin,
|
||||||
|
yMax: this.machineYMax,
|
||||||
|
};
|
||||||
|
|
||||||
|
const scale = Math.max(layout.scale || 0, 1e-9);
|
||||||
|
const worldX = (px - layout.cx) / scale + layout.worldCx;
|
||||||
|
const worldY = -(py - layout.cy) / scale + layout.worldCy;
|
||||||
|
const clamp = options.clamp !== false;
|
||||||
|
|
||||||
|
if (!clamp) return { x: worldX, y: worldY };
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: Math.min(bounds.xMax, Math.max(bounds.xMin, worldX)),
|
||||||
|
y: Math.min(bounds.yMax, Math.max(bounds.yMin, worldY)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Public API — path buffer (queue only; nothing is sent until runPath)
|
// Public API — path buffer (queue only; nothing is sent until runPath)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -929,19 +961,42 @@ class WallPlotter {
|
|||||||
/** Pen up and travel to (x, y) mm immediately. Requires home. */
|
/** Pen up and travel to (x, y) mm immediately. Requires home. */
|
||||||
async moveTo(x, y) {
|
async moveTo(x, y) {
|
||||||
if (!this._requireHome()) return;
|
if (!this._requireHome()) return;
|
||||||
|
const startX = this.x;
|
||||||
|
const startY = this.y;
|
||||||
await this.penUp();
|
await this.penUp();
|
||||||
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;
|
||||||
|
await this._waitForMotionComplete(
|
||||||
|
Math.hypot((x ?? 0) - (startX ?? 0), (y ?? 0) - (startY ?? 0)),
|
||||||
|
this.feedRateTravel
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pen down and draw to (x, y) mm immediately. Requires home. */
|
/** Pen down and draw to (x, y) mm immediately. Requires home. */
|
||||||
async drawTo(x, y) {
|
async drawTo(x, y) {
|
||||||
if (!this._requireHome()) return;
|
if (!this._requireHome()) return;
|
||||||
|
const startX = this.x;
|
||||||
|
const startY = this.y;
|
||||||
await this.penDown();
|
await this.penDown();
|
||||||
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;
|
||||||
|
await this._waitForMotionComplete(
|
||||||
|
Math.hypot((x ?? 0) - (startX ?? 0), (y ?? 0) - (startY ?? 0)),
|
||||||
|
this.feedRateDraw
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _waitForMotionComplete(distanceMm, feedRateMmPerMin) {
|
||||||
|
const distance = Number.isFinite(distanceMm) ? Math.max(0, distanceMm) : 0;
|
||||||
|
const speedMmPerS = Math.max(1, Number(feedRateMmPerMin) || 1) / 60;
|
||||||
|
const etaMs = Math.round((distance / speedMmPerS) * 1000);
|
||||||
|
await this.send('M400', {
|
||||||
|
timeoutMs: Math.max(WallPlotter.OK_TIMEOUT_MS, etaMs + 15000),
|
||||||
|
// M400 legitimately blocks until mechanics are done moving.
|
||||||
|
waitLogAfterMs: Math.max(WallPlotter.OK_WAIT_LOG_MS, etaMs + 1500),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Travel to (0, 0). */
|
/** Travel to (0, 0). */
|
||||||
|
|||||||
+10
-2
@@ -184,13 +184,21 @@ Write one G-code line and wait for Marlin `ok`.
|
|||||||
|
|
||||||
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 in that state. Pass `true` to always send (used by `P` and on connect) so you can test the configured angles.
|
||||||
|
|
||||||
|
### `screenToPlot(px, py, options?)`
|
||||||
|
|
||||||
|
Convert canvas pixel coordinates to plot coordinates in mm.
|
||||||
|
|
||||||
|
- `px`, `py` (number): screen/canvas pixel coordinates.
|
||||||
|
- `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)`
|
||||||
|
|
||||||
Pen up, travel to `(x, y)` mm immediately (not queued). Requires home.
|
Pen up, travel to `(x, y)` mm immediately (not queued), then wait for motion completion (`M400`). Requires home.
|
||||||
|
|
||||||
### `drawTo(x, y)`
|
### `drawTo(x, y)`
|
||||||
|
|
||||||
Pen down, draw to `(x, y)` mm immediately. Requires home.
|
Pen down, draw to `(x, y)` mm immediately, then wait for motion completion (`M400`). Requires home.
|
||||||
|
|
||||||
### `goToZero()`
|
### `goToZero()`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user