Files
figcraft/crates/pathops/src/sides_tests.rs
T
BFlat 08d91869cb M4: live boolean editing, arc fields, shape builder on strokes, one-sided widths (#111)
Boolean operands are editable in place with live results (per-operand operations, cached prefixes so a drag recomputes from the edited operand up); a Design panel Arc section; the shape builder takes strokes and open paths as outlined areas; variable width points can be wider on one side.
2026-10-11 10:04:25 +07:00

71 lines
3.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! One-sided variable widths in stroke outlines: each side offset by its own width, lopsided
//! caps around the middle of the end, dashes and closed paths.
use figcraft_doc::{StrokeAlign, StrokeCap, StrokeProps, WidthPoint, WidthProfile};
use figcraft_geom::kurbo::{BezPath, Shape};
use crate::stroke_outline;
/// A horizontal line from (0, 0) to (100, 0): looking along it, its right side is +y (y down).
fn line() -> BezPath {
let mut p = BezPath::new();
p.move_to((0.0, 0.0));
p.line_to((100.0, 0.0));
p
}
fn props(points: Vec<WidthPoint>) -> StrokeProps {
StrokeProps { weight: 10.0, align: StrokeAlign::Center, width_profile: Some(WidthProfile { points }.sanitized()), ..Default::default() }
}
#[test]
fn each_side_is_offset_by_its_own_width() {
// All on the right: 0 to the left, 10 to the right (side scale 2 × weight 10 / 2).
let p = props(vec![WidthPoint::from_sides(0.0, 0.0, 2.0), WidthPoint::from_sides(1.0, 0.0, 2.0)]);
let b = stroke_outline(&line(), &p, false).bounding_box();
assert!(b.y0.abs() < 1e-6 && (b.y1 - 10.0).abs() < 1e-6 && b.x0.abs() < 1e-6 && (b.x1 - 100.0).abs() < 1e-6, "{b:?}");
assert!((stroke_outline(&line(), &p, false).area().abs() - 1000.0).abs() < 1.0);
// Lopsided: 2 on the left, 8 on the right.
let p = props(vec![WidthPoint::from_sides(0.5, 0.4, 1.6)]);
let b = stroke_outline(&line(), &p, false).bounding_box();
assert!((b.y0 + 2.0).abs() < 1e-6 && (b.y1 - 8.0).abs() < 1e-6, "{b:?}");
// The right side swells in the middle while the left stays straight.
let p = props(vec![WidthPoint::new(0.0, 1.0), WidthPoint::from_sides(0.5, 1.0, 4.0), WidthPoint::new(1.0, 1.0)]);
let b = stroke_outline(&line(), &p, false).bounding_box();
assert!((b.y0 + 5.0).abs() < 1e-6 && (b.y1 - 20.0).abs() < 1e-6, "{b:?}");
}
#[test]
fn caps_wrap_the_middle_of_a_lopsided_end() {
// 0 on the left, 10 on the right: a round cap is a half circle of radius 5 around (100, 5).
let mut p = props(vec![WidthPoint::from_sides(0.0, 0.0, 2.0), WidthPoint::from_sides(1.0, 0.0, 2.0)]);
p.cap_start = StrokeCap::Round;
p.cap_end = StrokeCap::Round;
let b = stroke_outline(&line(), &p, false).bounding_box();
assert!((b.x1 - 105.0).abs() < 0.05 && (b.x0 + 5.0).abs() < 0.05, "{b:?}");
assert!(b.y0 > -0.05 && (b.y1 - 10.0).abs() < 0.05, "{b:?}");
p.cap_end = StrokeCap::Square;
let b = stroke_outline(&line(), &p, false).bounding_box();
assert!((b.x1 - 105.0).abs() < 1e-6 && b.y0 > -1e-6, "{b:?}");
}
#[test]
fn dashes_and_closed_paths_keep_their_sides() {
let mut p = props(vec![WidthPoint::from_sides(0.0, 0.0, 2.0), WidthPoint::from_sides(1.0, 0.0, 2.0)]);
p.dashes = vec![10.0, 10.0];
let s = stroke_outline(&line(), &p, false);
let b = s.bounding_box();
assert!(b.y0.abs() < 1e-6 && (b.y1 - 10.0).abs() < 1e-6, "{b:?}");
assert!((s.area().abs() - 500.0).abs() < 5.0, "{}", s.area());
// A closed square drawn clockwise on screen: its right side is the inside.
let mut sq = BezPath::new();
sq.move_to((0.0, 0.0));
sq.line_to((100.0, 0.0));
sq.line_to((100.0, 100.0));
sq.line_to((0.0, 100.0));
sq.close_path();
let p = props(vec![WidthPoint::from_sides(0.0, 0.0, 2.0), WidthPoint::from_sides(1.0, 0.0, 2.0)]);
let b = stroke_outline(&sq, &p, true).bounding_box();
assert!(b.x0 > -1e-6 && b.y0 > -1e-6 && b.x1 < 100.0 + 1e-6 && b.y1 < 100.0 + 1e-6, "all inside: {b:?}");
}