You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

442 lines
15 KiB

use crate::arc::{ArcOrLineSegment, FlattenWithArcs, Transformed};
use crate::machine::Machine;
use g_code::{command, emit::Token};
use lyon_geom::euclid::{default::Transform2D, Angle};
use lyon_geom::{point, vector, Point, Vector};
use lyon_geom::{ArcFlags, CubicBezierSegment, QuadraticBezierSegment, SvgArc};
type F64Point = Point<f64>;
/// Turtle graphics simulator for paths that outputs the g-code representation for each operation.
/// Handles transforms, position, offsets, etc. See https://www.w3.org/TR/SVG/paths.html
#[derive(Debug)]
pub struct Turtle<'input> {
current_position: F64Point,
initial_position: F64Point,
current_transform: Transform2D<f64>,
transform_stack: Vec<Transform2D<f64>>,
pub machine: Machine<'input>,
previous_control: Option<F64Point>,
}
impl<'input> Turtle<'input> {
/// Create a turtle at the origin with no transform
pub fn new(machine: Machine<'input>) -> Self {
Self {
current_position: Point::zero(),
initial_position: Point::zero(),
current_transform: Transform2D::identity(),
transform_stack: vec![],
machine,
previous_control: None,
}
}
/// Move the turtle to the given absolute/relative coordinates in the current transform
/// https://www.w3.org/TR/SVG/paths.html#PathDataMovetoCommands
pub fn move_to<X, Y>(&mut self, abs: bool, x: X, y: Y) -> Vec<Token<'input>>
where
X: Into<Option<f64>>,
Y: Into<Option<f64>>,
{
let inverse_transform = self.current_transform.inverse().unwrap();
let original_current_position = inverse_transform.transform_point(self.current_position);
let x = x
.into()
.map(|x| {
if abs {
x
} else {
original_current_position.x + x
}
})
.unwrap_or(original_current_position.x);
let y = y
.into()
.map(|y| {
if abs {
y
} else {
original_current_position.y + y
}
})
.unwrap_or(original_current_position.y);
let to = self.current_transform.transform_point(point(x, y));
self.current_position = to;
self.initial_position = to;
self.previous_control = None;
self.machine
.tool_off()
.drain(..)
.chain(self.machine.absolute())
.chain(
command!(RapidPositioning {
X: to.x as f64,
Y: to.y as f64,
})
.into_token_vec(),
)
.collect()
}
fn linear_interpolation(x: f64, y: f64, feedrate: f64) -> Vec<Token<'static>> {
command!(LinearInterpolation {
X: x,
Y: y,
F: feedrate,
})
.into_token_vec()
}
fn circular_interpolation(svg_arc: SvgArc<f64>, feedrate: f64) -> Vec<Token<'input>> {
debug_assert!((svg_arc.radii.x.abs() - svg_arc.radii.y.abs()).abs() < f64::EPSILON);
match (svg_arc.flags.large_arc, svg_arc.flags.sweep) {
(false, true) => command!(CounterclockwiseCircularInterpolation {
X: svg_arc.to.x,
Y: svg_arc.to.y,
R: svg_arc.radii.x,
F: feedrate,
})
.into_token_vec(),
(false, false) => command!(ClockwiseCircularInterpolation {
X: svg_arc.to.x,
Y: svg_arc.to.y,
R: svg_arc.radii.x,
F: feedrate,
})
.into_token_vec(),
(true, _) => {
let (left, right) = svg_arc.to_arc().split(0.5);
let mut token_vec = Self::circular_interpolation(left.to_svg_arc(), feedrate);
token_vec.append(&mut Self::circular_interpolation(
right.to_svg_arc(),
feedrate,
));
token_vec
}
}
}
/// Close an SVG path, cutting back to its initial position
/// https://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
pub fn close(&mut self, feedrate: f64) -> Vec<Token<'input>> {
// See https://www.w3.org/TR/SVG/paths.html#Segment-CompletingClosePath
// which could result in a G91 G1 X0 Y0
if (self.current_position - self.initial_position)
.abs()
.lower_than(vector(std::f64::EPSILON, std::f64::EPSILON))
.all()
{
return vec![];
}
self.current_position = self.initial_position;
self.machine
.tool_on()
.drain(..)
.chain(self.machine.absolute())
.chain(Self::linear_interpolation(
self.initial_position.x,
self.initial_position.y,
feedrate,
))
.collect()
}
/// Draw a line from the current position in the current transform to the specified position
/// https://www.w3.org/TR/SVG/paths.html#PathDataLinetoCommands
pub fn line<X, Y>(&mut self, abs: bool, x: X, y: Y, feedrate: f64) -> Vec<Token<'input>>
where
X: Into<Option<f64>>,
Y: Into<Option<f64>>,
{
let inverse_transform = self.current_transform.inverse().unwrap();
let original_current_position = inverse_transform.transform_point(self.current_position);
let x = x
.into()
.map(|x| {
if abs {
x
} else {
original_current_position.x + x
}
})
.unwrap_or(original_current_position.x);
let y = y
.into()
.map(|y| {
if abs {
y
} else {
original_current_position.y + y
}
})
.unwrap_or(original_current_position.y);
let to = self.current_transform.transform_point(point(x, y));
self.current_position = to;
self.previous_control = None;
self.machine
.tool_on()
.drain(..)
.chain(self.machine.absolute())
.chain(Self::linear_interpolation(to.x, to.y, feedrate))
.collect()
}
/// Draw a cubic bezier curve segment
/// The public bezier functions call this command after converting to a cubic bezier segment
/// https://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands
fn bezier(
&mut self,
cbs: CubicBezierSegment<f64>,
tolerance: f64,
feedrate: f64,
) -> Vec<Token<'input>> {
let tokens: Vec<_> = if self
.machine
.supported_functionality()
.circular_interpolation
{
FlattenWithArcs::<f64>::flattened(&cbs, tolerance)
.drain(..)
.flat_map(|segment| match segment {
ArcOrLineSegment::Arc(arc) => Self::circular_interpolation(arc, feedrate),
ArcOrLineSegment::Line(line) => {
Self::linear_interpolation(line.to.x, line.to.y, feedrate)
}
})
.collect()
} else {
cbs.flattened(tolerance)
.flat_map(|point| Self::linear_interpolation(point.x, point.y, feedrate))
.collect()
};
self.current_position = cbs.to;
// See https://www.w3.org/TR/SVG/paths.html#ReflectedControlPoints
self.previous_control = Some(point(
2. * self.current_position.x - cbs.ctrl2.x,
2. * self.current_position.y - cbs.ctrl2.y,
));
self.machine
.tool_on()
.drain(..)
.chain(self.machine.absolute())
.chain(tokens)
.collect()
}
/// Draw a cubic curve from the current point to (x, y) with specified control points (x1, y1) and (x2, y2)
/// https://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands
pub fn cubic_bezier(
&mut self,
abs: bool,
mut ctrl1: Point<f64>,
mut ctrl2: Point<f64>,
mut to: Point<f64>,
tolerance: f64,
feedrate: f64,
) -> Vec<Token<'input>> {
let from = self.current_position;
if !abs {
let inverse_transform = self.current_transform.inverse().unwrap();
let original_current_position = inverse_transform.transform_point(from);
ctrl1 += original_current_position.to_vector();
ctrl2 += original_current_position.to_vector();
to += original_current_position.to_vector();
}
ctrl1 = self.current_transform.transform_point(ctrl1);
ctrl2 = self.current_transform.transform_point(ctrl2);
to = self.current_transform.transform_point(to);
let cbs = lyon_geom::CubicBezierSegment {
from,
ctrl1,
ctrl2,
to,
};
self.bezier(cbs, tolerance, feedrate)
}
/// Draw a shorthand/smooth cubic bezier segment, where the first control point was already given
/// https://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands
pub fn smooth_cubic_bezier(
&mut self,
abs: bool,
mut ctrl2: Point<f64>,
mut to: Point<f64>,
tolerance: f64,
feedrate: f64,
) -> Vec<Token<'input>> {
let from = self.current_position;
let ctrl1 = self.previous_control.unwrap_or(self.current_position);
if !abs {
let inverse_transform = self.current_transform.inverse().unwrap();
let original_current_position = inverse_transform.transform_point(from);
ctrl2 += original_current_position.to_vector();
to += original_current_position.to_vector();
}
ctrl2 = self.current_transform.transform_point(ctrl2);
to = self.current_transform.transform_point(to);
let cbs = lyon_geom::CubicBezierSegment {
from,
ctrl1,
ctrl2,
to,
};
self.bezier(cbs, tolerance, feedrate)
}
/// Draw a shorthand/smooth cubic bezier segment, where the control point was already given
/// https://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands
pub fn smooth_quadratic_bezier(
&mut self,
abs: bool,
mut to: Point<f64>,
tolerance: f64,
feedrate: f64,
) -> Vec<Token<'input>> {
let from = self.current_position;
let ctrl = self.previous_control.unwrap_or(self.current_position);
if !abs {
let inverse_transform = self.current_transform.inverse().unwrap();
let original_current_position = inverse_transform.transform_point(from);
to += original_current_position.to_vector();
}
to = self.current_transform.transform_point(to);
let qbs = QuadraticBezierSegment { from, ctrl, to };
self.bezier(qbs.to_cubic(), tolerance, feedrate)
}
/// Draw a quadratic bezier segment
/// https://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands
pub fn quadratic_bezier(
&mut self,
abs: bool,
mut ctrl: Point<f64>,
mut to: Point<f64>,
tolerance: f64,
feedrate: f64,
) -> Vec<Token<'input>> {
let from = self.current_position;
if !abs {
let inverse_transform = self.current_transform.inverse().unwrap();
let original_current_position = inverse_transform.transform_point(from);
to += original_current_position.to_vector();
ctrl += original_current_position.to_vector();
}
ctrl = self.current_transform.transform_point(ctrl);
to = self.current_transform.transform_point(to);
let qbs = QuadraticBezierSegment { from, ctrl, to };
self.bezier(qbs.to_cubic(), tolerance, feedrate)
}
/// Draw an elliptical arc segment
/// https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
pub fn elliptical(
&mut self,
abs: bool,
radii: Vector<f64>,
x_rotation: Angle<f64>,
flags: ArcFlags,
mut to: Point<f64>,
feedrate: f64,
tolerance: f64,
) -> Vec<Token<'input>> {
let from = self
.current_transform
.inverse()
.unwrap()
.transform_point(self.current_position);
if !abs {
to += from.to_vector()
}
let svg_arc = SvgArc {
from,
to,
radii,
x_rotation,
flags,
}
.transformed(&self.current_transform);
let arc_tokens = if svg_arc.is_straight_line() {
Self::linear_interpolation(svg_arc.to.x, svg_arc.to.y, feedrate)
} else if self
.machine
.supported_functionality()
.circular_interpolation
{
FlattenWithArcs::flattened(&svg_arc, tolerance)
.drain(..)
.flat_map(|segment| match segment {
ArcOrLineSegment::Arc(arc) => Self::circular_interpolation(arc, feedrate),
ArcOrLineSegment::Line(line) => {
Self::linear_interpolation(line.to.x, line.to.y, feedrate)
}
})
.collect()
} else {
svg_arc
.to_arc()
.flattened(tolerance)
.flat_map(|point| Self::linear_interpolation(point.x, point.y, feedrate))
.collect()
};
self.current_position = svg_arc.to;
self.previous_control = None;
self.machine
.tool_on()
.drain(..)
.chain(self.machine.absolute())
.chain(arc_tokens)
.collect()
}
/// Push a generic transform onto the stack
/// Could be any valid CSS transform https://drafts.csswg.org/css-transforms-1/#typedef-transform-function
/// https://www.w3.org/TR/SVG/coords.html#InterfaceSVGTransform
pub fn push_transform(&mut self, trans: Transform2D<f64>) {
self.transform_stack.push(self.current_transform);
self.current_transform = trans.then(&self.current_transform);
}
/// Pop a generic transform off the stack, returning to the previous transform state
/// This means that most recent transform went out of scope
pub fn pop_transform(&mut self) {
self.current_transform = self
.transform_stack
.pop()
.expect("popped when no transforms left");
}
/// Remove all transforms, returning to true absolute coordinates
pub fn pop_all_transforms(&mut self) {
self.transform_stack.clear();
self.current_transform = Transform2D::identity();
}
/// Reset the position of the turtle to the origin in the current transform stack
/// Used for starting a new path
pub fn reset(&mut self) {
self.current_position = Point::zero();
self.current_position = self
.current_transform
.transform_point(self.current_position);
self.previous_control = None;
self.initial_position = self.current_position;
}
}