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.
340 lines
12 KiB
340 lines
12 KiB
use std::fmt::Debug;
|
|
|
|
use lyon_geom::euclid::{default::Transform2D, Angle};
|
|
use lyon_geom::{point, vector, Point, Vector};
|
|
use lyon_geom::{ArcFlags, CubicBezierSegment, QuadraticBezierSegment, SvgArc};
|
|
|
|
use crate::arc::{Transformed};
|
|
|
|
mod g_code;
|
|
pub use self::g_code::GCodeTurtle;
|
|
|
|
pub trait Turtle: Debug {
|
|
fn begin(&mut self);
|
|
fn end(&mut self);
|
|
fn comment(&mut self, comment: String);
|
|
fn move_to(&mut self, to: Point<f64>);
|
|
fn line_to(&mut self, to: Point<f64>);
|
|
fn arc(&mut self, svg_arc: SvgArc<f64>);
|
|
fn cubic_bezier(&mut self, cbs: CubicBezierSegment<f64>);
|
|
fn quadratic_bezier(&mut self, qbs: QuadraticBezierSegment<f64>);
|
|
}
|
|
|
|
/// Wrapper for [Turtle] that handles transforms, position, offsets, etc. See https://www.w3.org/TR/SVG/paths.html
|
|
#[derive(Debug)]
|
|
pub struct Terrarium<T: Turtle + std::fmt::Debug> {
|
|
pub turtle: T,
|
|
current_position: Point<f64>,
|
|
initial_position: Point<f64>,
|
|
current_transform: Transform2D<f64>,
|
|
transform_stack: Vec<Transform2D<f64>>,
|
|
previous_quadratic_control: Option<Point<f64>>,
|
|
previous_cubic_control: Option<Point<f64>>,
|
|
}
|
|
|
|
impl<T: Turtle + std::fmt::Debug> Terrarium<T> {
|
|
/// Create a turtle at the origin with no transform
|
|
pub fn new(turtle: T) -> Self {
|
|
Self {
|
|
turtle,
|
|
current_position: Point::zero(),
|
|
initial_position: Point::zero(),
|
|
current_transform: Transform2D::identity(),
|
|
transform_stack: vec![],
|
|
previous_quadratic_control: None,
|
|
previous_cubic_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)
|
|
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_quadratic_control = None;
|
|
self.previous_cubic_control = None;
|
|
self.turtle.move_to(to);
|
|
}
|
|
|
|
/// Close an SVG path, cutting back to its initial position
|
|
/// https://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
|
|
pub fn close(&mut self) {
|
|
// 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()
|
|
{
|
|
self.turtle.line_to(self.initial_position);
|
|
}
|
|
self.current_position = self.initial_position;
|
|
self.previous_quadratic_control = None;
|
|
self.previous_cubic_control = None;
|
|
}
|
|
|
|
/// 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)
|
|
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_quadratic_control = None;
|
|
self.previous_cubic_control = None;
|
|
|
|
self.turtle.line_to(to);
|
|
}
|
|
|
|
/// 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>,
|
|
) {
|
|
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.current_position = cbs.to;
|
|
|
|
// See https://www.w3.org/TR/SVG/paths.html#ReflectedControlPoints
|
|
self.previous_cubic_control = Some(point(
|
|
2. * self.current_position.x - cbs.ctrl2.x,
|
|
2. * self.current_position.y - cbs.ctrl2.y,
|
|
));
|
|
self.previous_quadratic_control = None;
|
|
|
|
self.turtle.cubic_bezier(cbs);
|
|
}
|
|
|
|
/// 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>) {
|
|
let from = self.current_position;
|
|
let ctrl1 = self.previous_cubic_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.current_position = cbs.to;
|
|
|
|
// See https://www.w3.org/TR/SVG/paths.html#ReflectedControlPoints
|
|
self.previous_cubic_control = Some(point(
|
|
2. * self.current_position.x - cbs.ctrl2.x,
|
|
2. * self.current_position.y - cbs.ctrl2.y,
|
|
));
|
|
self.previous_quadratic_control = None;
|
|
|
|
self.turtle.cubic_bezier(cbs);
|
|
}
|
|
|
|
/// 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>) {
|
|
let from = self.current_position;
|
|
let ctrl = self
|
|
.previous_quadratic_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.current_position = qbs.to;
|
|
|
|
// See https://www.w3.org/TR/SVG/paths.html#ReflectedControlPoints
|
|
self.previous_quadratic_control = Some(point(
|
|
2. * self.current_position.x - qbs.ctrl.x,
|
|
2. * self.current_position.y - qbs.ctrl.y,
|
|
));
|
|
self.previous_cubic_control = None;
|
|
|
|
self.turtle.quadratic_bezier(qbs);
|
|
}
|
|
|
|
/// 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>) {
|
|
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.current_position = qbs.to;
|
|
|
|
// See https://www.w3.org/TR/SVG/paths.html#ReflectedControlPoints
|
|
self.previous_quadratic_control = Some(point(
|
|
2. * self.current_position.x - qbs.ctrl.x,
|
|
2. * self.current_position.y - qbs.ctrl.y,
|
|
));
|
|
self.previous_cubic_control = None;
|
|
|
|
self.turtle.quadratic_bezier(qbs);
|
|
}
|
|
|
|
/// 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>,
|
|
) {
|
|
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);
|
|
|
|
self.current_position = svg_arc.to;
|
|
self.previous_quadratic_control = None;
|
|
self.previous_cubic_control = None;
|
|
|
|
self.turtle.arc(svg_arc);
|
|
}
|
|
|
|
/// 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_quadratic_control = None;
|
|
self.previous_cubic_control = None;
|
|
self.initial_position = self.current_position;
|
|
}
|
|
}
|