use smooth_frame::{
PathCommand, Point, SmoothCorner, SmoothError, SmoothFrame, SmoothRect, Vector,
};
use std::f64::consts::PI;
const TOLERANCE: f64 = 1.0e-9;
#[test]
fn radius_zero_outputs_plain_rect() {
let path = SmoothRect::new(1000.0, 500.0)
.with_radius(0.0)
.with_smoothing(0.6)
.to_path();
assert_eq!(path.cubics().len(), 0);
assert_eq!(
path.commands(),
&[
PathCommand::MoveTo(Point::new(0.0, 0.0)),
PathCommand::LineTo(Point::new(1000.0, 0.0)),
PathCommand::LineTo(Point::new(1000.0, 500.0)),
PathCommand::LineTo(Point::new(0.0, 500.0)),
PathCommand::Close,
]
);
}
#[test]
fn sketchtool_reference_1000_r100_s06() {
let path = SmoothRect::new(1000.0, 1000.0)
.with_radius(100.0)
.with_smoothing(0.6)
.to_path();
let cubics = path.cubics();
assert_eq!(cubics.len(), 12);
assert_point_close(cubics[0].from, Point::new(840.0, 0.0));
assert_point_close(cubics[0].ctrl1, Point::new(896.005250605341, 0.0));
assert_point_close(cubics[0].ctrl2, Point::new(924.007875908012, 0.0));
assert_point_close(cubics[0].to, Point::new(945.399049973955, 10.899347581163));
assert_point_close(
cubics[1].ctrl1,
Point::new(964.215259261833, 20.486685076350),
);
assert_point_close(
cubics[1].ctrl2,
Point::new(979.513314923650, 35.784740738167),
);
assert_point_close(cubics[1].to, Point::new(989.100652418837, 54.600950026045));
assert_point_close(cubics[2].ctrl1, Point::new(1000.0, 75.992124091988));
assert_point_close(cubics[2].ctrl2, Point::new(1000.0, 103.994749394659));
assert_point_close(cubics[2].to, Point::new(1000.0, 160.0));
}
#[test]
fn saturated_radius_400_matches_sketchtool_zero_length_edge_omission() {
let path = SmoothRect::new(1000.0, 1000.0)
.with_radius(400.0)
.with_smoothing(0.6)
.to_path();
assert_eq!(path.cubics().len(), 12);
assert_eq!(path.commands().len(), 14);
assert_eq!(
path.commands()[0],
PathCommand::MoveTo(Point::new(500.0, 0.0))
);
}
#[test]
fn short_axis_saturation_remains_continuous() {
let path = SmoothRect::new(1000.0, 500.0)
.with_radius(250.0)
.with_smoothing(0.6)
.to_path();
assert_eq!(path.cubics().len(), 8);
assert_eq!(path.commands().len(), 12);
assert_eq!(
path.commands()[0],
PathCommand::MoveTo(Point::new(400.0, 0.0))
);
assert!(path.cubics().iter().all(|c| c.from.is_finite()
&& c.ctrl1.is_finite()
&& c.ctrl2.is_finite()
&& c.to.is_finite()));
}
#[test]
fn max_radius_matches_sketchtool_four_cubic_circle() {
let path = SmoothRect::new(1000.0, 1000.0)
.with_radius(500.0)
.with_smoothing(0.6)
.to_path();
assert_eq!(path.cubics().len(), 4);
assert_eq!(path.commands().len(), 6);
assert_eq!(
path.commands()[0],
PathCommand::MoveTo(Point::new(500.0, 0.0))
);
}
#[test]
fn near_max_radius_matches_sketchtool_round_rect_fallback() {
let path = SmoothRect::new(1000.0, 1000.0)
.with_radius(498.0)
.with_smoothing(0.6)
.to_path();
assert_eq!(path.cubics().len(), 4);
assert_eq!(path.commands().len(), 10);
assert_eq!(
path.commands()[0],
PathCommand::MoveTo(Point::new(498.0, 0.0))
);
}
#[test]
fn smoothing_influence_rule_matches_sketch_formula() {
for smoothing in [0.0, 0.3, 0.6, 0.8, 1.0] {
let geometry = SmoothCorner::new(
Point::new(0.0, 0.0),
Vector::new(1.0, 0.0),
Vector::new(0.0, 1.0),
)
.with_radius(100.0)
.with_smoothing(smoothing)
.with_limits(500.0, 500.0)
.geometry()
.unwrap();
assert_close(geometry.incoming_influence, (1.0 + smoothing) * 100.0);
assert_close(geometry.outgoing_influence, (1.0 + smoothing) * 100.0);
assert_close(geometry.alpha0, smoothing * PI / 4.0);
assert_close(geometry.alpha1, smoothing * PI / 4.0);
}
}
#[test]
fn convex_polygon_frame_is_supported() {
let path = SmoothFrame::closed([
Point::new(0.0, 0.0),
Point::new(220.0, 30.0),
Point::new(180.0, 170.0),
Point::new(20.0, 140.0),
])
.with_radius(24.0)
.with_smoothing(0.5)
.to_path()
.unwrap();
assert_eq!(path.cubics().len(), 12);
}
#[test]
fn convex_triangle_frame_is_supported() {
let path = SmoothFrame::closed([
Point::new(40.0, 0.0),
Point::new(180.0, 30.0),
Point::new(80.0, 150.0),
])
.with_radius(18.0)
.with_smoothing(0.6)
.to_path()
.unwrap();
assert_eq!(path.cubics().len(), 9);
assert_eq!(path.commands().len(), 14);
assert!(path.cubics().iter().all(|c| c.from.is_finite()
&& c.ctrl1.is_finite()
&& c.ctrl2.is_finite()
&& c.to.is_finite()));
}
#[test]
fn concave_star_frame_returns_error() {
let result = SmoothFrame::closed([
Point::new(100.0, 0.0),
Point::new(124.0, 68.0),
Point::new(196.0, 72.0),
Point::new(138.0, 114.0),
Point::new(158.0, 184.0),
Point::new(100.0, 142.0),
Point::new(42.0, 184.0),
Point::new(62.0, 114.0),
Point::new(4.0, 72.0),
Point::new(76.0, 68.0),
])
.with_radius(12.0)
.with_smoothing(0.6)
.to_path();
assert_eq!(result.unwrap_err(), SmoothError::ConcaveFrame);
}
#[test]
fn self_intersecting_star_frame_returns_error() {
let result = SmoothFrame::closed([
Point::new(100.0, 0.0),
Point::new(158.0, 184.0),
Point::new(4.0, 72.0),
Point::new(196.0, 72.0),
Point::new(42.0, 184.0),
])
.with_radius(12.0)
.with_smoothing(0.6)
.to_path();
assert_eq!(result.unwrap_err(), SmoothError::SelfIntersectingFrame);
}
#[test]
fn concave_frame_returns_error() {
let result = SmoothFrame::closed([
Point::new(0.0, 0.0),
Point::new(100.0, 0.0),
Point::new(40.0, 40.0),
Point::new(100.0, 100.0),
Point::new(0.0, 100.0),
])
.with_radius(12.0)
.with_smoothing(0.6)
.to_path();
assert_eq!(result.unwrap_err(), SmoothError::ConcaveFrame);
}
#[test]
fn radius_sweep_0_to_500_matches_sketchtool_structure() {
for radius in 0..=500 {
let path = SmoothRect::new(1000.0, 1000.0)
.with_radius(radius as f64)
.with_smoothing(0.6)
.to_path();
if radius == 0 {
assert_eq!(path.cubics().len(), 0, "radius={radius}");
assert_eq!(path.commands().len(), 5, "radius={radius}");
} else if radius <= 312 {
assert_eq!(path.cubics().len(), 12, "radius={radius}");
assert_eq!(path.commands().len(), 18, "radius={radius}");
} else if radius <= 497 {
assert_eq!(path.cubics().len(), 12, "radius={radius}");
assert_eq!(path.commands().len(), 14, "radius={radius}");
} else if radius <= 499 {
assert_eq!(path.cubics().len(), 4, "radius={radius}");
assert_eq!(path.commands().len(), 10, "radius={radius}");
} else {
assert_eq!(path.cubics().len(), 4, "radius={radius}");
assert_eq!(path.commands().len(), 6, "radius={radius}");
}
assert!(
path.cubics().iter().all(|c| c.from.is_finite()
&& c.ctrl1.is_finite()
&& c.ctrl2.is_finite()
&& c.to.is_finite()),
"radius={radius}"
);
}
}
#[test]
fn svg_path_is_stable_without_trailing_zeroes() {
let svg = SmoothRect::new(1000.0, 500.0)
.with_radius(0.0)
.to_path()
.to_svg_path_with_precision(3);
assert_eq!(svg, "M 0,0 L 1000,0 L 1000,500 L 0,500 Z");
assert!(!svg.contains(".000"));
}
fn assert_point_close(actual: Point, expected: Point) {
assert_close(actual.x, expected.x);
assert_close(actual.y, expected.y);
}
fn assert_close(actual: f64, expected: f64) {
assert!(
(actual - expected).abs() <= TOLERANCE,
"actual={actual:?}, expected={expected:?}, diff={:?}",
(actual - expected).abs()
);
}