Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Optional flat, ball, bull and V-bit cutter geometry in IR tools. Run jobs preserve their shapes and resolve chamfer geometry per tool ID, rejecting conflicting definitions before output (#146).
- `verify --mesh` uses each cutting move's tool geometry when `--tool` is omitted, including tool changes within a path. Missing shapes and unsupported V-bit verification fail explicitly; `--tool` remains a global override (#146).
- Three-axis mill and router posts may set `"units": "inch"`. The IR and physical tolerances stay in millimetres. The writer emits `G20` and converts linear words (`X` `Y` `Z`, `I` `J` `K`, `R`) and units-per-minute feeds; read-back and mill audit convert them back to millimetres. Template literals are not converted. Rotary, five-axis, and lathe inch configurations are refused. Built-in posts stay metric. This output has not been proven on a machine (#88).

### Fixed

Expand Down
26 changes: 26 additions & 0 deletions crates/post/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@ RS274/NGC posts. A post definition is JSON (`posts/linuxcnc.json` and the other

`decimal_point`, when true, keeps a decimal point on every dimensional word (`X` `Y` `Z`, `I` `J` `K`, `R`, rotary `A`/`B`, and lathe `X`/`Z`): a whole number is `52.` and zero is `0.`. Fanuc and Haas controls read a word written without a decimal point as least-input increments, so `X52` is 0.052 mm and `A90` is 0.09 degrees. Feed (`F`), spindle (`S`), dwell (`P`), line numbers, and tool and program numbers are left alone. LinuxCNC, Mach3, Grbl, and plasma omit the flag and read plain decimals.

## Inch output

Custom three-axis mill/router posts may set `"units": "inch"`. Built-in posts stay metric.
Use the existing `--post FILE.json` option; input job units and output post units are independent.
The IR, probe targets, feeds, and `PostOptions::arc_tolerance` remain in millimetres and mm/min.
The writer converts linear axes, IJK offsets, arc radii, and feeds to inches and inches/minute.
Dwell times, spindle speeds, identifiers, and digital-output pins are unchanged.

Start the inch post's header with `G20` and use absolute motion and units-per-minute feeds
(`G90`, `G94`). The writer rejects missing or conflicting units and templates selecting G91,
G93, or G95. A program uses one unit system throughout; switches to G21 are rejected even in
its footer. Literal numbers in templates are already in output units: review such values as
`G64 P0.01` when adapting a metric post. They are not automatically converted.

`decimals.linear` and `decimals.feed` specify digits in the **output** units. Four linear digits
in inches have a 0.00254 mm increment. Choose enough precision for the required physical
accuracy; `compare`'s positional tolerance is still supplied in mm. Rounding guards and feed
comparison use the physical output increment. Feeds that round to zero are rejected.

`read_back` and the mill `audit` normalize lengths to mm and G94 feeds to mm/min. The audit
retains `UnitMode::Inch` to identify the actual declaration. G93 feeds in read-back/audit remain
reciprocal minutes. Unit and feed-mode words apply to the whole block regardless of word order.

Inch posts with `rotary`, `five_axis`, or `lathe` configurations are not supported yet; loading
one returns an explicit error. Their metric output and verification are unchanged.

## Process cycle

`process_cycle` is `hold` or `per_cut`. Omit it for `hold`.
Expand Down
43 changes: 37 additions & 6 deletions crates/post/src/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ impl SpindleState {
pub struct ArcReplay {
/// `G2` or `G3`.
pub sense: ArcSense,
/// Absolute centre.
/// Absolute centre in millimetres.
pub center: [f64; 3],
/// Signed radius when the block used `R`.
/// Signed radius in millimetres when the block used `R`.
pub radius: Option<f64>,
}

Expand All @@ -159,7 +159,7 @@ pub struct MotionState {
pub units: UnitMode,
/// Feed mode after this block.
pub feed_mode: FeedMode,
/// Active feed, including a modal `F` from an earlier block.
/// Active feed in mm/min (G94) or reciprocal minutes (G93), including a modal `F`.
pub feed: Option<f64>,
/// This block carried its own `F` word.
pub feed_on_block: bool,
Expand All @@ -173,7 +173,7 @@ pub struct MotionState {
pub spindle: SpindleState,
/// Coolant.
pub coolant: CoolantState,
/// Absolute end, in the frame the post wrote. An omitted axis stays at the previous value.
/// Absolute end in millimetres, in the frame the post wrote. Omitted axes retain their value.
pub end: [f64; 3],
/// Set on `G2` and `G3`.
pub arc: Option<ArcReplay>,
Expand Down Expand Up @@ -384,6 +384,7 @@ impl<'a> Parser<'a> {
}

fn interpret(&mut self, line_no: usize, words: &[Word]) -> Result<()> {
let previous_feed_mode = self.feed_mode;
let previous_motion = self.motion;
let previous_plane = self.plane;
let mut machine_move = false;
Expand Down Expand Up @@ -488,6 +489,33 @@ impl<'a> Parser<'a> {
}
}

// Resolve modal G words before normalizing this block's lengths and feed.
if self.post.is_inch() {
if previous_feed_mode != self.feed_mode && !feed_on_block {
self.feed = None;
}
if self.units.is_none()
&& (feed_on_block || x.is_some() || y.is_some() || z.is_some() || i.is_some() || j.is_some() || k.is_some() || r.is_some())
{
return Err(fail(line_no, "length or feed before units are set"));
}
for v in [&mut x, &mut y, &mut z, &mut i, &mut j, &mut k, &mut r].into_iter().flatten() {
*v *= self.post.length_scale();
if !v.is_finite() {
return Err(fail(line_no, "scaled length is not finite"));
}
}
if feed_on_block && self.feed_mode == FeedMode::PerMinute {
self.feed = self.feed.map(|f| f * self.post.length_scale());
if self.feed.is_some_and(|f| !f.is_finite()) {
return Err(fail(line_no, "scaled feed is not finite"));
}
}
if saw_rotary {
return Err(fail(line_no, "inch rotary motion is not supported"));
}
}

if digital {
let Some(pin) = p_value else {
return Err(fail(line_no, "digital output needs a P word"));
Expand Down Expand Up @@ -618,7 +646,7 @@ impl<'a> Parser<'a> {
17 => self.set_plane(line_no, Plane::Xy, block_plane)?,
18 => self.set_plane(line_no, Plane::Zx, block_plane)?,
19 => self.set_plane(line_no, Plane::Yz, block_plane)?,
20 => return Err(fail(line_no, "inches are not supported")),
20 => self.set_units(line_no, UnitMode::Inch)?,
21 => self.set_units(line_no, UnitMode::Millimetre)?,
40 | 53 | 64 | 80 => {}
43 => *want_length = true,
Expand Down Expand Up @@ -666,6 +694,9 @@ impl<'a> Parser<'a> {
}

fn set_units(&mut self, line_no: usize, units: UnitMode) -> Result<()> {
if (units == UnitMode::Inch) != self.post.is_inch() {
return Err(fail(line_no, "units disagree with post"));
}
if let Some(prev) = self.units
&& prev != units
{
Expand Down Expand Up @@ -760,7 +791,7 @@ impl<'a> Parser<'a> {
if radius == 0.0 {
return Err(fail(line_no, "arc radius must be non-zero"));
}
let quantum = 10_f64.powi(-i32::from(self.post.linear_decimals()));
let quantum = self.post.linear_quantum();
let center = geom::center_from_radius(self.pos, end, radius, sense, plane, quantum)?;
(center, Some(radius))
} else {
Expand Down
2 changes: 1 addition & 1 deletion crates/post/src/audit_5ax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ impl<'a> Parser<'a> {
17 => self.set_plane(line_no, Plane::Xy, &mut block_plane)?,
18 => self.set_plane(line_no, Plane::Zx, &mut block_plane)?,
19 => self.set_plane(line_no, Plane::Yz, &mut block_plane)?,
20 => return Err(fail(line_no, "inches are not supported")),
20 => return Err(fail(line_no, "inch five-axis verification is not supported yet")),
21 => self.set_units(line_no, UnitMode::Millimetre)?,
40 | 64 | 80 => {}
43 => want_length = true,
Expand Down
5 changes: 3 additions & 2 deletions crates/post/src/audit_lathe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -850,7 +850,7 @@ impl<'a> Parser<'a> {
match code {
7 => self.x_mode = XMode::Diameter,
8 => self.x_mode = XMode::Radius,
20 => return Err(fail(line_no, "inches are not supported")),
20 => return Err(fail(line_no, "inch lathe verification is not supported yet")),
21 => self.units = Some(UnitMode::Millimetre),
40 => self.nose = NoseComp::Off,
41 => self.nose = NoseComp::Left,
Expand Down Expand Up @@ -1252,7 +1252,8 @@ impl<'a> Parser<'a> {
impl Codes {
fn from_post(post: &PostDef, lathe: &LatheOutput) -> Self {
let mut known_g = BTreeSet::from([
0, 1, 2, 3, 4, 7, 8, 17, 18, 19, 21, 28, 40, 41, 42, 43, 49, 50, 53, 54, 55, 56, 57, 58, 59, 61, 64, 80, 90, 93, 94, 95, 96, 97, 98, 99,
0, 1, 2, 3, 4, 7, 8, 17, 18, 19, 20, 21, 28, 40, 41, 42, 43, 49, 50, 53, 54, 55, 56, 57, 58, 59, 61, 64, 80, 90, 93, 94, 95, 96, 97, 98,
99,
]);
let mut known_m = BTreeSet::from([2, 3, 4, 5, 6, 7, 8, 9, 30]);
let mut absorb = |lines: &[String]| {
Expand Down
43 changes: 32 additions & 11 deletions crates/post/src/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,14 @@ pub fn compare(program: &Program, moves: &[ReadMove], tol: f64, post: &PostDef)
tol,
index,
drop_z,
LineCheck { setup, angle: end_angle, angle_tol, feed: None, feed_decimals: post.decimals.feed },
LineCheck {
setup,
angle: end_angle,
angle_tol,
feed: None,
feed_decimals: post.decimals.feed,
feed_scale: post.length_scale(),
},
)?;
}
}
Expand Down Expand Up @@ -132,7 +139,14 @@ pub fn compare(program: &Program, moves: &[ReadMove], tol: f64, post: &PostDef)
tol,
index,
drop_z,
LineCheck { setup, angle: end_angle, angle_tol, feed: feed_check, feed_decimals: post.decimals.feed },
LineCheck {
setup,
angle: end_angle,
angle_tol,
feed: feed_check,
feed_decimals: post.decimals.feed,
feed_scale: post.length_scale(),
},
)?;
}
}
Expand Down Expand Up @@ -217,7 +231,13 @@ fn skip_z_only(post: &PostDef, pos: Option<Xyz>, end: Xyz, rapid: bool) -> Resul
let Some(pos) = pos else {
return Ok(false);
};
if !xy_text_eq(pos, end, post.decimals.linear, post.trailing_zeros, post.decimal_point)? {
if !xy_text_eq(
pos.map(|v| v / post.length_scale()),
end.map(|v| v / post.length_scale()),
post.decimals.linear,
post.trailing_zeros,
post.decimal_point,
)? {
return Ok(false);
}
let drop_rapid = rapid && !post.process_feeds.rapid_z;
Expand Down Expand Up @@ -350,7 +370,7 @@ fn consume_fallback(
if want_inverse {
check_recovered_feed(got_feed, geom::dist3(prev_cart, cart), ir_feed, index)?;
} else {
check_feed_number(got_feed, ir_feed, post.decimals.feed, index)?;
check_feed_number(got_feed, ir_feed, post.decimals.feed, post.length_scale(), index)?;
}
}
prev_cart = cart;
Expand Down Expand Up @@ -404,6 +424,7 @@ struct LineCheck<'a> {
angle_tol: f64,
feed: Option<FeedCheck>,
feed_decimals: u8,
feed_scale: f64,
}

fn take_line(
Expand Down Expand Up @@ -446,7 +467,7 @@ fn take_line(
check_recovered_feed(got_feed, feed.length, feed.ir_feed, index)?;
} else {
let expected = if feed.inverse { rotary::inverse_minutes_feed(feed.ir_feed, feed.length)? } else { feed.ir_feed };
check_feed_number(got_feed, expected, check.feed_decimals, index)?;
check_feed_number(got_feed, expected, check.feed_decimals, if feed.inverse { 1.0 } else { check.feed_scale }, index)?;
}
}
Ok(())
Expand Down Expand Up @@ -648,7 +669,7 @@ fn consume_curve(
// A short move that merely ends near this arc is the next motion.
// The tail is another block of this same circle. R-word centre error
// follows the output quantum, not a loose chord tolerance.
let quantum = 10_f64.powi(-i32::from(check.post.linear_decimals()));
let quantum = check.post.linear_quantum();
let centre_tol =
if radius.is_some() { radius_centre_limit(point, next_end, center, plane, quantum) } else { tol.max(quantum) };
centre_ok(got_center, center, plane, radius.is_some(), centre_tol)
Expand Down Expand Up @@ -790,7 +811,7 @@ fn take_thread(
if geom::dist3(got, end) > tol {
return Err(invalid(format!("motion {index}: end differs by {:.6} mm", geom::dist3(got, end))));
}
let quantum = 10_f64.powi(-i32::from(post.linear_decimals()));
let quantum = post.linear_quantum();
let limit = tol.max(quantum * 0.51);
if (got_lead - lead).abs() > limit {
return Err(invalid(format!("motion {index}: lead {got_lead} differs from {lead}")));
Expand Down Expand Up @@ -850,11 +871,11 @@ fn check_angle(got: Option<f64>, expect: f64, tol: f64, index: usize) -> Result<
if gap > tol { Err(invalid(format!("motion {index}: rotary angle differs by {gap:.6} deg"))) } else { Ok(()) }
}

fn check_feed_number(got: Option<f64>, expected: f64, decimals: u8, index: usize) -> Result<()> {
fn check_feed_number(got: Option<f64>, expected: f64, decimals: u8, scale: f64, index: usize) -> Result<()> {
let Some(got) = got else {
return Err(invalid(format!("motion {index}: missing feed")));
};
let quantum = 10_f64.powi(-i32::from(decimals));
let quantum = scale * 10_f64.powi(-i32::from(decimals));
let limit = quantum * 0.51 + expected.abs() * 1e-12;
if (got - expected).abs() > limit { Err(invalid(format!("motion {index}: feed {got} differs from {expected}"))) } else { Ok(()) }
}
Expand Down Expand Up @@ -906,14 +927,14 @@ fn check_curve_feed(
}
let implied = check.ir_feed / got;
let length = geom::dist3(prev, point);
let quantum = 10_f64.powi(-i32::from(check.post.decimals.linear));
let quantum = check.post.linear_quantum();
let limit = (4.0 * quantum).max(tol);
if (implied - length).abs() > limit {
return Err(invalid(format!("motion {index}: inverse-time feed implies {implied:.6} mm, segment is {length:.6} mm")));
}
Ok(())
} else {
check_feed_number(move_feed(mv), check.ir_feed, check.post.decimals.feed, index)
check_feed_number(move_feed(mv), check.ir_feed, check.post.decimals.feed, check.post.length_scale(), index)
}
}

Expand Down
20 changes: 18 additions & 2 deletions crates/post/src/def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -741,8 +741,11 @@ impl PostDef {
check_text("name", &self.name, 1, 128)?;
check_text("description", &self.description, 0, 1024)?;
check_references(&self.references)?;
if self.units != "mm" {
return Err(field("units", "only mm is supported"));
if !matches!(self.units.as_str(), "mm" | "inch") {
return Err(field("units", "must be mm or inch"));
}
if self.is_inch() && (self.rotary.is_some() || self.five_axis.is_some() || self.lathe.is_some()) {
return Err(crate::error::unsupported("inch output is only supported for three-axis mill posts"));
}
check_line_numbers(&mut self.line_numbers)?;
check_decimals(&self.decimals)?;
Expand Down Expand Up @@ -849,6 +852,19 @@ impl PostDef {
self.max_line_length.map(|n| n as usize)
}

pub(crate) fn is_inch(&self) -> bool {
self.units == "inch"
}

/// Millimetres per output length unit. The IR always remains in millimetres.
pub(crate) fn length_scale(&self) -> f64 {
if self.is_inch() { 25.4 } else { 1.0 }
}

pub(crate) fn linear_quantum(&self) -> f64 {
self.length_scale() * 10_f64.powi(-i32::from(self.decimals.linear))
}

/// Digits after the decimal point on `X` `Y` `Z` `I` `J` `K` `R`.
pub fn linear_decimals(&self) -> u8 {
self.decimals.linear
Expand Down
1 change: 1 addition & 0 deletions crates/post/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod geom;
mod interp;
mod read;
mod rotary;
mod units;
mod write;

pub use audit::{
Expand Down
Loading
Loading