Continue documentation efforts & cleanup

master
Sameer Puri 5 years ago
parent f7e037cfdd
commit 5842b2df0b

@ -3,7 +3,7 @@ name = "svg2gcode"
version = "0.0.1" version = "0.0.1"
authors = ["Sameer Puri <purisame@spuri.io>"] authors = ["Sameer Puri <purisame@spuri.io>"]
edition = "2018" edition = "2018"
description = "Convert paths in SVG files to GCode for a pen plotter" description = "Convert paths in SVG files to GCode for a pen plotter or laser engraver"
[dependencies] [dependencies]
svgdom = "0" svgdom = "0"

@ -25,7 +25,7 @@ Convert any SVG 1.1 path to gcode for a pen plotter, laser engraver, etc.
### Input ### Input
```bash ```bash
cargo run --release -- examples/Vanderbilt_Commodores_logo.svg cargo run -- examples/Vanderbilt_Commodores_logo.svg --off 'M4' --on 'M5' -o out.gcode
``` ```
![Vanderbilt Commodores Logo](examples/Vanderbilt_Commodores_logo.svg) ![Vanderbilt Commodores Logo](examples/Vanderbilt_Commodores_logo.svg)

@ -0,0 +1,104 @@
use core::convert::TryFrom;
use std::io::{self, Write};
#[macro_use]
mod spec;
pub use spec::*;
/// Collapses GCode words into higher-level commands
pub struct CommandVecIntoIterator {
vec: Vec<Word>,
index: usize,
}
impl Iterator for CommandVecIntoIterator {
type Item = Command;
fn next(&mut self) -> Option<Self::Item> {
if self.vec.len() <= self.index {
return None;
}
let mut i = self.index + 1;
while i < self.vec.len() {
if CommandWord::is_command(&self.vec[i]) {
break;
}
i += 1;
}
let command = Command::try_from(&self.vec[self.index..i]).ok();
self.index = i;
command
}
}
impl From<Vec<Word>> for CommandVecIntoIterator {
fn from(vec: Vec<Word>) -> Self {
Self { vec, index: 0 }
}
}
pub fn parse_gcode(gcode: &str) -> Vec<Word> {
let mut vec = vec![];
let mut in_string = false;
let mut letter: Option<char> = None;
let mut value_range = 0..0;
gcode.char_indices().for_each(|(i, c)| {
if (c.is_alphabetic() || c.is_ascii_whitespace()) && !in_string {
if let Some(l) = letter {
vec.push(Word {
letter: l,
value: parse_value(&gcode[value_range.clone()]),
});
letter = None;
}
if c.is_alphabetic() {
letter = Some(c);
}
value_range = (i + 1)..(i + 1);
} else if in_string {
value_range = value_range.start..(i + 1);
} else {
if c == '"' {
in_string = !in_string;
}
value_range = value_range.start..(i + 1);
}
});
if let Some(l) = letter {
vec.push(Word {
letter: l,
value: parse_value(&gcode[value_range.clone()]),
});
}
vec
}
fn parse_value(word: &str) -> Value {
if word.starts_with('"') && word.ends_with('"') {
Value::String(Box::new(word.to_string()))
} else {
let index_of_dot = word.find('.');
Value::Fractional(
word[..index_of_dot.unwrap_or(word.len())]
.parse::<u32>()
.unwrap(),
index_of_dot.map(|j| word[j + 1..].parse::<u32>().unwrap()),
)
}
}
/// Writes a GCode program or sequence to a Writer
/// Each command is placed on a separate line
pub fn program2gcode<W: Write>(program: Vec<Command>, mut w: W) -> io::Result<()> {
for command in program.into_iter() {
let words: Vec<Word> = command.into();
let mut it = words.iter();
if let Some(command_word) = it.next() {
write!(w, "{}{}", command_word.letter, command_word.value)?;
for word in it {
write!(w, " {}{} ", word.letter, word.value)?;
}
writeln!(w, "")?;
}
}
Ok(())
}

