use crate::library::load_polyline_from_svg_file;
use crate::library::print_svg_polylines;
use crate::library::spiropath;
use crate::library::Location;
use crate::library::Options;
use crate::library::Scale;
use clap::crate_version;
use clap::App;
use clap::Arg;
use clap::ArgMatches;
use std::f64::consts::PI;
use std::fs::File;
use std::io::stdout;
use std::io::BufWriter;
use svg2polylines::CoordinatePair as Point;
use svg2polylines::Polyline;
pub fn main(flags: &[String]) {
let arg_matches = app().get_matches_from(flags);
let mut options = arg_options(&arg_matches);
options.validate();
let stationary = parse_polyline(
&arg_matches,
"stationary",
options.stationary_teeth,
options.tolerance,
);
let rotating = parse_polyline(
&arg_matches,
"rotating",
options.rotating_teeth,
options.tolerance,
);
let (polylines, ids) = spiropath(stationary, rotating, options);
let output_path = arg_matches.value_of("output").unwrap();
if output_path == "-" {
print_svg_polylines(&polylines, &ids, &mut BufWriter::new(stdout())); } else {
print_svg_polylines(
&polylines,
&ids,
&mut BufWriter::new(
File::create(output_path)
.unwrap_or_else(|error| panic!("{} creating output: {}", error, output_path)),
),
);
};
}
fn app() -> App<'static, 'static> {
App::new("spiropath")
.about("\nGeneralized Spirograph using arbitrary SVG paths.")
.after_help(
"\
PROCESS:\n\
\n\
- Take as input a stationary SVG path and a rotating SVG path;\n \
due to svg2polylines limitations, arcs are not supported.\n \
Or, generate a circle or a regular polygon instead.\n\
\n\
- Scale these so their total length is an integer,\n \
equivalent to a number of virtual gear \"teeth\".\n\
\n\
- Pick the top-most point of the stationary path,\n \
and the top-most point of the (rotated) rotating path.\n \
Position the rotating path so its top point is at some offset(s)\n \
relative to the top-point of the stationary path.\n\
\n\
- Rotate the rotating path around the stationary path,\n \
until it returns to its start position.\n \
Ignore collisions; consider only the contact point.\n\
\n\
- Generate the path traced by a rotating point,\n \
which is fixed relative to the rotating path.\n \
Allow this point to be outside the rotating path.\n\
\n\
- Optionally also include the stationary path.\n\
\n\
- Scale the result; coordinates are in SVG \"pt\" units.\n\
\n\
- Print this as SVG file.\
",
)
.version(crate_version!())
.arg(
Arg::with_name("stationary")
.long("stationary")
.short("s")
.value_name("FILE or CIRCLE or COUNT")
.help(
"SVG file containing the rotating path,\n\
CIRCLE for generating a radius-100 circle,\n\
or the number of sides for a radius-100 polygon:\n\
2 - line, 3 - triangle, 4 - square, etc.\n",
)
.default_value("CIRCLE"),
)
.arg(
Arg::with_name("rotating")
.long("rotating")
.short("r")
.value_name("FILE or CIRCLE or COUNT")
.help(
"SVG file containing the rotating path,\n\
CIRCLE for generating a radius-100 circle,\n\
or the number of sides for a radius-100 polygon:\n\
2 - line, 3 - triangle, 4 - square, etc.\n",
)
.default_value("CIRCLE"),
)
.arg(
Arg::with_name("output")
.long("output")
.short("o")
.value_name("FILE")
.help("SVG file to write the output spiropath into;\nspecify \"-\" for STDOUT")
.default_value("-"),
)
.arg(
Arg::with_name("location")
.long("location")
.short("L")
.value_name("SIDE")
.possible_values(&["inside", "outside"])
.help("Location of the rotating path relative to the stationary path\n")
.default_value("outside"),
)
.arg(
Arg::with_name("stationary-teeth")
.long("stationary-teeth")
.short("S")
.value_name("COUNT")
.help(
"Number of virtual teeth on (total length of)\n\
the stationary path",
)
.default_value("47"),
)
.arg(
Arg::with_name("rotating-teeth")
.long("rotating-teeth")
.short("R")
.value_name("COUNT")
.help(
"Number of virtual teeth on (total length of)\n\
the rotating path",
)
.default_value("7"),
)
.arg(
Arg::with_name("tolerance")
.long("tolerance")
.short("T")
.value_name("FRACTION")
.help(
"Linear path approximation tolerance,\n\
as a fraction of the teeth size",
)
.default_value("0.001"),
)
.arg(
Arg::with_name("point")
.long("point")
.short("P")
.help(
"Coordinates of the traced point,\n\
relative to the raw rotating path\n",
)
.value_name("X,Y")
.number_of_values(2)
.require_delimiter(true)
.default_value("0,100"),
)
.arg(
Arg::with_name("angle")
.long("angle")
.short("A")
.value_name("DEGREES")
.help(
"Angle to use for picking the top point of the rotating path,\n\
in degrees",
)
.default_value("0"),
)
.arg(
Arg::with_name("offset")
.long("offset")
.short("O")
.value_name("FRACTION(S)")
.help(
"Offset(s) of start position(s) of rotating path,\n\
relative to top point of stationary path,\n\
as a fraction of the teeth size;\n\
repeat for including multiple paths in the output\n",
)
.multiple(true)
.use_delimiter(true)
.min_values(1)
.default_value("0"),
)
.arg(
Arg::with_name("x-scale")
.long("x-scale")
.short("X")
.value_name("SCALE")
.help(
"Scaling of the output SVG, one of:\n\
\"<size>pt\" / \"<factor>x\" / \"same\" as y-scale\n",
)
.default_value("1x"),
)
.arg(
Arg::with_name("y-scale")
.long("y-scale")
.short("Y")
.value_name("SCALE")
.help(
"Scaling of the output SVG, one of:\n\
\"<size>pt\" / \"<factor>x\" / \"same\" as x-scale\n",
)
.default_value("1x"),
)
.arg(
Arg::with_name("mirror-rotating")
.long("mirror-rotating")
.short("M")
.help("Mirror the rotating path"),
)
.arg(
Arg::with_name("include-stationary")
.long("include-stationary")
.short("I")
.help("Include the stationary path in the output"),
)
.arg(
Arg::with_name("duration")
.long("duration")
.short("D")
.value_name("SECONDS")
.help("Duration of animation in seconds, or 0 for none")
.default_value("0"),
)
}
fn arg_options(arg_matches: &ArgMatches) -> Options {
let location = parse_location(arg_matches, "location");
let stationary_teeth = parse_count(arg_matches, "stationary-teeth");
let rotating_teeth = parse_count(arg_matches, "rotating-teeth");
let tolerance = parse_fraction(arg_matches, "tolerance");
let traced_point = parse_point(arg_matches, "point");
let initial_rotating_angle = parse_value(arg_matches, "angle");
let mirror_rotating = arg_matches.is_present("mirror-rotating");
let include_stationary = arg_matches.is_present("include-stationary");
let initial_offsets = parse_offsets(arg_matches, "offset", stationary_teeth as f64);
let x_scale = parse_scale(arg_matches, "x-scale");
let y_scale = parse_scale(arg_matches, "y-scale");
let duration = parse_duration(arg_matches, "duration");
Options {
location,
stationary_teeth,
rotating_teeth,
tolerance,
traced_point,
initial_rotating_angle,
mirror_rotating,
include_stationary,
initial_offsets,
x_scale,
y_scale,
duration,
}
}
fn parse_location(arg_matches: &ArgMatches, name: &str) -> Location {
match arg_matches.value_of(name).unwrap() {
"inside" => Location::Inside,
"outside" => Location::Outside,
_ => unreachable!(),
}
}
fn parse_count(arg_matches: &ArgMatches, name: &str) -> usize {
let value = arg_matches
.value_of(name)
.unwrap()
.parse::<usize>()
.unwrap_or_else(|error| {
panic!(
"{} in {}: {}",
error,
name,
arg_matches.value_of(name).unwrap()
)
});
if value == 0 {
panic!("{} is zero", name); }
value
}
fn parse_fraction(arg_matches: &ArgMatches, name: &str) -> f64 {
let value = parse_value(arg_matches, name);
if value <= 0.0 {
panic!("{}: {} is not positive", name, value); }
value
}
fn parse_duration(arg_matches: &ArgMatches, name: &str) -> f64 {
let value = parse_value(arg_matches, name);
if value < 0.0 {
panic!("{}: {} is negative", name, value); }
value
}
fn parse_value(arg_matches: &ArgMatches, name: &str) -> f64 {
arg_matches
.value_of(name)
.unwrap()
.parse::<f64>()
.unwrap_or_else(|error| {
panic!(
"{} in {}: {}",
error,
name,
arg_matches.value_of(name).unwrap()
)
})
}
fn parse_point(arg_matches: &ArgMatches, name: &str) -> Point {
let coordinates: Vec<f64> = parse_values(arg_matches, name);
assert!(coordinates.len() == 2);
Point {
x: coordinates[0],
y: coordinates[1],
}
}
fn parse_values(arg_matches: &ArgMatches, name: &str) -> Vec<f64> {
arg_matches
.values_of(name)
.unwrap()
.map(|string| {
string
.parse::<f64>()
.unwrap_or_else(|error| panic!("{} in {}: {}", error, name, string))
})
.collect()
}
fn parse_offsets(arg_matches: &ArgMatches, name: &str, stationary_teeth: f64) -> Vec<f64> {
let mut offsets = parse_values(arg_matches, name);
for offset in offsets.iter_mut() {
while *offset < 0.0 {
*offset += stationary_teeth; }
while *offset > stationary_teeth {
*offset -= stationary_teeth; }
if *offset >= stationary_teeth {
panic!(
"{}: {} is not less than stationary-teeth: {}",
name, offset, stationary_teeth
);
}
}
offsets
}
fn parse_scale(arg_matches: &ArgMatches, name: &str) -> Scale {
match arg_matches.value_of(name).unwrap() {
"same" => Scale::Same,
value if value.ends_with('x') => Scale::Factor(
value[..(value.len() - 1)]
.parse::<f64>()
.unwrap_or_else(|error| panic!("{} in {}: {}", error, name, value)),
),
value if value.ends_with("pt") => Scale::Size(
value[..(value.len() - 2)]
.parse::<f64>()
.unwrap_or_else(|error| panic!("{} in {}: {}", error, name, value)),
),
value => panic!("invalid {}: {}", name, value), }
}
fn parse_polyline(arg_matches: &ArgMatches, name: &str, teeth: usize, tolerance: f64) -> Polyline {
let value = arg_matches.value_of(name).unwrap();
if value == "CIRCLE" {
let max_step_angle = 2.0 * (1.0 - tolerance / 100.0).acos();
let minimal_teeth = 2.0 * PI / max_step_angle;
let factor = (minimal_teeth / teeth as f64).ceil();
regular_polyline(teeth * factor as usize)
} else if let Result::Ok(sides) = value.parse::<usize>() {
if sides < 2 {
panic!("{} sides: {} are less than 2", name, sides); }
regular_polyline(sides)
} else {
load_polyline_from_svg_file(value, tolerance)
}
}
fn regular_polyline(sides: usize) -> Polyline {
let angle = 2.0 * PI / (sides as f64);
let mut polyline = vec![Point { x: 0.0, y: 0.0 }; sides];
for (side, point) in polyline.iter_mut().enumerate() {
point.x = 100.0 * (angle * side as f64).sin();
point.y = 100.0 * (angle * side as f64).cos();
}
polyline
}