pub mod abs_to_rel;
pub mod normalize;
pub mod rel_to_abs;
use crate::Plugin;
use anyhow::Result;
use lyon::geom::{CubicBezierSegment, Point, QuadraticBezierSegment, Vector};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use vexy_vsvg::ast::{Document, Element, Node};
const DEFAULT_FLOAT_PRECISION: u8 = 3;
const DEFAULT_TRANSFORM_PRECISION: u8 = 5;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConvertPathDataConfig {
#[serde(default = "default_float_precision")]
pub float_precision: u8,
#[serde(default = "default_transform_precision")]
pub transform_precision: u8,
#[serde(default = "default_true")]
pub remove_useless: bool,
#[serde(default = "default_true")]
pub collapse_repeated: bool,
#[serde(default = "default_true")]
pub utilize_absolute: bool,
#[serde(default = "default_true")]
pub leading_zero: bool,
#[serde(default = "default_true")]
pub negative_extra_space: bool,
#[serde(default)]
pub make_arcs: MakeArcsConfig,
#[serde(default = "default_true")]
pub straight_curves: bool,
#[serde(default = "default_true")]
pub line_shorthands: bool,
#[serde(default = "default_false")]
pub convert_to_q: bool,
#[serde(default = "default_curve_tolerance")]
pub curve_tolerance: f64,
#[serde(default = "default_arc_tolerance")]
pub arc_tolerance: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MakeArcsConfig {
Enabled(bool),
Params(MakeArcsParams),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct MakeArcsParams {
#[serde(default = "default_make_arcs_threshold")]
pub threshold: f64,
#[serde(default = "default_make_arcs_tolerance")]
pub tolerance: f64,
}
impl MakeArcsConfig {
fn is_enabled(&self) -> bool {
match self {
Self::Enabled(enabled) => *enabled,
Self::Params(_) => true,
}
}
fn tolerance(&self, default_tolerance: f64) -> f64 {
match self {
Self::Params(params) => params.tolerance,
Self::Enabled(_) => default_tolerance,
}
}
}
impl Default for MakeArcsConfig {
fn default() -> Self {
Self::Enabled(false)
}
}
fn default_float_precision() -> u8 {
DEFAULT_FLOAT_PRECISION
}
fn default_transform_precision() -> u8 {
DEFAULT_TRANSFORM_PRECISION
}
fn default_true() -> bool {
true
}
fn default_false() -> bool {
false
}
fn default_curve_tolerance() -> f64 {
0.001
}
fn default_arc_tolerance() -> f64 {
0.5
}
fn default_make_arcs_threshold() -> f64 {
2.5
}
fn default_make_arcs_tolerance() -> f64 {
0.5
}
impl Default for ConvertPathDataConfig {
fn default() -> Self {
Self {
float_precision: default_float_precision(),
transform_precision: default_transform_precision(),
remove_useless: true,
collapse_repeated: true,
utilize_absolute: true,
leading_zero: false,
negative_extra_space: false,
make_arcs: MakeArcsConfig::default(),
straight_curves: true,
line_shorthands: true,
convert_to_q: false,
curve_tolerance: default_curve_tolerance(),
arc_tolerance: default_arc_tolerance(),
}
}
}
pub struct ConvertPathDataPlugin {
config: ConvertPathDataConfig,
}
impl ConvertPathDataPlugin {
pub fn new() -> Self {
Self {
config: ConvertPathDataConfig::default(),
}
}
pub fn with_config(config: ConvertPathDataConfig) -> Self {
Self { config }
}
fn parse_config(params: &Value) -> Result<ConvertPathDataConfig> {
if params.is_null() {
Ok(ConvertPathDataConfig::default())
} else {
serde_json::from_value(params.clone())
.map_err(|e| anyhow::anyhow!("Invalid plugin configuration: {}", e))
}
}
fn optimize_paths_in_element(&self, element: &mut Element) {
if element.name == "path" {
if let Some(d) = element.attr("d") {
match optimize_path_data(d, &self.config) {
Ok(optimized) => {
element.set_attr("d", &optimized);
}
Err(e) => {
eprintln!("Warning: Failed to optimize path data: {}", e);
}
}
}
}
let mut i = 0;
while i < element.children.len() {
if let Node::Element(child) = &mut element.children[i] {
self.optimize_paths_in_element(child);
}
i += 1;
}
}
}
impl Default for ConvertPathDataPlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for ConvertPathDataPlugin {
fn name(&self) -> &'static str {
"convertPathData"
}
fn description(&self) -> &'static str {
"converts path data to relative or absolute, optimizes segments, simplifies curves"
}
fn validate_params(&self, params: &Value) -> Result<()> {
Self::parse_config(params)?;
Ok(())
}
fn configure(&mut self, params: &Value) -> Result<()> {
self.config = Self::parse_config(params)?;
Ok(())
}
fn apply(&self, document: &mut Document) -> Result<()> {
self.optimize_paths_in_element(&mut document.root);
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum CommandType {
MoveTo,
LineTo,
HorizontalLineTo,
VerticalLineTo,
CurveTo,
SmoothCurveTo,
QuadraticBezier,
SmoothQuadraticBezier,
Arc,
ClosePath,
}
#[derive(Debug, Clone)]
struct PathCommand {
cmd_type: CommandType,
is_absolute: bool,
params: Vec<f64>,
}
impl PathCommand {
fn get_char(&self) -> char {
match (self.cmd_type, self.is_absolute) {
(CommandType::MoveTo, true) => 'M',
(CommandType::MoveTo, false) => 'm',
(CommandType::LineTo, true) => 'L',
(CommandType::LineTo, false) => 'l',
(CommandType::HorizontalLineTo, true) => 'H',
(CommandType::HorizontalLineTo, false) => 'h',
(CommandType::VerticalLineTo, true) => 'V',
(CommandType::VerticalLineTo, false) => 'v',
(CommandType::CurveTo, true) => 'C',
(CommandType::CurveTo, false) => 'c',
(CommandType::SmoothCurveTo, true) => 'S',
(CommandType::SmoothCurveTo, false) => 's',
(CommandType::QuadraticBezier, true) => 'Q',
(CommandType::QuadraticBezier, false) => 'q',
(CommandType::SmoothQuadraticBezier, true) => 'T',
(CommandType::SmoothQuadraticBezier, false) => 't',
(CommandType::Arc, true) => 'A',
(CommandType::Arc, false) => 'a',
(CommandType::ClosePath, _) => 'z',
}
}
}
fn parse_path_data(path_data: &str) -> Result<Vec<PathCommand>> {
let mut commands = Vec::new();
let mut chars = path_data.chars().peekable();
let mut current_nums = Vec::new();
let mut current_num = String::new();
let mut last_cmd_type = None;
let mut in_number = false;
let mut in_exponent = false;
let mut seen_decimal = false;
for ch in chars.by_ref() {
match ch {
'M' | 'm' | 'L' | 'l' | 'H' | 'h' | 'V' | 'v' | 'C' | 'c' | 'S' | 's' | 'Q' | 'q'
| 'T' | 't' | 'A' | 'a' | 'Z' | 'z' => {
if !current_num.is_empty() {
if let Ok(num) = current_num.parse::<f64>() {
current_nums.push(num);
}
current_num.clear();
in_number = false;
in_exponent = false;
seen_decimal = false;
}
if let Some(cmd_type) = last_cmd_type {
process_accumulated_params(&mut commands, cmd_type, &mut current_nums)?;
}
let (cmd_type, is_absolute) = parse_command_char(ch)?;
if cmd_type == CommandType::ClosePath {
commands.push(PathCommand {
cmd_type,
is_absolute: true,
params: vec![],
});
last_cmd_type = None;
} else {
last_cmd_type = Some((cmd_type, is_absolute));
}
}
'0'..='9' | '.' | '-' | '+' | 'e' | 'E' => {
if ch == '-' || ch == '+' {
if !current_num.is_empty() && in_number && !in_exponent {
if let Ok(num) = current_num.parse::<f64>() {
current_nums.push(num);
}
current_num.clear();
in_exponent = false;
seen_decimal = false;
}
current_num.push(ch);
} else if ch == '.' {
if !current_num.is_empty() && in_number && seen_decimal && !in_exponent {
if let Ok(num) = current_num.parse::<f64>() {
current_nums.push(num);
}
current_num.clear();
in_exponent = false;
}
current_num.push(ch);
seen_decimal = true;
} else if ch == 'e' || ch == 'E' {
current_num.push(ch);
in_exponent = true;
} else {
current_num.push(ch);
}
in_number = true;
}
' ' | ',' | '\t' | '\n' | '\r' => {
if !current_num.is_empty() {
if let Ok(num) = current_num.parse::<f64>() {
current_nums.push(num);
}
current_num.clear();
in_number = false;
in_exponent = false;
seen_decimal = false;
}
}
_ => {}
}
}
if !current_num.is_empty() {
if let Ok(num) = current_num.parse::<f64>() {
current_nums.push(num);
}
}
if let Some(cmd_type) = last_cmd_type {
process_accumulated_params(&mut commands, cmd_type, &mut current_nums)?;
}
Ok(commands)
}
fn parse_command_char(ch: char) -> Result<(CommandType, bool)> {
match ch {
'M' => Ok((CommandType::MoveTo, true)),
'm' => Ok((CommandType::MoveTo, false)),
'L' => Ok((CommandType::LineTo, true)),
'l' => Ok((CommandType::LineTo, false)),
'H' => Ok((CommandType::HorizontalLineTo, true)),
'h' => Ok((CommandType::HorizontalLineTo, false)),
'V' => Ok((CommandType::VerticalLineTo, true)),
'v' => Ok((CommandType::VerticalLineTo, false)),
'C' => Ok((CommandType::CurveTo, true)),
'c' => Ok((CommandType::CurveTo, false)),
'S' => Ok((CommandType::SmoothCurveTo, true)),
's' => Ok((CommandType::SmoothCurveTo, false)),
'Q' => Ok((CommandType::QuadraticBezier, true)),
'q' => Ok((CommandType::QuadraticBezier, false)),
'T' => Ok((CommandType::SmoothQuadraticBezier, true)),
't' => Ok((CommandType::SmoothQuadraticBezier, false)),
'A' => Ok((CommandType::Arc, true)),
'a' => Ok((CommandType::Arc, false)),
'Z' | 'z' => Ok((CommandType::ClosePath, true)),
_ => Err(anyhow::anyhow!("Unknown command character: {}", ch)),
}
}
fn process_accumulated_params(
commands: &mut Vec<PathCommand>,
(cmd_type, is_absolute): (CommandType, bool),
params: &mut Vec<f64>,
) -> Result<()> {
let expected = match cmd_type {
CommandType::MoveTo | CommandType::LineTo => 2,
CommandType::HorizontalLineTo | CommandType::VerticalLineTo => 1,
CommandType::CurveTo => 6,
CommandType::SmoothCurveTo | CommandType::QuadraticBezier => 4,
CommandType::SmoothQuadraticBezier => 2,
CommandType::Arc => 7,
CommandType::ClosePath => 0,
};
if expected == 0 {
return Ok(());
}
let mut is_first_chunk = true;
while params.len() >= expected {
let chunk: Vec<f64> = params.drain(..expected).collect();
let actual_cmd_type = if cmd_type == CommandType::MoveTo && !is_first_chunk {
CommandType::LineTo
} else {
cmd_type
};
commands.push(PathCommand {
cmd_type: actual_cmd_type,
is_absolute,
params: chunk,
});
is_first_chunk = false;
}
if !params.is_empty() {
params.clear();
}
Ok(())
}
fn optimize_path_data(path_data: &str, config: &ConvertPathDataConfig) -> Result<String> {
let mut commands = parse_path_data(path_data)?;
let mut current_x = 0.0;
let mut current_y = 0.0;
let mut start_x = 0.0;
let mut start_y = 0.0;
for cmd in &mut commands {
match cmd.cmd_type {
CommandType::MoveTo => {
if !cmd.is_absolute && cmd.params.len() >= 2 {
cmd.params[0] += current_x;
cmd.params[1] += current_y;
cmd.is_absolute = true;
}
if cmd.params.len() >= 2 {
current_x = cmd.params[0];
current_y = cmd.params[1];
start_x = current_x;
start_y = current_y;
}
}
CommandType::LineTo => {
if !cmd.is_absolute && cmd.params.len() >= 2 {
cmd.params[0] += current_x;
cmd.params[1] += current_y;
cmd.is_absolute = true;
}
if cmd.params.len() >= 2 {
current_x = cmd.params[0];
current_y = cmd.params[1];
}
}
CommandType::HorizontalLineTo => {
if !cmd.is_absolute && !cmd.params.is_empty() {
cmd.params[0] += current_x;
cmd.is_absolute = true;
}
if !cmd.params.is_empty() {
current_x = cmd.params[0];
}
}
CommandType::VerticalLineTo => {
if !cmd.is_absolute && !cmd.params.is_empty() {
cmd.params[0] += current_y;
cmd.is_absolute = true;
}
if !cmd.params.is_empty() {
current_y = cmd.params[0];
}
}
CommandType::CurveTo => {
if !cmd.is_absolute && cmd.params.len() >= 6 {
cmd.params[0] += current_x;
cmd.params[1] += current_y;
cmd.params[2] += current_x;
cmd.params[3] += current_y;
cmd.params[4] += current_x;
cmd.params[5] += current_y;
cmd.is_absolute = true;
}
if cmd.params.len() >= 6 {
current_x = cmd.params[4];
current_y = cmd.params[5];
}
}
CommandType::SmoothCurveTo => {
if !cmd.is_absolute && cmd.params.len() >= 4 {
cmd.params[0] += current_x;
cmd.params[1] += current_y;
cmd.params[2] += current_x;
cmd.params[3] += current_y;
cmd.is_absolute = true;
}
if cmd.params.len() >= 4 {
current_x = cmd.params[2];
current_y = cmd.params[3];
}
}
CommandType::QuadraticBezier => {
if !cmd.is_absolute && cmd.params.len() >= 4 {
cmd.params[0] += current_x;
cmd.params[1] += current_y;
cmd.params[2] += current_x;
cmd.params[3] += current_y;
cmd.is_absolute = true;
}
if cmd.params.len() >= 4 {
current_x = cmd.params[2];
current_y = cmd.params[3];
}
}
CommandType::SmoothQuadraticBezier => {
if !cmd.is_absolute && cmd.params.len() >= 2 {
cmd.params[0] += current_x;
cmd.params[1] += current_y;
cmd.is_absolute = true;
}
if cmd.params.len() >= 2 {
current_x = cmd.params[0];
current_y = cmd.params[1];
}
}
CommandType::Arc => {
if !cmd.is_absolute && cmd.params.len() >= 7 {
cmd.params[5] += current_x;
cmd.params[6] += current_y;
cmd.is_absolute = true;
}
if cmd.params.len() >= 7 {
current_x = cmd.params[5];
current_y = cmd.params[6];
}
}
CommandType::ClosePath => {
current_x = start_x;
current_y = start_y;
}
}
}
if config.remove_useless {
commands = remove_useless_commands(commands);
}
if config.collapse_repeated {
commands = collapse_repeated_commands(commands);
}
if config.straight_curves {
commands = straighten_curves(commands, config.curve_tolerance);
}
if config.line_shorthands {
commands = apply_line_shorthands(commands);
}
if config.convert_to_q {
commands = convert_cubic_to_quadratic(commands, config.curve_tolerance);
}
if config.make_arcs.is_enabled() {
commands =
convert_curves_to_arcs(commands, config.make_arcs.tolerance(config.arc_tolerance));
}
stringify_commands(
&commands,
config.float_precision,
config.utilize_absolute,
config.leading_zero,
config.negative_extra_space,
)
}
fn remove_useless_commands(mut commands: Vec<PathCommand>) -> Vec<PathCommand> {
let mut result = Vec::new();
let mut current_x = 0.0;
let mut current_y = 0.0;
let mut start_x = 0.0;
let mut start_y = 0.0;
for cmd in commands.drain(..) {
let mut keep = true;
match cmd.cmd_type {
CommandType::LineTo => {
if cmd.params.len() >= 2 {
if (cmd.params[0] - current_x).abs() < f64::EPSILON
&& (cmd.params[1] - current_y).abs() < f64::EPSILON
{
keep = false;
} else {
current_x = cmd.params[0];
current_y = cmd.params[1];
}
}
}
CommandType::HorizontalLineTo => {
if !cmd.params.is_empty() {
if (cmd.params[0] - current_x).abs() < f64::EPSILON {
keep = false;
} else {
current_x = cmd.params[0];
}
}
}
CommandType::VerticalLineTo => {
if !cmd.params.is_empty() {
if (cmd.params[0] - current_y).abs() < f64::EPSILON {
keep = false;
} else {
current_y = cmd.params[0];
}
}
}
CommandType::MoveTo => {
if cmd.params.len() >= 2 {
current_x = cmd.params[0];
current_y = cmd.params[1];
start_x = current_x;
start_y = current_y;
}
}
CommandType::CurveTo => {
if cmd.params.len() >= 6 {
current_x = cmd.params[4];
current_y = cmd.params[5];
}
}
CommandType::SmoothCurveTo => {
if cmd.params.len() >= 4 {
current_x = cmd.params[2];
current_y = cmd.params[3];
}
}
CommandType::QuadraticBezier => {
if cmd.params.len() >= 4 {
current_x = cmd.params[2];
current_y = cmd.params[3];
}
}
CommandType::SmoothQuadraticBezier => {
if cmd.params.len() >= 2 {
current_x = cmd.params[0];
current_y = cmd.params[1];
}
}
CommandType::Arc => {
if cmd.params.len() >= 7 {
current_x = cmd.params[5];
current_y = cmd.params[6];
}
}
CommandType::ClosePath => {
current_x = start_x;
current_y = start_y;
}
}
if keep {
result.push(cmd);
}
}
result
}
fn collapse_repeated_commands(commands: Vec<PathCommand>) -> Vec<PathCommand> {
if commands.is_empty() {
return commands;
}
let len = commands.len();
let mut result = Vec::with_capacity(len);
let mut current_x = 0.0;
let mut current_y = 0.0;
let mut start_x = 0.0;
let mut start_y = 0.0;
let mut is_first = true;
for cmd in commands {
if !is_first && can_collapse_commands(&result[result.len() - 1], &cmd) {
let rlen = result.len();
let prev_cmd = &mut result[rlen - 1];
match cmd.cmd_type {
CommandType::MoveTo => {
if cmd.params.len() >= 2 {
prev_cmd.params[0] += cmd.params[0];
prev_cmd.params[1] += cmd.params[1];
}
}
CommandType::LineTo => {
if cmd.params.len() >= 2 && !cmd.is_absolute {
prev_cmd.params[0] += cmd.params[0];
prev_cmd.params[1] += cmd.params[1];
} else {
result.push(cmd);
}
}
CommandType::HorizontalLineTo => {
if !cmd.params.is_empty() {
prev_cmd.params[0] += cmd.params[0];
}
}
CommandType::VerticalLineTo => {
if !cmd.params.is_empty() {
prev_cmd.params[0] += cmd.params[0];
}
}
_ => {
result.push(cmd);
}
}
} else {
result.push(cmd);
}
is_first = false;
if let Some(last_cmd) = result.last() {
update_position(
last_cmd,
&mut current_x,
&mut current_y,
&mut start_x,
&mut start_y,
);
}
}
result
}
fn can_collapse_commands(prev: &PathCommand, current: &PathCommand) -> bool {
if prev.cmd_type != current.cmd_type {
return false;
}
if prev.is_absolute != current.is_absolute {
return false;
}
match current.cmd_type {
CommandType::MoveTo => {
true
}
CommandType::LineTo => {
!current.is_absolute
}
CommandType::HorizontalLineTo => {
if prev.params.is_empty() || current.params.is_empty() {
return false;
}
(prev.params[0] >= 0.0) == (current.params[0] >= 0.0)
}
CommandType::VerticalLineTo => {
if prev.params.is_empty() || current.params.is_empty() {
return false;
}
(prev.params[0] >= 0.0) == (current.params[0] >= 0.0)
}
_ => false, }
}
#[inline(always)]
fn needs_separator(prev_has_dot: bool, next_first_byte: u8) -> bool {
if next_first_byte == b'-' {
return false;
}
if next_first_byte == b'.' {
return !prev_has_dot;
}
true
}
#[inline]
fn format_number_to_buf(value: f64, precision: u8, buf: &mut String) -> (bool, u8) {
buf.clear();
let factor = 10_f64.powi(i32::from(precision));
let rounded = (value * factor).round() / factor;
if rounded.fract() == 0.0 && rounded.abs() < 1e15 {
#[allow(clippy::cast_possible_truncation)]
let int_val = rounded as i64;
use std::fmt::Write;
let _ = write!(buf, "{int_val}");
let first = buf.as_bytes().first().copied().unwrap_or(b'0');
return (false, first);
}
let mut ryu_buf = ryu::Buffer::new();
let ryu_str = ryu_buf.format(rounded);
if ryu_str.contains('e') || ryu_str.contains('E') {
use std::fmt::Write;
let _ = write!(buf, "{rounded:.prec$}", prec = precision as usize);
let trimmed_len = buf.trim_end_matches('0').trim_end_matches('.').len();
buf.truncate(trimmed_len);
} else {
buf.push_str(ryu_str);
if let Some(dot_pos) = buf.find('.') {
let max_len = dot_pos + 1 + precision as usize;
if buf.len() > max_len {
buf.truncate(max_len);
}
let trimmed_len = buf.trim_end_matches('0').trim_end_matches('.').len();
buf.truncate(trimmed_len);
}
}
if buf.is_empty() || buf == "-" {
buf.clear();
buf.push('0');
return (false, b'0');
}
if buf.starts_with("0.") {
buf.remove(0);
} else if buf.starts_with("-0.") {
buf.remove(1);
}
let has_dot = buf.contains('.');
let first = buf.as_bytes().first().copied().unwrap_or(b'0');
(has_dot, first)
}
#[inline]
fn count_formatted_params_len(params: &[f64], precision: u8) -> usize {
let mut buf = String::with_capacity(24);
let mut total_len: usize = 0;
let mut prev_has_dot = false;
for (i, ¶m) in params.iter().enumerate() {
let (has_dot, first_byte) = format_number_to_buf(param, precision, &mut buf);
if i > 0 && needs_separator(prev_has_dot, first_byte) {
total_len += 1;
}
total_len += buf.len();
prev_has_dot = has_dot;
}
total_len
}
fn stringify_commands(
commands: &[PathCommand],
precision: u8,
utilize_absolute: bool,
_leading_zero: bool,
_negative_extra_space: bool,
) -> Result<String> {
let param_count: usize = commands.iter().map(|c| c.params.len()).sum();
let mut result = String::with_capacity(param_count * 8 + commands.len() * 2);
let mut last_cmd_char = 0u8;
let mut current_x = 0.0_f64;
let mut current_y = 0.0_f64;
let mut start_x = 0.0_f64;
let mut start_y = 0.0_f64;
let mut prev_has_dot = false;
let mut num_buf = String::with_capacity(24);
for (i, cmd) in commands.iter().enumerate() {
let use_absolute = if utilize_absolute && i > 0 {
should_use_absolute_fast(cmd, current_x, current_y, precision)
} else {
cmd.is_absolute
};
let cmd_char = if use_absolute {
cmd.get_char().to_ascii_uppercase() as u8
} else {
cmd.get_char().to_ascii_lowercase() as u8
};
let is_implicit_repeat;
if cmd_char != last_cmd_char || cmd.cmd_type == CommandType::MoveTo {
result.push(cmd_char as char);
last_cmd_char = cmd_char;
prev_has_dot = false;
is_implicit_repeat = false;
} else {
is_implicit_repeat = true;
}
let params = &cmd.params;
let param_count_for_cmd = params.len();
#[allow(clippy::needless_range_loop)]
for j in 0..param_count_for_cmd {
let raw_val = params[j];
let param_val = if use_absolute {
raw_val
} else {
compute_relative_param(cmd.cmd_type, j, raw_val, current_x, current_y)
};
let (has_dot, first_byte) = format_number_to_buf(param_val, precision, &mut num_buf);
if (j > 0 || is_implicit_repeat) && needs_separator(prev_has_dot, first_byte) {
result.push(' ');
}
result.push_str(&num_buf);
prev_has_dot = has_dot;
}
update_position(
cmd,
&mut current_x,
&mut current_y,
&mut start_x,
&mut start_y,
);
}
Ok(result)
}
#[inline(always)]
#[allow(clippy::manual_is_multiple_of)]
fn compute_relative_param(
cmd_type: CommandType,
param_index: usize,
absolute_val: f64,
current_x: f64,
current_y: f64,
) -> f64 {
match cmd_type {
CommandType::MoveTo | CommandType::LineTo => {
if param_index % 2 == 0 {
absolute_val - current_x
} else {
absolute_val - current_y
}
}
CommandType::HorizontalLineTo => absolute_val - current_x,
CommandType::VerticalLineTo => absolute_val - current_y,
CommandType::CurveTo => {
if param_index % 2 == 0 {
absolute_val - current_x
} else {
absolute_val - current_y
}
}
CommandType::SmoothCurveTo | CommandType::QuadraticBezier => {
if param_index % 2 == 0 {
absolute_val - current_x
} else {
absolute_val - current_y
}
}
CommandType::SmoothQuadraticBezier => {
if param_index % 2 == 0 {
absolute_val - current_x
} else {
absolute_val - current_y
}
}
CommandType::Arc => {
match param_index % 7 {
5 => absolute_val - current_x,
6 => absolute_val - current_y,
_ => absolute_val,
}
}
CommandType::ClosePath => absolute_val,
}
}
fn should_use_absolute_fast(
cmd: &PathCommand,
current_x: f64,
current_y: f64,
precision: u8,
) -> bool {
let abs_len = count_formatted_params_len(&cmd.params, precision);
let mut buf = String::with_capacity(24);
let mut rel_len: usize = 0;
let mut prev_has_dot = false;
for (j, &raw_val) in cmd.params.iter().enumerate() {
let rel_val = compute_relative_param(cmd.cmd_type, j, raw_val, current_x, current_y);
let (has_dot, first_byte) = format_number_to_buf(rel_val, precision, &mut buf);
if j > 0 && needs_separator(prev_has_dot, first_byte) {
rel_len += 1;
}
rel_len += buf.len();
prev_has_dot = has_dot;
}
abs_len <= rel_len
}
#[allow(clippy::manual_is_multiple_of)]
fn update_position(
cmd: &PathCommand,
current_x: &mut f64,
current_y: &mut f64,
start_x: &mut f64,
start_y: &mut f64,
) {
let abs = cmd.is_absolute;
match cmd.cmd_type {
CommandType::MoveTo => {
if cmd.params.len() >= 2 {
if abs {
*current_x = cmd.params[0];
*current_y = cmd.params[1];
} else {
*current_x += cmd.params[0];
*current_y += cmd.params[1];
}
*start_x = *current_x;
*start_y = *current_y;
}
}
CommandType::LineTo => {
if cmd.params.len() >= 2 {
if abs {
*current_x = cmd.params[0];
*current_y = cmd.params[1];
} else {
*current_x += cmd.params[0];
*current_y += cmd.params[1];
}
}
}
CommandType::HorizontalLineTo => {
if !cmd.params.is_empty() {
if abs {
*current_x = cmd.params[0];
} else {
*current_x += cmd.params[0];
}
}
}
CommandType::VerticalLineTo => {
if !cmd.params.is_empty() {
if abs {
*current_y = cmd.params[0];
} else {
*current_y += cmd.params[0];
}
}
}
CommandType::CurveTo => {
let n = cmd.params.len();
if n >= 6 {
let last_chunk_start = if n % 6 == 0 { n - 6 } else { n - (n % 6) };
if abs {
*current_x = cmd.params[last_chunk_start + 4];
*current_y = cmd.params[last_chunk_start + 5];
} else {
*current_x += cmd.params[last_chunk_start + 4];
*current_y += cmd.params[last_chunk_start + 5];
}
}
}
CommandType::SmoothCurveTo => {
let n = cmd.params.len();
if n >= 4 {
let last_start = if n % 4 == 0 { n - 4 } else { n - (n % 4) };
if abs {
*current_x = cmd.params[last_start + 2];
*current_y = cmd.params[last_start + 3];
} else {
*current_x += cmd.params[last_start + 2];
*current_y += cmd.params[last_start + 3];
}
}
}
CommandType::QuadraticBezier => {
let n = cmd.params.len();
if n >= 4 {
let last_start = if n % 4 == 0 { n - 4 } else { n - (n % 4) };
if abs {
*current_x = cmd.params[last_start + 2];
*current_y = cmd.params[last_start + 3];
} else {
*current_x += cmd.params[last_start + 2];
*current_y += cmd.params[last_start + 3];
}
}
}
CommandType::SmoothQuadraticBezier => {
if cmd.params.len() >= 2 {
if abs {
*current_x = cmd.params[0];
*current_y = cmd.params[1];
} else {
*current_x += cmd.params[0];
*current_y += cmd.params[1];
}
}
}
CommandType::Arc => {
if cmd.params.len() >= 7 {
if abs {
*current_x = cmd.params[5];
*current_y = cmd.params[6];
} else {
*current_x += cmd.params[5];
*current_y += cmd.params[6];
}
}
}
CommandType::ClosePath => {
*current_x = *start_x;
*current_y = *start_y;
}
}
}
fn apply_line_shorthands(commands: Vec<PathCommand>) -> Vec<PathCommand> {
commands
.into_iter()
.map(|mut cmd| {
if cmd.cmd_type == CommandType::LineTo && cmd.params.len() == 2 {
if cmd.is_absolute {
} else {
let dx = cmd.params[0];
let dy = cmd.params[1];
if dy == 0.0 {
cmd.cmd_type = CommandType::HorizontalLineTo;
cmd.params.pop();
} else if dx == 0.0 {
cmd.cmd_type = CommandType::VerticalLineTo;
cmd.params.remove(0);
}
}
}
cmd
})
.collect()
}
fn straighten_curves(commands: Vec<PathCommand>, tolerance: f64) -> Vec<PathCommand> {
let mut result = Vec::new();
let mut current_x = 0.0;
let mut current_y = 0.0;
let mut start_x = 0.0;
let mut start_y = 0.0;
for cmd in commands {
let new_cmd = cmd.clone();
match cmd.cmd_type {
CommandType::CurveTo if cmd.params.len() >= 6 => {
let chunks: Vec<&[f64]> = cmd.params.chunks(6).collect();
for chunk in chunks {
if chunk.len() < 6 {
break; }
let (abs_ctrl1x, abs_ctrl1y, abs_ctrl2x, abs_ctrl2y, abs_ex, abs_ey) =
if cmd.is_absolute {
(chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5])
} else {
(
current_x + chunk[0],
current_y + chunk[1],
current_x + chunk[2],
current_y + chunk[3],
current_x + chunk[4],
current_y + chunk[5],
)
};
let start = Point::new(current_x as f32, current_y as f32);
let ctrl1 = Point::new(abs_ctrl1x as f32, abs_ctrl1y as f32);
let ctrl2 = Point::new(abs_ctrl2x as f32, abs_ctrl2y as f32);
let end = Point::new(abs_ex as f32, abs_ey as f32);
let curve = CubicBezierSegment {
from: start,
ctrl1,
ctrl2,
to: end,
};
let out_cmd = if is_curve_nearly_straight(&curve, tolerance as f32) {
PathCommand {
cmd_type: CommandType::LineTo,
is_absolute: cmd.is_absolute,
params: vec![chunk[4], chunk[5]],
}
} else {
PathCommand {
cmd_type: CommandType::CurveTo,
is_absolute: cmd.is_absolute,
params: chunk.to_vec(),
}
};
result.push(out_cmd);
current_x = abs_ex;
current_y = abs_ey;
}
continue; }
CommandType::QuadraticBezier if cmd.params.len() >= 4 => {
let chunks: Vec<&[f64]> = cmd.params.chunks(4).collect();
for chunk in chunks {
if chunk.len() < 4 {
break;
}
let (abs_ctrlx, abs_ctrly, abs_ex, abs_ey) = if cmd.is_absolute {
(chunk[0], chunk[1], chunk[2], chunk[3])
} else {
(
current_x + chunk[0],
current_y + chunk[1],
current_x + chunk[2],
current_y + chunk[3],
)
};
let start = Point::new(current_x as f32, current_y as f32);
let ctrl = Point::new(abs_ctrlx as f32, abs_ctrly as f32);
let end = Point::new(abs_ex as f32, abs_ey as f32);
let curve = QuadraticBezierSegment {
from: start,
ctrl,
to: end,
};
let out_cmd = if is_quadratic_curve_nearly_straight(&curve, tolerance as f32) {
PathCommand {
cmd_type: CommandType::LineTo,
is_absolute: cmd.is_absolute,
params: vec![chunk[2], chunk[3]],
}
} else {
PathCommand {
cmd_type: CommandType::QuadraticBezier,
is_absolute: cmd.is_absolute,
params: chunk.to_vec(),
}
};
result.push(out_cmd);
current_x = abs_ex;
current_y = abs_ey;
}
continue;
}
_ => {
update_position(
&cmd,
&mut current_x,
&mut current_y,
&mut start_x,
&mut start_y,
);
}
}
result.push(new_cmd);
}
result
}
fn convert_cubic_to_quadratic(commands: Vec<PathCommand>, tolerance: f64) -> Vec<PathCommand> {
let mut result = Vec::new();
let mut current_x = 0.0;
let mut current_y = 0.0;
let mut start_x = 0.0;
let mut start_y = 0.0;
for cmd in commands {
let mut new_cmd = cmd.clone();
if cmd.cmd_type == CommandType::CurveTo && cmd.params.len() >= 6 {
let start = Point::new(current_x as f32, current_y as f32);
let ctrl1 = Point::new(cmd.params[0] as f32, cmd.params[1] as f32);
let ctrl2 = Point::new(cmd.params[2] as f32, cmd.params[3] as f32);
let end = Point::new(cmd.params[4] as f32, cmd.params[5] as f32);
let curve = CubicBezierSegment {
from: start,
ctrl1,
ctrl2,
to: end,
};
if let Some(quad_ctrl) = cubic_to_quadratic_control_point(&curve, tolerance as f32) {
new_cmd = PathCommand {
cmd_type: CommandType::QuadraticBezier,
is_absolute: cmd.is_absolute,
params: vec![
quad_ctrl.x as f64,
quad_ctrl.y as f64,
cmd.params[4],
cmd.params[5],
],
};
}
current_x = cmd.params[4];
current_y = cmd.params[5];
} else {
update_position(
&cmd,
&mut current_x,
&mut current_y,
&mut start_x,
&mut start_y,
);
}
result.push(new_cmd);
}
result
}
fn convert_curves_to_arcs(commands: Vec<PathCommand>, tolerance: f64) -> Vec<PathCommand> {
let mut result = Vec::new();
let mut current_x = 0.0;
let mut current_y = 0.0;
let mut start_x = 0.0;
let mut start_y = 0.0;
for cmd in commands {
let mut new_cmd = cmd.clone();
match cmd.cmd_type {
CommandType::CurveTo if cmd.params.len() >= 6 => {
let start = Point::new(current_x as f32, current_y as f32);
let ctrl1 = Point::new(cmd.params[0] as f32, cmd.params[1] as f32);
let ctrl2 = Point::new(cmd.params[2] as f32, cmd.params[3] as f32);
let end = Point::new(cmd.params[4] as f32, cmd.params[5] as f32);
let curve = CubicBezierSegment {
from: start,
ctrl1,
ctrl2,
to: end,
};
if let Some(arc_params) = cubic_to_arc_parameters(&curve, tolerance as f32) {
new_cmd = PathCommand {
cmd_type: CommandType::Arc,
is_absolute: cmd.is_absolute,
params: arc_params,
};
}
current_x = cmd.params[4];
current_y = cmd.params[5];
}
CommandType::QuadraticBezier if cmd.params.len() >= 4 => {
let start = Point::new(current_x as f32, current_y as f32);
let ctrl = Point::new(cmd.params[0] as f32, cmd.params[1] as f32);
let end = Point::new(cmd.params[2] as f32, cmd.params[3] as f32);
let curve = QuadraticBezierSegment {
from: start,
ctrl,
to: end,
};
if let Some(arc_params) = quadratic_to_arc_parameters(&curve, tolerance as f32) {
new_cmd = PathCommand {
cmd_type: CommandType::Arc,
is_absolute: cmd.is_absolute,
params: arc_params,
};
}
current_x = cmd.params[2];
current_y = cmd.params[3];
}
_ => {
update_position(
&cmd,
&mut current_x,
&mut current_y,
&mut start_x,
&mut start_y,
);
}
}
result.push(new_cmd);
}
result
}
fn is_curve_nearly_straight(curve: &CubicBezierSegment<f32>, tolerance: f32) -> bool {
let line_vec = curve.to - curve.from;
let line_length = line_vec.length();
if line_length < tolerance {
return false;
}
let line_unit = line_vec / line_length;
let ctrl1_vec = curve.ctrl1 - curve.from;
let ctrl1_proj = ctrl1_vec.dot(line_unit);
let ctrl1_perp = ctrl1_vec - line_unit * ctrl1_proj;
let ctrl1_dist = ctrl1_perp.length();
let ctrl2_vec = curve.ctrl2 - curve.from;
let ctrl2_proj = ctrl2_vec.dot(line_unit);
let ctrl2_perp = ctrl2_vec - line_unit * ctrl2_proj;
let ctrl2_dist = ctrl2_perp.length();
ctrl1_dist < tolerance && ctrl2_dist < tolerance
}
fn is_quadratic_curve_nearly_straight(curve: &QuadraticBezierSegment<f32>, tolerance: f32) -> bool {
let line_vec = curve.to - curve.from;
let line_length = line_vec.length();
if line_length < tolerance {
return false;
}
let line_unit = line_vec / line_length;
let ctrl_vec = curve.ctrl - curve.from;
let ctrl_proj = ctrl_vec.dot(line_unit);
let ctrl_perp = ctrl_vec - line_unit * ctrl_proj;
let ctrl_dist = ctrl_perp.length();
ctrl_dist < tolerance
}
fn cubic_to_quadratic_control_point(
curve: &CubicBezierSegment<f32>,
tolerance: f32,
) -> Option<Point<f32>> {
let start_to_ctrl1 = curve.ctrl1 - curve.from;
let end_to_ctrl2 = curve.ctrl2 - curve.to;
let cross_product = start_to_ctrl1.x * end_to_ctrl2.y - start_to_ctrl1.y * end_to_ctrl2.x;
if cross_product.abs() < 1e-6 {
return None;
}
let dx = curve.to.x - curve.from.x;
let dy = curve.to.y - curve.from.y;
let det = start_to_ctrl1.x * (-end_to_ctrl2.y) - start_to_ctrl1.y * (-end_to_ctrl2.x);
if det.abs() < 1e-6 {
return None;
}
let t = (dx * (-end_to_ctrl2.y) - dy * (-end_to_ctrl2.x)) / det;
let quad_ctrl = curve.from + start_to_ctrl1 * t;
let test_quad = QuadraticBezierSegment {
from: curve.from,
ctrl: quad_ctrl,
to: curve.to,
};
const SAMPLES: usize = 10;
for i in 1..SAMPLES {
let t = i as f32 / SAMPLES as f32;
let cubic_point = curve.sample(t);
let quad_point = test_quad.sample(t);
let distance = (cubic_point - quad_point).length();
if distance > tolerance {
return None;
}
}
Some(quad_ctrl)
}
fn cubic_to_arc_parameters(curve: &CubicBezierSegment<f32>, tolerance: f32) -> Option<Vec<f64>> {
const SAMPLES: usize = 5;
let mut points = Vec::new();
for i in 0..=SAMPLES {
let t = i as f32 / SAMPLES as f32;
points.push(curve.sample(t));
}
if let Some((center, radius)) = fit_circle_to_points(&points, tolerance) {
let start_angle = (curve.from.y - center.y).atan2(curve.from.x - center.x);
let end_angle = (curve.to.y - center.y).atan2(curve.to.x - center.x);
let mut angle_diff = end_angle - start_angle;
if angle_diff > std::f32::consts::PI {
angle_diff -= 2.0 * std::f32::consts::PI;
} else if angle_diff < -std::f32::consts::PI {
angle_diff += 2.0 * std::f32::consts::PI;
}
let large_arc_flag = if angle_diff.abs() > std::f32::consts::PI {
1.0
} else {
0.0
};
let sweep_flag = if angle_diff > 0.0 { 1.0 } else { 0.0 };
return Some(vec![
radius as f64, radius as f64, 0.0, large_arc_flag, sweep_flag, curve.to.x as f64, curve.to.y as f64, ]);
}
None
}
fn quadratic_to_arc_parameters(
curve: &QuadraticBezierSegment<f32>,
tolerance: f32,
) -> Option<Vec<f64>> {
const SAMPLES: usize = 5;
let mut points = Vec::new();
for i in 0..=SAMPLES {
let t = i as f32 / SAMPLES as f32;
points.push(curve.sample(t));
}
if let Some((center, radius)) = fit_circle_to_points(&points, tolerance) {
let start_angle = (curve.from.y - center.y).atan2(curve.from.x - center.x);
let end_angle = (curve.to.y - center.y).atan2(curve.to.x - center.x);
let mut angle_diff = end_angle - start_angle;
if angle_diff > std::f32::consts::PI {
angle_diff -= 2.0 * std::f32::consts::PI;
} else if angle_diff < -std::f32::consts::PI {
angle_diff += 2.0 * std::f32::consts::PI;
}
let large_arc_flag = if angle_diff.abs() > std::f32::consts::PI {
1.0
} else {
0.0
};
let sweep_flag = if angle_diff > 0.0 { 1.0 } else { 0.0 };
return Some(vec![
radius as f64, radius as f64, 0.0, large_arc_flag, sweep_flag, curve.to.x as f64, curve.to.y as f64, ]);
}
None
}
fn fit_circle_to_points(points: &[Point<f32>], tolerance: f32) -> Option<(Point<f32>, f32)> {
if points.len() < 3 {
return None;
}
let p1 = points[0];
let p2 = points[1];
let p3 = points[2];
let mid12 = Point::new((p1.x + p2.x) / 2.0, (p1.y + p2.y) / 2.0);
let mid23 = Point::new((p2.x + p3.x) / 2.0, (p2.y + p3.y) / 2.0);
let dir12 = Vector::new(-(p2.y - p1.y), p2.x - p1.x); let dir23 = Vector::new(-(p3.y - p2.y), p3.x - p2.x);
let det = dir12.x * dir23.y - dir12.y * dir23.x;
if det.abs() < 1e-6 {
return None; }
let diff = mid23 - mid12;
let t = (diff.x * dir23.y - diff.y * dir23.x) / det;
let center = mid12 + dir12 * t;
let radius = (p1 - center).length();
for &point in points {
let distance = (point - center).length();
if (distance - radius).abs() > tolerance {
return None;
}
}
Some((center, radius))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_plugin_info() {
let plugin = ConvertPathDataPlugin::new();
assert_eq!(plugin.name(), "convertPathData");
assert_eq!(
plugin.description(),
"converts path data to relative or absolute, optimizes segments, simplifies curves"
);
}
#[test]
fn test_param_validation() {
let plugin = ConvertPathDataPlugin::new();
assert!(plugin.validate_params(&Value::Null).is_ok());
assert!(plugin
.validate_params(&json!({
"floatPrecision": 2,
"removeUseless": false
}))
.is_ok());
assert!(plugin
.validate_params(&json!({
"invalidParam": true
}))
.is_err());
}
#[test]
fn test_parse_simple_path() {
let path = "M10 20 L30 40";
let commands = parse_path_data(path).unwrap();
assert_eq!(commands.len(), 2);
assert_eq!(commands[0].cmd_type, CommandType::MoveTo);
assert_eq!(commands[0].params, vec![10.0, 20.0]);
assert_eq!(commands[1].cmd_type, CommandType::LineTo);
assert_eq!(commands[1].params, vec![30.0, 40.0]);
}
#[test]
fn test_parse_relative_path() {
let path = "m10 20 l30 40";
let commands = parse_path_data(path).unwrap();
assert_eq!(commands.len(), 2);
assert_eq!(commands[0].cmd_type, CommandType::MoveTo);
assert!(!commands[0].is_absolute);
assert_eq!(commands[1].cmd_type, CommandType::LineTo);
assert!(!commands[1].is_absolute);
}
#[test]
fn test_format_number() {
let mut buf = String::new();
format_number_to_buf(1.0, 3, &mut buf);
assert_eq!(buf, "1");
buf.clear();
format_number_to_buf(1.234567, 3, &mut buf);
assert_eq!(buf, "1.235");
buf.clear();
format_number_to_buf(0.5, 1, &mut buf);
assert_eq!(buf, ".5");
buf.clear();
format_number_to_buf(-0.5, 1, &mut buf);
assert_eq!(buf, "-.5");
}
#[test]
fn test_parse_dot_separator_bug() {
let path1 = "M 10.5.5";
let commands1 = parse_path_data(path1).unwrap();
assert_eq!(commands1.len(), 1);
assert_eq!(commands1[0].cmd_type, CommandType::MoveTo);
assert_eq!(commands1[0].params.len(), 2);
assert_eq!(commands1[0].params[0], 10.5);
assert_eq!(commands1[0].params[1], 0.5);
let path2 = "M -7.1.6";
let commands2 = parse_path_data(path2).unwrap();
assert_eq!(commands2.len(), 1);
assert_eq!(commands2[0].cmd_type, CommandType::MoveTo);
assert_eq!(commands2[0].params.len(), 2);
assert_eq!(commands2[0].params[0], -7.1);
assert_eq!(commands2[0].params[1], 0.6);
}
#[test]
fn test_optimize_removes_useless_lineto() {
let path = "M10 10 L10 10 L20 20";
let config = ConvertPathDataConfig {
float_precision: 3,
transform_precision: 5,
remove_useless: true,
collapse_repeated: true,
utilize_absolute: true,
leading_zero: true,
negative_extra_space: true,
make_arcs: MakeArcsConfig::Enabled(false),
straight_curves: false,
line_shorthands: false,
convert_to_q: false,
curve_tolerance: 0.1,
arc_tolerance: 0.5,
};
let optimized = optimize_path_data(path, &config).unwrap();
assert!(!optimized.contains("L10 10"));
}
#[test]
fn test_curve_straightening() {
let curve = CubicBezierSegment {
from: Point::new(0.0, 0.0),
ctrl1: Point::new(1.0, 0.01), ctrl2: Point::new(2.0, -0.01),
to: Point::new(3.0, 0.0),
};
assert!(is_curve_nearly_straight(&curve, 0.1));
assert!(!is_curve_nearly_straight(&curve, 0.001));
}
#[test]
fn test_quadratic_curve_straightening() {
let curve = QuadraticBezierSegment {
from: Point::new(0.0, 0.0),
ctrl: Point::new(1.5, 0.01), to: Point::new(3.0, 0.0),
};
assert!(is_quadratic_curve_nearly_straight(&curve, 0.1));
assert!(!is_quadratic_curve_nearly_straight(&curve, 0.001));
}
#[test]
fn test_circle_fitting() {
let center = Point::new(10.0, 10.0);
let radius = 5.0;
let points = vec![
Point::new(center.x + radius, center.y),
Point::new(center.x, center.y + radius),
Point::new(center.x - radius, center.y),
Point::new(center.x, center.y - radius),
];
if let Some((fitted_center, fitted_radius)) = fit_circle_to_points(&points, 0.1) {
assert!((fitted_center.x - center.x).abs() < 0.1);
assert!((fitted_center.y - center.y).abs() < 0.1);
assert!((fitted_radius - radius).abs() < 0.1);
} else {
panic!("Should be able to fit circle to points on circle");
}
}
#[test]
fn test_advanced_config_validation() {
let plugin = ConvertPathDataPlugin::new();
assert!(plugin
.validate_params(&json!({
"floatPrecision": 2,
"makeArcs": true,
"straightCurves": true,
"convertToQ": true,
"curveTolerance": 0.05,
"arcTolerance": 0.3
}))
.is_ok());
}
#[test]
fn test_collapse_repeated_commands() {
let commands = vec![
PathCommand {
cmd_type: CommandType::HorizontalLineTo,
is_absolute: false,
params: vec![10.0],
},
PathCommand {
cmd_type: CommandType::HorizontalLineTo,
is_absolute: false,
params: vec![20.0],
},
];
let result = collapse_repeated_commands(commands);
assert_eq!(result.len(), 1);
assert_eq!(result[0].params[0], 30.0);
let commands = vec![
PathCommand {
cmd_type: CommandType::VerticalLineTo,
is_absolute: false,
params: vec![5.0],
},
PathCommand {
cmd_type: CommandType::VerticalLineTo,
is_absolute: false,
params: vec![15.0],
},
];
let result = collapse_repeated_commands(commands);
assert_eq!(result.len(), 1);
assert_eq!(result[0].params[0], 20.0);
let commands = vec![
PathCommand {
cmd_type: CommandType::HorizontalLineTo,
is_absolute: false,
params: vec![10.0],
},
PathCommand {
cmd_type: CommandType::HorizontalLineTo,
is_absolute: false,
params: vec![-5.0],
},
];
let result = collapse_repeated_commands(commands);
assert_eq!(result.len(), 2); }
#[test]
fn test_should_use_absolute() {
let cmd = PathCommand {
cmd_type: CommandType::LineTo,
is_absolute: true,
params: vec![5.0, 5.0],
};
assert!(should_use_absolute_fast(&cmd, 1000.0, 1000.0, 3));
let cmd = PathCommand {
cmd_type: CommandType::LineTo,
is_absolute: true,
params: vec![1001.0, 1001.0],
};
assert!(!should_use_absolute_fast(&cmd, 1000.0, 1000.0, 3));
}
#[test]
fn test_format_params() {
fn format_params_test(params: &[f64], precision: u8) -> String {
let mut result = String::new();
let mut buf = String::new();
let mut prev_has_dot = false;
for (i, &p) in params.iter().enumerate() {
let (has_dot, first_byte) = format_number_to_buf(p, precision, &mut buf);
if i > 0 && needs_separator(prev_has_dot, first_byte) {
result.push(' ');
}
result.push_str(&buf);
prev_has_dot = has_dot;
}
result
}
assert_eq!(format_params_test(&[10.0, 20.0, 30.0], 3), "10 20 30");
assert_eq!(format_params_test(&[10.0, -20.0, 30.0], 3), "10-20 30");
assert_eq!(format_params_test(&[10.123456, 20.999], 2), "10.12 21");
assert_eq!(format_params_test(&[0.5, 0.3], 3), ".5.3");
assert_eq!(format_params_test(&[5.0, 0.3], 3), "5 .3");
}
#[test]
fn test_can_collapse_commands() {
let cmd1 = PathCommand {
cmd_type: CommandType::HorizontalLineTo,
is_absolute: false,
params: vec![10.0],
};
let cmd2 = PathCommand {
cmd_type: CommandType::HorizontalLineTo,
is_absolute: false,
params: vec![20.0],
};
assert!(can_collapse_commands(&cmd1, &cmd2));
let cmd1 = PathCommand {
cmd_type: CommandType::HorizontalLineTo,
is_absolute: false,
params: vec![10.0],
};
let cmd2 = PathCommand {
cmd_type: CommandType::VerticalLineTo,
is_absolute: false,
params: vec![20.0],
};
assert!(!can_collapse_commands(&cmd1, &cmd2));
let cmd1 = PathCommand {
cmd_type: CommandType::HorizontalLineTo,
is_absolute: true,
params: vec![10.0],
};
let cmd2 = PathCommand {
cmd_type: CommandType::HorizontalLineTo,
is_absolute: false,
params: vec![20.0],
};
assert!(!can_collapse_commands(&cmd1, &cmd2));
let cmd1 = PathCommand {
cmd_type: CommandType::LineTo,
is_absolute: false,
params: vec![10.0, 20.0],
};
let cmd2 = PathCommand {
cmd_type: CommandType::LineTo,
is_absolute: false,
params: vec![5.0, 5.0],
};
assert!(can_collapse_commands(&cmd1, &cmd2));
let cmd1 = PathCommand {
cmd_type: CommandType::LineTo,
is_absolute: true,
params: vec![10.0, 20.0],
};
let cmd2 = PathCommand {
cmd_type: CommandType::LineTo,
is_absolute: true,
params: vec![15.0, 25.0],
};
assert!(!can_collapse_commands(&cmd1, &cmd2));
}
}