@ -1,69 +1,48 @@
use core::convert::TryFrom; use std::convert::TryFrom;
use std::io::{self, Write};
/// Collapses GCode words into higher-level commands /// Fundamental unit of GCode: a value preceded by a descriptive letter.
/// Relies on the first word being a command. #[derive(Clone, PartialEq, Debug)]
pub struct CommandVec { pub struct Word {
pub inner: Vec<Word>, pub letter: char,
} pub value: Value,
impl Default for CommandVec {
fn default() -> Self {
Self {
inner: vec![]
}
}
}
impl IntoIterator for CommandVec {
type Item = Command;
type IntoIter = CommandVecIntoIterator;
fn into_iter(self) -> Self::IntoIter {
CommandVecIntoIterator {
vec: self,
index: 0,
}
}
} }
pub struct CommandVecIntoIterator { /// All the possible variations of a word's value.
vec: CommandVec, /// Fractional is needed to support commands like G91.1 which would be changed by float arithmetic.
index: usize, /// Some flavors of GCode also allow for strings.
#[derive(Clone, PartialEq, Debug)]
pub enum Value {
Fractional(u32, Option<u32>),
Float(f64),
String(Box<String>)
} }
impl Iterator for CommandVecIntoIterator { impl std::fmt::Display for Value {
type Item = Command; fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn next(&mut self) -> Option<Self::Item> { match self {
if self.vec.inner.len() == self.index { Self::Fractional(number, Some(fraction)) => {
return None; write!(f, "{}.{}", number, fraction)
} },
Self::Fractional(number, None) => {
let mut i = self.index + 1; write!(f, "{}", number)
while i < self.vec.inner.len() { },
if CommandWord::is_command(&self.vec.inner[i]) { Self::Float(float) => {
break; write!(f, "{}", float)
},
Self::String(string) => {
write!(f, "\"{}\"", string)
} }
i += 1;
} }
Command::try_from(&self.vec.inner[self.index..i]).ok()
} }
} }
/// Fundamental unit of GCode: a value preceded by a descriptive letter. /// A macro for quickly instantiating a float-valued command
/// A float is used here to encompass all the possible variations of a value.
/// Some flavors of GCode may allow strings, but that is currently not supported.
#[derive(Clone, PartialEq, Debug)]
pub struct Word {
pub letter: char,
pub value: f64,
}
#[macro_export] #[macro_export]
macro_rules! command { macro_rules! command {
($commandWord: expr, {$($argument: ident : $value: expr,)*}) => { ($commandWord: expr, {$($argument: ident : $value: expr,)*}) => {
paste::expr! (Command::new($commandWord, vec![$(Word { paste::expr! (Command::new($commandWord, vec![$(Word {
letter: stringify!([<$argument:upper>]).chars().next().unwrap(), letter: stringify!([<$argument:upper>]).chars().next().unwrap(),
value: $value as f64, value: Value::Float($value),
},)*])) },)*]))
}; };
} }
@ -124,6 +103,9 @@ macro_rules! commands {
impl TryFrom<&[Word]> for Command { impl TryFrom<&[Word]> for Command {
type Error = (); type Error = ();
fn try_from(words: &[Word]) -> Result<Self, ()> { fn try_from(words: &[Word]) -> Result<Self, ()> {
if words.len() == 0 {
return Err(());
}
let command_word = CommandWord::try_from(&words[0])?; let command_word = CommandWord::try_from(&words[0])?;
let mut arguments = Vec::with_capacity(words.len() - 1); let mut arguments = Vec::with_capacity(words.len() - 1);
for i in 1..words.len() { for i in 1..words.len() {
@ -162,13 +144,9 @@ macro_rules! commands {
paste::item! { paste::item! {
impl CommandWord { impl CommandWord {
pub fn is_command(word: &Word) -> bool { pub fn is_command(word: &Word) -> bool {
let number = word.value as u16; let (number, fraction) = match &word.value {
let fraction_numeric = Value::Fractional(number, fraction) => (number, fraction),
f64::from_bits(word.value.fract().to_bits() & 0x00_00_FF_FF_FF_FF_FF_FF) as u16; _other => return false
let fraction = if fraction_numeric == 0 {
None
} else {
Some(fraction_numeric)
}; };
match (word.letter, number, fraction) { match (word.letter, number, fraction) {
$(($letter, $number, $fraction) => true,)* $(($letter, $number, $fraction) => true,)*
@ -183,18 +161,14 @@ macro_rules! commands {
impl TryFrom<&Word> for CommandWord { impl TryFrom<&Word> for CommandWord {
type Error = (); type Error = ();
fn try_from(word: &Word) -> Result<Self, ()> { fn try_from(word: &Word) -> Result<Self, ()> {
let number = word.value as u16; let (number, fraction) = match &word.value {
let fraction_numeric = Value::Fractional(number, fraction) => (number, fraction),
f64::from_bits(word.value.fract().to_bits() & 0x00_00_FF_FF_FF_FF_FF_FF) as u16; _other => return Err(())
let fraction = if fraction_numeric == 0 {
None
} else {
Some(fraction_numeric)
}; };
match (word.letter, number, fraction) { match (word.letter, number, fraction) {
$(($letter, $number, $fraction) => Ok(Self::$commandName),)* $(($letter, $number, $fraction) => Ok(Self::$commandName),)*
('*', checksum, None) => Ok(Self::Checksum(checksum as u8)), ('*', checksum, None) => Ok(Self::Checksum(*checksum as u8)),
('N', line_number, None) => Ok(Self::LineNumber(line_number)), ('N', line_number, None) => Ok(Self::LineNumber(*line_number as u16)),
(_, _, _) => Err(()) (_, _, _) => Err(())
} }
} }
@ -208,20 +182,20 @@ macro_rules! commands {
Self::$commandName {} => Word { Self::$commandName {} => Word {
letter: $letter, letter: $letter,
// TODO: fix fraction // TODO: fix fraction
value: $number as f64 + ($fraction.unwrap_or(0) as f64) value: Value::Fractional($number, $fraction)
}, },
)* )*
Self::Checksum(value) => Word { Self::Checksum(value) => Word {
letter: '*', letter: '*',
value: value as f64 value: Value::Fractional(value as u32, None)
}, },
Self::LineNumber(value) => Word { Self::LineNumber(value) => Word {
letter: 'N', letter: 'N',
value: value as f64 value: Value::Fractional(value as u32, None)
}, },
Self::Comment(_string) => Word { Self::Comment(string) => Word {
letter: ';', letter: ';',
value: 0.0 value: Value::String(string)
} }
} }
} }
@ -288,6 +262,9 @@ commands!(
RelativeDistanceMode { RelativeDistanceMode {
'G', 91, None, {} 'G', 91, None, {}
}, },
FeedRateUnitsPerMinute {
'G', 94, None, {}
},
/// Start spinning the spindle clockwise with speed `p` /// Start spinning the spindle clockwise with speed `p`
StartSpindleClockwise { StartSpindleClockwise {
'M', 3, None, { 'M', 3, None, {
@ -311,33 +288,3 @@ commands!(
'M', 20, None, {} 'M', 20, None, {}
}, },
); );
/// Rudimentary regular expression GCode validator
pub fn validate_gcode(gcode: &&str) -> bool {
use regex::Regex;
let re = Regex::new(r##"^(?:(?:%|\(.*\)|(?:[A-Z^E^U][+-]?\d+(?:\.\d*)?))\h*)*$"##).unwrap();
gcode.lines().all(|line| re.is_match(line))
}
/// Writes a GCode program (or sequence) to a Writer
pub fn program2gcode<W: Write>(program: Vec<Command>, mut w: W) -> io::Result<()> {
for command in program.into_iter() {
match &command.command_word {
CommandWord::Comment(string) => {
writeln!(w, ";{}", string)?;
},
_other => {
let words: Vec<Word> = command.into();
let mut it = words.iter();
if let Some(command_word) = it.next() {
write!(w, "{}{}", command_word.letter, command_word.value)?;
for word in it {
write!(w, " {}{} ", word.letter, word.value)?;
}
writeln!(w, "")?;
}
}
}
}
Ok(())
}

@ -29,23 +29,32 @@ pub struct Machine {
distance_mode: Option<Distance>, distance_mode: Option<Distance>,
tool_on_action: Vec<Command>, tool_on_action: Vec<Command>,
tool_off_action: Vec<Command>, tool_off_action: Vec<Command>,
program_begin_sequence: Vec<Command>,
program_end_sequence: Vec<Command>,
} }
impl Machine { impl Machine {
/// Create a generic machine, given a tool on/off GCode sequence. /// Create a generic machine, given a tool on/off GCode sequence.
pub fn new(tool_on_action: CommandVec, tool_off_action: CommandVec) -> Self { pub fn new(
tool_on_action: Vec<Word>,
tool_off_action: Vec<Word>,
program_begin_sequence: Vec<Word>,
program_end_sequence: Vec<Word>,
) -> Self {
Self { Self {
tool_state: None, tool_state: None,
distance_mode: None, distance_mode: None,
tool_on_action: tool_on_action.into_iter().collect(), tool_on_action: CommandVecIntoIterator::from(tool_on_action).collect(),
tool_off_action: tool_off_action.into_iter().collect() tool_off_action: CommandVecIntoIterator::from(tool_off_action).collect(),
program_begin_sequence: CommandVecIntoIterator::from(program_begin_sequence).collect(),
program_end_sequence: CommandVecIntoIterator::from(program_end_sequence).collect(),
} }
} }
} }
impl Machine { impl Machine {
/// Output gcode to turn the tool on. /// Output gcode to turn the tool on.
pub fn tool_on(& mut self) -> Vec<Command> { pub fn tool_on(&mut self) -> Vec<Command> {
if self.tool_state == Some(Tool::Off) || self.tool_state == None { if self.tool_state == Some(Tool::Off) || self.tool_state == None {
self.tool_state = Some(Tool::On); self.tool_state = Some(Tool::On);
self.tool_on_action.clone() self.tool_on_action.clone()
@ -63,6 +72,10 @@ impl Machine {
vec![] vec![]
} }
} }
pub fn program_begin(&self) -> Vec<Command> { self.program_begin_sequence.clone() }
pub fn program_end(&self) -> Vec<Command> { self.program_end_sequence.clone() }
/// Output relative distance field if mode was absolute or unknown. /// Output relative distance field if mode was absolute or unknown.
pub fn absolute(&mut self) -> Vec<Command> { pub fn absolute(&mut self) -> Vec<Command> {
if self.distance_mode == Some(Distance::Relative) || self.distance_mode == None { if self.distance_mode == Some(Distance::Relative) || self.distance_mode == None {
@ -83,4 +96,3 @@ impl Machine {
} }
} }
} }

@ -1,5 +1,3 @@
/// TODO: documentation
#[macro_use] #[macro_use]
extern crate clap; extern crate clap;
extern crate env_logger; extern crate env_logger;
@ -26,9 +24,8 @@ use code::*;
use machine::*; use machine::*;
use turtle::*; use turtle::*;
// TODO: Documentation
fn main() -> io::Result<()> { fn main() -> io::Result<()> {
if env::var("RUST_LOG").is_err() { if let Err(_) = env::var("RUST_LOG") {
env::set_var("RUST_LOG", "svg2gcode=info") env::set_var("RUST_LOG", "svg2gcode=info")
} }
env_logger::init(); env_logger::init();
@ -36,12 +33,15 @@ fn main() -> io::Result<()> {
(version: crate_version!()) (version: crate_version!())
(author: crate_authors!()) (author: crate_authors!())
(about: crate_description!()) (about: crate_description!())
(@arg FILE: "Selects the input SVG file to use, else reading from stdin") (@arg FILE: "A file path for an SVG, else reads from stdin")
(@arg tolerance: --tolerance "Sets the interpolation tolerance for curves") (@arg tolerance: --tolerance +takes_value "Curve interpolation tolerance (default: 0.002mm)")
(@arg feedrate: --feedrate "Sets the machine feed rate") (@arg feedrate: --feedrate +takes_value "Machine feed rate in mm/min (default: 300mm/min)")
(@arg dpi: --dpi "Sets the DPI for SVGs with units in pt, pc, etc. (default 72.0)") (@arg dpi: --dpi +takes_value "Dots per inch (DPI) for pixels, points, picas, etc. (default: 96dpi)")
(@arg tool_on_action: --tool_on_action "Sets the tool on GCode sequence") (@arg tool_on_sequence: --on +takes_value +required "Tool on GCode sequence")
(@arg tool_off_action: --tool_off_action "Sets the tool off GCode sequence") (@arg tool_off_sequence: --off +takes_value +required "Tool off GCode sequence")
(@arg begin_sequence: --begin +takes_value "Optional GCode begin sequence (i.e. change to a tool)")
(@arg end_sequence: --end +takes_value "Optional GCode end sequence, prior to program end (i.e. change to a tool)")
(@arg out: --out -o +takes_value "Output file path (overwrites old files), else writes to stdout")
) )
.get_matches(); .get_matches();
@ -54,68 +54,69 @@ fn main() -> io::Result<()> {
input input
} }
None => { None => {
info!("Reading from stdin");
let mut input = String::new(); let mut input = String::new();
io::stdin().read_to_string(&mut input)?; io::stdin().read_to_string(&mut input)?;
input input
} }
}; };
let mut opts = ProgramOptions::default(); let opts = ProgramOptions {
let mut mach = Machine::new(CommandVec::default(), CommandVec::default()); tolerance: matches
.value_of("tolerance")
if let Some(tolerance) = matches.value_of("tolerance").and_then(|x| x.parse().ok()) { .map(|x| x.parse().expect("could not parse tolerance"))
opts.tolerance = tolerance; .unwrap_or(0.002),
} feedrate: matches
if let Some(feedrate) = matches.value_of("feedrate").and_then(|x| x.parse().ok()) { .value_of("feedrate")
opts.feedrate = feedrate; .map(|x| x.parse().expect("could not parse feedrate"))
} .unwrap_or(300.0),
if let Some(dpi) = matches.value_of("dpi").and_then(|x| x.parse().ok()) { dpi: matches
opts.dpi = dpi; .value_of("dpi")
} .map(|x| x.parse().expect("could not parse DPI"))
.unwrap_or(96.0),
};
// if let Some(tool_on_action) = matches.value_of("tool_on_action").filter(validate_gcode) { let mach = Machine::new(
// mach.tool_on_action = vec![GCode::Raw(Box::new(tool_on_action.to_string()))]; matches.value_of("tool_on_sequence").map(parse_gcode).unwrap_or_default(),
// } matches.value_of("tool_off_sequence").map(parse_gcode).unwrap_or_default(),
// if let Some(tool_off_action) = matches.value_of("tool_off_action").filter(validate_gcode) { matches
// mach.tool_off_action = vec![GCode::Raw(Box::new(tool_off_action.to_string()))]; .value_of("begin_sequence")
// } .map(parse_gcode)
.unwrap_or_default(),
matches.value_of("end_sequence").map(parse_gcode).unwrap_or_default(),
);
let doc = svgdom::Document::from_str(&input).expect("Invalid or unsupported SVG file"); let doc = svgdom::Document::from_str(&input).expect("Invalid or unsupported SVG file");
let prog = svg2program(&doc, opts, mach); let prog = svg2program(&doc, opts, mach);
program2gcode(prog, File::create("out.gcode")?) if let Some(out_path) = matches.value_of("out") {
program2gcode(prog, File::create(out_path)?)
} else {
program2gcode(prog, std::io::stdout())
}
} }
// TODO: Documentation /// High-level output options
struct ProgramOptions { struct ProgramOptions {
/// Curve interpolation tolerance in millimeters
tolerance: f64, tolerance: f64,
/// Feedrate in millimeters / minute
feedrate: f64, feedrate: f64,
/// Dots per inch for pixels, picas, points, etc.
dpi: f64, dpi: f64,
} }
// Sets the baseline options for the machine.
impl Default for ProgramOptions {
fn default() -> Self {
ProgramOptions {
tolerance: 0.002, // See https://github.com/gnea/grbl/wiki/Grbl-v1.1-Configuration#12--arc-tolerance-mm
feedrate: 3000.0,
dpi: 72.0,
}
}
}
// TODO: Documentation
// TODO: This function is much too large
fn svg2program(doc: &svgdom::Document, opts: ProgramOptions, mach: Machine) -> Vec<Command> { fn svg2program(doc: &svgdom::Document, opts: ProgramOptions, mach: Machine) -> Vec<Command> {
let mut p = vec![];
let mut t = Turtle::new(mach); let mut t = Turtle::new(mach);
let mut p = vec![
command!(CommandWord::UnitsMillimeters, {}),
command!(CommandWord::FeedRateUnitsPerMinute, {}),
];
let mut namestack: Vec<String> = vec![]; let mut namestack: Vec<String> = vec![];
p.append(&mut t.mach.program_begin());
p.push(command!(CommandWord::UnitsMillimeters, {})); p.append(&mut t.mach.absolute());
p.append(&mut t.mach.tool_off()); p.append(&mut t.move_to(true, 0.0, 0.0));
p.append(&mut
t.move_to(true, 0.0, 0.0));
for edge in doc.root().traverse() { for edge in doc.root().traverse() {
let (node, is_start) = match edge { let (node, is_start) = match edge {
@ -193,8 +194,8 @@ fn svg2program(doc: &svgdom::Document, opts: ProgramOptions, mach: Machine) -> V
for segment in path.iter() { for segment in path.iter() {
p.append(&mut match segment { p.append(&mut match segment {
PathSegment::MoveTo { abs, x, y } => t.move_to(*abs, *x, *y), PathSegment::MoveTo { abs, x, y } => t.move_to(*abs, *x, *y),
PathSegment::ClosePath { abs } => { PathSegment::ClosePath { abs: _ } => {
// Ignore abs, should have identical effect: https://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand // Ignore abs, should have identical effect: [9.3.4. The "closepath" command]("https://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand)
t.close(None, opts.feedrate) t.close(None, opts.feedrate)
} }
PathSegment::LineTo { abs, x, y } => { PathSegment::LineTo { abs, x, y } => {
@ -291,16 +292,17 @@ fn svg2program(doc: &svgdom::Document, opts: ProgramOptions, mach: Machine) -> V
p.append(&mut t.mach.tool_off()); p.append(&mut t.mach.tool_off());
p.append(&mut t.mach.absolute()); p.append(&mut t.mach.absolute());
p.push(command!(CommandWord::RapidPositioning, { p.append(&mut t.move_to(true, 0.0, 0.0));
x: 0.0, p.append(&mut t.mach.program_end());
y: 0.0,
}));
p.push(command!(CommandWord::ProgramEnd, {})); p.push(command!(CommandWord::ProgramEnd, {}));
p p
} }
// TODO: Documentation /// Convenience function for converting absolute lengths to millimeters
/// Absolute lengths are listed in [CSS 4 §6.2](https://www.w3.org/TR/css-values/#absolute-lengths)
/// Relative lengths in [CSS 4 §6.1](https://www.w3.org/TR/css-values/#relative-lengths) are not supported and will cause a panic.
/// A default DPI of 96 is used as per [CSS 4 §7.4](https://www.w3.org/TR/css-values/#resolution), which you can adjust with --dpi
fn length_to_mm(l: svgdom::Length, dpi: f64) -> f64 { fn length_to_mm(l: svgdom::Length, dpi: f64) -> f64 {
use svgdom::LengthUnit::*; use svgdom::LengthUnit::*;
use uom::si::f64::Length; use uom::si::f64::Length;
@ -310,9 +312,16 @@ fn length_to_mm(l: svgdom::Length, dpi: f64) -> f64 {
Cm => Length::new::<centimeter>(l.num), Cm => Length::new::<centimeter>(l.num),
Mm => Length::new::<millimeter>(l.num), Mm => Length::new::<millimeter>(l.num),
In => Length::new::<inch>(l.num), In => Length::new::<inch>(l.num),
Pt => Length::new::<point_printers>(l.num) * dpi / 72.0, // See https://github.com/iliekturtles/uom/blob/5cad47d4e67c902304c4c2b7feeb9c3d34fdffba/src/si/length.rs#L61 Pc => Length::new::<pica_computer>(l.num) * dpi / 96.0,
Pc => Length::new::<pica_printers>(l.num) * dpi / 72.0, // See https://github.com/iliekturtles/uom/blob/5cad47d4e67c902304c4c2b7feeb9c3d34fdffba/src/si/length.rs#L58 Pt => Length::new::<point_computer>(l.num) * dpi / 96.0,
_ => Length::new::<millimeter>(l.num), Px => Length::new::<inch>(l.num * dpi / 96.0),
other => {
warn!(
"Converting from '{:?}' to millimeters is not supported, treating as millimeters",
other
);
Length::new::<millimeter>(l.num)
}
}; };
length.get::<millimeter>() length.get::<millimeter>()

@ -77,13 +77,13 @@ impl Turtle {
if let Some(z) = z { if let Some(z) = z {
linear_interpolation.push(Word { linear_interpolation.push(Word {
letter: 'Z', letter: 'Z',
value: z, value: Value::Float(z),
}); });
} }
if let Some(f) = f { if let Some(f) = f {
linear_interpolation.push(Word { linear_interpolation.push(Word {
letter: 'F', letter: 'F',
value: f, value: Value::Float(f),
}); });
} }
linear_interpolation linear_interpolation

Loading…
Cancel
Save