Files
craft-libs/crates/plotter/src/hpgl.rs
T
Dylan M. Taylor 5375f49ffc Add craft-plotter (HP-GL, DMPL, GP-GL); craft-gcode: bare output and Grbl replies (#863)
* craft-gcode: bare output for line-by-line senders, and Grbl replies

Writer::bare writes the same moves as Writer::new with every line a
command: no comments, coordinates at up to three decimals without
trailing zeros, M5 at the start, and the return to the origin at the
end. A sender that waits for an ok after each line can stream it as is.
Writer::set_feed changes the feed rate between contours.

The new grbl module has the real-time command bytes (status query, feed
hold, cycle start, soft reset) and the parsers of the controller's
answers: the state of a status report (read up to MAX_REPLY_LEN bytes)
and the ok / error / ALARM acknowledgement of a line. No I/O.

* Add craft-plotter: HP-GL, DMPL and GP-GL from a job in millimetres

One crate for the command languages of vinyl cutters and pen plotters,
one module per language: hpgl, dmpl, gpgl (plain) and gpgl::silhouette
(the Silhouette dialect in its three generations, with registration
marks, the device queries and the parsers of their replies).

A Job is ordered operations: polylines in millimetres plus tool
settings. Each encode gives a Program: the commands, their framing, and
where a sender waits for the registration marks. The crate does no I/O
and builds for wasm.

A job is refused past 64 operations, 2,000,000 points, 10 passes or a
2 mm overcut, or when a value is not finite. Replies are read up to 256
bytes and report at most 8 tool holders. Every limit has a test, and
tests/golden.rs fixes the exact commands of each language.

The README records where the protocol facts come from.

* craft-plotter: optional serde for Mat, as its short name

A caller that keeps a mat in a settings file or a machine table needs to
read and write it. The serde feature serializes Mat as its short name
("12x24") and refuses an unknown name. Off by default; the crate still
has thiserror as its only required dependency.
2026-11-11 17:29:14 -05:00

44 lines
1.8 KiB
Rust

//! HP-GL, the subset every cutter that speaks it understands (`IN`, `SP`, `PU`, `PD`, which is
//! also valid HP-GL/2): Roland, USCutter and many unbranded sign cutters, and Graphtec machines in
//! HP-GL mode.
//!
//! The stream is `IN;`, then per operation `SP<holder>;` and per path `PUx,y;` to its start and
//! one `PDx,y,…;` through the rest, then `PU;` and `SP0;`. One command per line. The origin is
//! bottom-left and y points up, so the design is flipped about its bottom edge. Force and speed
//! are set on the machine's panel.
use crate::{Command, Error, Framing, Job, Program, y_up};
/// Plotter units per mm (1 plu = 0.025 mm).
pub const UNITS_PER_MM: f64 = 40.0;
/// Encodes `job` as HP-GL. A job with no geometry gives an empty program.
pub fn encode(job: &Job) -> Result<Program, Error> {
let Some(prepared) = job.prepare()? else {
return Ok(Program::default());
};
let p = y_up(job, prepared.bbox_mm.2, UNITS_PER_MM);
let mut c = vec![Command::Raw("IN;".to_string())];
for (op, paths) in job.operations.iter().zip(&prepared.operations) {
c.push(Command::Raw(format!("SP{};", op.holder)));
for path in paths {
let Some((first, rest)) = path.split_first() else {
continue;
};
let (x, y) = p(first);
c.push(Command::Raw(format!("PU{x},{y};")));
let pts: Vec<String> = rest
.iter()
.map(|q| {
let (x, y) = p(q);
format!("{x},{y}")
})
.collect();
c.push(Command::Raw(format!("PD{};", pts.join(","))));
}
}
c.push(Command::Raw("PU;".to_string()));
c.push(Command::Raw("SP0;".to_string()));
Ok(Program { commands: c, bbox_mm: Some(prepared.bbox_mm), framing: Framing::Newline, registration_wait: None })
}