mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 05:16:56 -06:00
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.
This commit is contained in:
+30
-33
@@ -6,6 +6,10 @@
|
||||
//! dashes, caps, joins, arrowheads and variable width profiles ([`width`]).
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
#[cfg(test)]
|
||||
mod ops_tests;
|
||||
#[cfg(test)]
|
||||
mod sides_tests;
|
||||
pub mod width;
|
||||
#[cfg(test)]
|
||||
mod width_tests;
|
||||
@@ -79,44 +83,37 @@ pub fn simplify(path: &BezPath) -> Result<BezPath, PathOpsError> {
|
||||
binary(path, &BezPath::new(), linesweeper::BinaryOp::Union)
|
||||
}
|
||||
|
||||
/// Combine `acc` (the operands so far) with the next operand `p` by `op`.
|
||||
pub fn boolean_step(acc: &BezPath, p: &BezPath, op: BoolOp) -> Result<BezPath, PathOpsError> {
|
||||
let op = match op {
|
||||
BoolOp::Union => linesweeper::BinaryOp::Union,
|
||||
BoolOp::Intersect => linesweeper::BinaryOp::Intersection,
|
||||
BoolOp::Exclude => linesweeper::BinaryOp::Xor,
|
||||
BoolOp::Subtract => linesweeper::BinaryOp::Difference,
|
||||
};
|
||||
binary(acc, p, op)
|
||||
}
|
||||
|
||||
/// Apply a boolean operation to `paths` in order (bottom to top, like a boolean group's children):
|
||||
/// Subtract removes every later shape from the first one.
|
||||
pub fn boolean(paths: &[BezPath], op: BoolOp) -> Result<BezPath, PathOpsError> {
|
||||
let mut it = paths.iter();
|
||||
let Some(first) = it.next() else { return Ok(BezPath::new()) };
|
||||
let mut acc = first.clone();
|
||||
match op {
|
||||
BoolOp::Union => {
|
||||
for p in it {
|
||||
acc = binary(&acc, p, linesweeper::BinaryOp::Union)?;
|
||||
}
|
||||
}
|
||||
BoolOp::Intersect => {
|
||||
for p in it {
|
||||
acc = binary(&acc, p, linesweeper::BinaryOp::Intersection)?;
|
||||
}
|
||||
}
|
||||
BoolOp::Exclude => {
|
||||
for p in it {
|
||||
acc = binary(&acc, p, linesweeper::BinaryOp::Xor)?;
|
||||
}
|
||||
}
|
||||
BoolOp::Subtract => {
|
||||
let rest: Vec<BezPath> = it.cloned().collect();
|
||||
if rest.is_empty() {
|
||||
return Ok(acc);
|
||||
}
|
||||
let mut cut = BezPath::new();
|
||||
for p in &rest {
|
||||
cut = binary(&cut, p, linesweeper::BinaryOp::Union)?;
|
||||
}
|
||||
acc = binary(&acc, &cut, linesweeper::BinaryOp::Difference)?;
|
||||
}
|
||||
boolean_ops(paths.iter().map(|p| (p, op)))
|
||||
}
|
||||
|
||||
/// Fold `operands` bottom to top, each combined with the result of those below it by its own
|
||||
/// operation (the bottom operand's operation is unused): what a boolean group whose operands carry
|
||||
/// their own operations paints. A lone operand comes out simplified.
|
||||
pub fn boolean_ops<'a>(operands: impl IntoIterator<Item = (&'a BezPath, BoolOp)>) -> Result<BezPath, PathOpsError> {
|
||||
let mut it = operands.into_iter();
|
||||
let Some((first, _)) = it.next() else { return Ok(BezPath::new()) };
|
||||
let mut acc: Option<BezPath> = None;
|
||||
for (p, op) in it {
|
||||
acc = Some(boolean_step(acc.as_ref().unwrap_or(first), p, op)?);
|
||||
}
|
||||
if paths.len() == 1 {
|
||||
return simplify(&acc);
|
||||
match acc {
|
||||
Some(a) => Ok(a),
|
||||
None => simplify(first),
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
/// Per-input winding numbers for [`faces`] (index = input path; trailing zeros trimmed so equal
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Boolean groups whose operands carry their own operations: the fold bottom to top, its
|
||||
//! agreement with a uniform operation, and single steps.
|
||||
|
||||
use figcraft_doc::BoolOp;
|
||||
use figcraft_geom::Rect;
|
||||
use figcraft_geom::kurbo::{BezPath, Shape};
|
||||
|
||||
use crate::{boolean, boolean_ops, boolean_step};
|
||||
|
||||
fn sq(x: f64, y: f64, s: f64) -> BezPath {
|
||||
Rect::new(x, y, x + s, y + s).to_path(0.1)
|
||||
}
|
||||
|
||||
fn area(p: &BezPath) -> f64 {
|
||||
p.area().abs()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_operand_combines_with_the_result_below_it() {
|
||||
let (a, b, c) = (sq(0.0, 0.0, 10.0), sq(5.0, 0.0, 10.0), sq(0.0, 0.0, 15.0));
|
||||
// (A ∪ B) ∩ C: the union spans x 0..15, y 0..10 (150), all inside C.
|
||||
let r = boolean_ops([(&a, BoolOp::Union), (&b, BoolOp::Union), (&c, BoolOp::Intersect)]).unwrap();
|
||||
assert!((area(&r) - 150.0).abs() < 0.5, "{}", area(&r));
|
||||
// (A ∪ B) − a 5 × 5 square in the middle.
|
||||
let hole = sq(5.0, 2.5, 5.0);
|
||||
let r = boolean_ops([(&a, BoolOp::Union), (&b, BoolOp::Union), (&hole, BoolOp::Subtract)]).unwrap();
|
||||
assert!((area_eo(&r) - 125.0).abs() < 0.5, "{}", area_eo(&r));
|
||||
// The bottom operand's own operation is unused.
|
||||
let r2 = boolean_ops([(&a, BoolOp::Exclude), (&b, BoolOp::Union), (&hole, BoolOp::Subtract)]).unwrap();
|
||||
assert!((area_eo(&r2) - 125.0).abs() < 0.5);
|
||||
}
|
||||
|
||||
/// Even-odd area: contours nested in another count negative (holes).
|
||||
fn area_eo(p: &BezPath) -> f64 {
|
||||
let mut total = 0.0;
|
||||
let subs: Vec<BezPath> = split(p);
|
||||
for (i, s) in subs.iter().enumerate() {
|
||||
let inside = subs.iter().enumerate().filter(|(j, o)| *j != i && o.bounding_box().contains_rect(s.bounding_box())).count();
|
||||
total += if inside % 2 == 0 { area(s) } else { -area(s) };
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
fn split(p: &BezPath) -> Vec<BezPath> {
|
||||
use figcraft_geom::kurbo::PathEl;
|
||||
let mut out: Vec<BezPath> = Vec::new();
|
||||
for el in p.elements() {
|
||||
if matches!(el, PathEl::MoveTo(_)) {
|
||||
out.push(BezPath::new());
|
||||
}
|
||||
if let Some(last) = out.last_mut() {
|
||||
last.push(*el);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_uniform_fold_matches_the_group_operation() {
|
||||
let (a, b, c) = (sq(0.0, 0.0, 10.0), sq(5.0, 5.0, 10.0), sq(8.0, 0.0, 4.0));
|
||||
for op in [BoolOp::Union, BoolOp::Subtract, BoolOp::Intersect, BoolOp::Exclude] {
|
||||
let all = boolean(&[a.clone(), b.clone(), c.clone()], op).unwrap();
|
||||
let folded = boolean_ops([(&a, op), (&b, op), (&c, op)]).unwrap();
|
||||
assert!((area_eo(&all) - area_eo(&folded)).abs() < 1e-6, "{op:?}");
|
||||
}
|
||||
// Subtract removes every later operand from the first.
|
||||
let s = boolean(&[a.clone(), b.clone(), c.clone()], BoolOp::Subtract).unwrap();
|
||||
assert!((area_eo(&s) - (100.0 - 25.0 - 8.0)).abs() < 0.5, "{}", area_eo(&s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_steps_and_edge_cases() {
|
||||
let (a, b) = (sq(0.0, 0.0, 10.0), sq(5.0, 5.0, 10.0));
|
||||
assert!((area(&boolean_step(&a, &b, BoolOp::Intersect).unwrap()) - 25.0).abs() < 0.5);
|
||||
assert!((area(&boolean_step(&a, &b, BoolOp::Subtract).unwrap()) - 75.0).abs() < 0.5);
|
||||
// Nothing, and a lone operand (simplified).
|
||||
assert!(boolean_ops(std::iter::empty()).unwrap().elements().is_empty());
|
||||
assert!((area(&boolean_ops([(&a, BoolOp::Subtract)]).unwrap()) - 100.0).abs() < 1e-6);
|
||||
let mut bad = BezPath::new();
|
||||
bad.move_to((f64::NAN, 0.0));
|
||||
bad.line_to((1.0, 1.0));
|
||||
assert!(boolean_ops([(&a, BoolOp::Union), (&bad, BoolOp::Intersect)]).is_err());
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
//! 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:?}");
|
||||
}
|
||||
+46
-27
@@ -2,9 +2,10 @@
|
||||
//! follows a [`WidthProfile`] along each subpath.
|
||||
//!
|
||||
//! The path is flattened into polylines (sampled densely enough for the width to vary smoothly),
|
||||
//! each point is offset along the polyline's normals by the interpolated half-width, the sides are
|
||||
//! joined (round, miter or bevel on the outer side, through the centre on the inner side) and
|
||||
//! capped. The result may overlap itself; fill it with the non-zero rule.
|
||||
//! each point is offset along the polyline's normals by the interpolated half-width of that side
|
||||
//! (the two sides can differ: one-sided widths), the sides are joined (round, miter or bevel on the
|
||||
//! outer side, through the centre on the inner side) and capped. The result may overlap itself;
|
||||
//! fill it with the non-zero rule.
|
||||
|
||||
use figcraft_doc::{StrokeCap, StrokeJoin, StrokeProps, WidthProfile};
|
||||
use figcraft_geom::kurbo::{self, BezPath, PathEl, Point, Vec2};
|
||||
@@ -132,13 +133,22 @@ pub fn nearest(lines: &[Polyline], p: Point) -> Option<(usize, f64, f64)> {
|
||||
lines.iter().enumerate().filter_map(|(i, l)| l.nearest(p).map(|(t, d)| (i, t, d))).min_by(|a, b| a.2.total_cmp(&b.2))
|
||||
}
|
||||
|
||||
/// Half-widths at a sample: `[along the normal, against it]`. The normal is the direction turned
|
||||
/// clockwise on screen (y down), so the first is the right side looking along the path.
|
||||
type Half = [f64; 2];
|
||||
|
||||
/// The half-width of `h` on side `sigma` (+1 = along the normal).
|
||||
fn on(h: Half, sigma: f64) -> f64 {
|
||||
if sigma > 0.0 { h[0] } else { h[1] }
|
||||
}
|
||||
|
||||
/// One straight piece of an offset polyline: its ends, the half-widths there, its direction and
|
||||
/// normal.
|
||||
struct Seg {
|
||||
a: Point,
|
||||
b: Point,
|
||||
ha: f64,
|
||||
hb: f64,
|
||||
ha: Half,
|
||||
hb: Half,
|
||||
d: Vec2,
|
||||
n: Vec2,
|
||||
}
|
||||
@@ -179,8 +189,9 @@ fn arc(out: &mut Vec<Point>, c: Point, from: Vec2, sweep: f64, r0: f64, r1: f64)
|
||||
|
||||
/// The corner between segments `p` and `c` on side `sigma` (+1 = the normal's side).
|
||||
fn join(out: &mut Vec<Point>, p: &Seg, c: &Seg, sigma: f64, style: Style) {
|
||||
let pe = p.b + p.n * (sigma * p.hb);
|
||||
let cs = c.a + c.n * (sigma * c.ha);
|
||||
let (phb, cha) = (on(p.hb, sigma), on(c.ha, sigma));
|
||||
let pe = p.b + p.n * (sigma * phb);
|
||||
let cs = c.a + c.n * (sigma * cha);
|
||||
out.push(pe);
|
||||
let (cross, dot) = (p.d.cross(c.d), p.d.dot(c.d));
|
||||
if cross.abs() < 1e-12 && dot > 0.0 {
|
||||
@@ -193,12 +204,12 @@ fn join(out: &mut Vec<Point>, p: &Seg, c: &Seg, sigma: f64, style: Style) {
|
||||
if outer {
|
||||
let theta = if cross.abs() < 1e-12 { std::f64::consts::PI } else { cross.atan2(dot) };
|
||||
match style.join {
|
||||
StrokeJoin::Round => arc(out, v, pe - v, theta, p.hb, c.ha),
|
||||
StrokeJoin::Round => arc(out, v, pe - v, theta, phb, cha),
|
||||
StrokeJoin::Miter => {
|
||||
let half = (theta / 2.0).cos();
|
||||
let bis = p.n + c.n;
|
||||
if half > 1e-6 && 1.0 / half <= style.miter_limit && bis.hypot() > 1e-9 {
|
||||
out.push(v + bis * (sigma * (p.hb + c.ha) / 2.0 / half / bis.hypot()));
|
||||
out.push(v + bis * (sigma * (phb + cha) / 2.0 / half / bis.hypot()));
|
||||
}
|
||||
}
|
||||
StrokeJoin::Bevel => {}
|
||||
@@ -217,25 +228,26 @@ fn side(segs: &[Seg], sigma: f64, closed: bool, style: Style) -> Vec<Point> {
|
||||
let prev = if j == 0 { closed.then(|| segs.last()).flatten() } else { segs.get(j - 1) };
|
||||
match prev {
|
||||
Some(p) => join(&mut out, p, s, sigma, style),
|
||||
None => out.push(s.a + s.n * (sigma * s.ha)),
|
||||
None => out.push(s.a + s.n * (sigma * on(s.ha, sigma))),
|
||||
}
|
||||
}
|
||||
if !closed && let Some(l) = segs.last() {
|
||||
out.push(l.b + l.n * (sigma * l.hb));
|
||||
out.push(l.b + l.n * (sigma * on(l.hb, sigma)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A cap at `c` facing `dir` (unit, outwards) with half-width `h`: from the `dir.turn_90()` side
|
||||
/// to the other one.
|
||||
fn cap(out: &mut Vec<Point>, c: Point, dir: Vec2, h: f64, kind: StrokeCap) {
|
||||
/// A cap at `c` facing `dir` (unit, outwards) from the `dir.turn_90()` side, `from` wide, to the
|
||||
/// other side, `to` wide. Lopsided ends are capped around the middle of the end.
|
||||
fn cap(out: &mut Vec<Point>, c: Point, dir: Vec2, from: f64, to: f64, kind: StrokeCap) {
|
||||
let n = dir.turn_90();
|
||||
let (m, r) = (c + n * ((from - to) / 2.0), (from + to) / 2.0);
|
||||
match cap_shape(kind) {
|
||||
StrokeCap::Square => {
|
||||
out.push(c + n * h + dir * h);
|
||||
out.push(c - n * h + dir * h);
|
||||
out.push(m + n * r + dir * r);
|
||||
out.push(m - n * r + dir * r);
|
||||
}
|
||||
StrokeCap::Round => arc(out, c, n, -std::f64::consts::PI, h, h),
|
||||
StrokeCap::Round => arc(out, m, n, -std::f64::consts::PI, r, r),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -251,7 +263,7 @@ fn push_contour(path: &mut BezPath, pts: impl IntoIterator<Item = Point>) {
|
||||
}
|
||||
|
||||
/// Outline one piece (a subpath or a dash) with its half-widths.
|
||||
fn outline_piece(path: &mut BezPath, pts: &[Point], hw: &[f64], closed: bool, style: Style) {
|
||||
fn outline_piece(path: &mut BezPath, pts: &[Point], hw: &[Half], closed: bool, style: Style) {
|
||||
let segs: Vec<Seg> = pts
|
||||
.windows(2)
|
||||
.zip(hw.windows(2))
|
||||
@@ -266,11 +278,12 @@ fn outline_piece(path: &mut BezPath, pts: &[Point], hw: &[f64], closed: bool, st
|
||||
.collect();
|
||||
let (Some(first), Some(last)) = (segs.first(), segs.last()) else {
|
||||
// A zero-length piece is a dot with round caps.
|
||||
if let (Some(p), Some(h)) = (pts.first(), hw.first())
|
||||
let r = hw.first().map_or(0.0, |h| (h[0] + h[1]) / 2.0);
|
||||
if let Some(p) = pts.first()
|
||||
&& cap_shape(style.cap_start) == StrokeCap::Round
|
||||
&& *h > 0.0
|
||||
&& r > 0.0
|
||||
{
|
||||
path.extend(kurbo::Shape::path_elements(&kurbo::Circle::new(*p, *h), FLATTEN_TOL));
|
||||
path.extend(kurbo::Shape::path_elements(&kurbo::Circle::new(*p, r), FLATTEN_TOL));
|
||||
}
|
||||
return;
|
||||
};
|
||||
@@ -282,18 +295,18 @@ fn outline_piece(path: &mut BezPath, pts: &[Point], hw: &[f64], closed: bool, st
|
||||
return;
|
||||
}
|
||||
let mut ring = left;
|
||||
cap(&mut ring, last.b, last.d, last.hb, style.cap_end);
|
||||
cap(&mut ring, last.b, last.d, last.hb[0], last.hb[1], style.cap_end);
|
||||
ring.extend(right.into_iter().rev());
|
||||
cap(&mut ring, first.a, -first.d, first.ha, style.cap_start);
|
||||
cap(&mut ring, first.a, -first.d, first.ha[1], first.ha[0], style.cap_start);
|
||||
push_contour(path, ring);
|
||||
}
|
||||
|
||||
/// Split a sampled polyline into dashes (`pattern` alternates dash and gap lengths).
|
||||
fn dashes(pts: &[Point], hw: &[f64], pattern: &[f64]) -> Vec<(Vec<Point>, Vec<f64>)> {
|
||||
fn dashes(pts: &[Point], hw: &[Half], pattern: &[f64]) -> Vec<(Vec<Point>, Vec<Half>)> {
|
||||
let mut out = Vec::new();
|
||||
let Some(&first) = pattern.first() else { return out };
|
||||
let (mut idx, mut left, mut on) = (0usize, first, true);
|
||||
let mut cur: (Vec<Point>, Vec<f64>) = (pts.iter().take(1).copied().collect(), hw.iter().take(1).copied().collect());
|
||||
let mut cur: (Vec<Point>, Vec<Half>) = (pts.iter().take(1).copied().collect(), hw.iter().take(1).copied().collect());
|
||||
for (p, h) in pts.windows(2).zip(hw.windows(2)) {
|
||||
let (a, b, ha, hb) = (p[0], p[1], h[0], h[1]);
|
||||
let len = (b - a).hypot();
|
||||
@@ -301,7 +314,7 @@ fn dashes(pts: &[Point], hw: &[f64], pattern: &[f64]) -> Vec<(Vec<Point>, Vec<f6
|
||||
while len - used > left {
|
||||
used += left;
|
||||
let f = if len > 0.0 { used / len } else { 0.0 };
|
||||
let (q, hq) = (a.lerp(b, f), ha + (hb - ha) * f);
|
||||
let (q, hq) = (a.lerp(b, f), [ha[0] + (hb[0] - ha[0]) * f, ha[1] + (hb[1] - ha[1]) * f]);
|
||||
if on {
|
||||
cur.0.push(q);
|
||||
cur.1.push(hq);
|
||||
@@ -381,7 +394,13 @@ pub fn variable_outline(path: &BezPath, width: f64, props: &StrokeProps, profile
|
||||
continue;
|
||||
}
|
||||
let (pts, lens) = resample(line, len / SAMPLES_PER_SUBPATH, &mut budget)?;
|
||||
let hw: Vec<f64> = lens.iter().map(|s| 0.5 * width * profile.scale_at(s / len)).collect();
|
||||
let hw: Vec<Half> = lens
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let (left, right) = profile.sides_at(s / len);
|
||||
[0.5 * width * right, 0.5 * width * left]
|
||||
})
|
||||
.collect();
|
||||
match &pattern {
|
||||
Some(pat) if len / pat.iter().sum::<f64>() * (pat.len() as f64) < MAX_DASHES as f64 => {
|
||||
for (p, h) in dashes(&pts, &hw, pat) {
|
||||
|
||||
Reference in New Issue
Block a user