mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
Add the linesweeper 0.5.0 package to vendor/ with no file changed, so that a fix to it can reach the Craft apps through this repository without waiting for a linesweeper release. The GitHub repository its manifest names is archived, and its README says development moved off GitHub. Leave out the test font (fonts live in craft-fonts), the logo artwork, the snapshot images and the package's repository tooling. SOURCE.json lists each with its hash. Add a workspace exclude, a [patch.crates-io] entry, SOURCE.json with the archive hash and per-file hashes, NOTICE and ATTRIBUTION.md rows, and the package's tests in the backend test script. craft-prepress's flatten feature now builds against the copy, so Cargo.lock drops the package's registry source and checksum.
343 lines
11 KiB
Rust
343 lines
11 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use anyhow::anyhow;
|
|
use clap::{Args, Parser};
|
|
use kurbo::BezPath;
|
|
use skrifa::{FontRef, MetadataProvider};
|
|
use svg::Document;
|
|
|
|
use linesweeper::{
|
|
Point, generators,
|
|
topology::{BinaryWindingNumber, Topology},
|
|
};
|
|
use linesweeper_util::{outline_to_bezpath, svg_to_bezpaths};
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
enum Op {
|
|
Union,
|
|
Intersection,
|
|
Xor,
|
|
Difference,
|
|
ReverseDifference,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, clap::ValueEnum)]
|
|
enum Example {
|
|
Checkerboard,
|
|
SlantedCheckerboard,
|
|
Slanties,
|
|
Star,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
struct Cli {
|
|
#[arg(long)]
|
|
output: PathBuf,
|
|
|
|
#[command(flatten)]
|
|
input: Input,
|
|
|
|
#[arg(long)]
|
|
non_zero: bool,
|
|
|
|
#[arg(long)]
|
|
path_bool: bool,
|
|
|
|
#[arg(long)]
|
|
epsilon: Option<f64>,
|
|
}
|
|
|
|
#[derive(Args, Debug)]
|
|
#[group(required = true, multiple = false)]
|
|
struct Input {
|
|
input: Option<PathBuf>,
|
|
|
|
#[arg(long)]
|
|
example: Option<Example>,
|
|
|
|
#[arg(long)]
|
|
char: Option<Vec<char>>,
|
|
}
|
|
|
|
fn points_to_bez(ps: Vec<Point>) -> BezPath {
|
|
let mut ps = ps.into_iter();
|
|
let mut ret = BezPath::new();
|
|
if let Some(p) = ps.next() {
|
|
ret.move_to((p.x, p.y));
|
|
}
|
|
for p in ps {
|
|
ret.line_to((p.x, p.y));
|
|
}
|
|
ret.close_path();
|
|
ret
|
|
}
|
|
|
|
fn contours_to_bezs((cs0, cs1): (Vec<Vec<Point>>, Vec<Vec<Point>>)) -> (BezPath, BezPath) {
|
|
(
|
|
cs0.into_iter().flat_map(points_to_bez).collect(),
|
|
cs1.into_iter().flat_map(points_to_bez).collect(),
|
|
)
|
|
}
|
|
|
|
fn get_contours(input: &Input) -> anyhow::Result<(BezPath, BezPath)> {
|
|
match (&input.input, &input.example, &input.char) {
|
|
(Some(path), None, None) => {
|
|
let input = std::fs::read_to_string(path)?;
|
|
let tree = usvg::Tree::from_str(&input, &usvg::Options::default())?;
|
|
let mut contours = svg_to_bezpaths(&tree).into_iter();
|
|
let first = contours.next().unwrap();
|
|
let rest = contours.flatten().collect();
|
|
Ok((first, rest))
|
|
}
|
|
(None, Some(example), None) => match example {
|
|
Example::Checkerboard => Ok(contours_to_bezs(generators::checkerboard(10))),
|
|
Example::SlantedCheckerboard => {
|
|
Ok(contours_to_bezs(generators::slanted_checkerboard(10)))
|
|
}
|
|
Example::Slanties => Ok(contours_to_bezs(generators::slanties(10))),
|
|
Example::Star => Ok(generators::star(20)),
|
|
},
|
|
(None, None, Some(chars)) => {
|
|
let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
|
let font_path = format!("{ws}/tests/fonts/Inconsolata-Regular.ttf");
|
|
let data = std::fs::read(&font_path).unwrap();
|
|
let font_ref = FontRef::new(&data).unwrap();
|
|
let charmap = font_ref.charmap();
|
|
let paths = chars
|
|
.iter()
|
|
.map(|c| -> anyhow::Result<_> {
|
|
let id = charmap.map(*c).ok_or(anyhow!("{c} not in the charmap"))?;
|
|
let outline = font_ref
|
|
.outline_glyphs()
|
|
.get(id)
|
|
.ok_or(anyhow!("missing glyph for {c}"))?;
|
|
Ok(outline_to_bezpath(outline))
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
Ok((paths[0].clone(), paths[1..].iter().flatten().collect()))
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
pub fn main() -> anyhow::Result<()> {
|
|
let args = Cli::parse();
|
|
let (shape_a, shape_b) = get_contours(&args.input)?;
|
|
|
|
let eps = args.epsilon.unwrap_or(0.1);
|
|
let top = Topology::from_paths_binary(&shape_a, &shape_b, eps)?;
|
|
let bbox = top.bounding_box();
|
|
let min_x = bbox.min_x();
|
|
let min_y = bbox.min_y();
|
|
let max_x = bbox.max_x();
|
|
let max_y = bbox.max_y();
|
|
let pad = 1.0 + eps;
|
|
let one_width = max_x - min_x + 2.0 * pad;
|
|
let one_height = max_y - min_y + 2.0 * pad;
|
|
let stroke_width = (max_y - min_y).max(max_x - max_y) / 512.0;
|
|
let mut document = svg::Document::new().set(
|
|
"viewBox",
|
|
(min_x - pad, min_y - pad, one_width * 3.0, one_height * 2.0),
|
|
);
|
|
|
|
// Draw the original document.
|
|
for c in [&shape_a, &shape_b] {
|
|
let mut data = svg::node::element::path::Data::new();
|
|
for el in c {
|
|
let p = |point: kurbo::Point| (point.x, point.y);
|
|
data = match el {
|
|
kurbo::PathEl::MoveTo(p0) => data.move_to(p(p0)),
|
|
kurbo::PathEl::LineTo(p0) => data.line_to(p(p0)),
|
|
kurbo::PathEl::QuadTo(p0, p1) => data.quadratic_curve_to((p(p0), p(p1))),
|
|
kurbo::PathEl::CurveTo(p0, p1, p2) => data.cubic_curve_to((p(p0), p(p1), p(p2))),
|
|
kurbo::PathEl::ClosePath => data.close(),
|
|
};
|
|
}
|
|
|
|
let path = svg::node::element::Path::new()
|
|
.set("stroke", "black")
|
|
.set("stroke-width", stroke_width)
|
|
.set("stroke-linecap", "round")
|
|
.set("stroke-linejoin", "round")
|
|
.set("opacity", 0.2)
|
|
.set("fill", "none")
|
|
.set("d", data);
|
|
document = document.add(path);
|
|
}
|
|
|
|
let add_one = |doc, op, x_off, y_off| {
|
|
if args.path_bool {
|
|
add_path_bool_op(doc, op, &shape_a, &shape_b, x_off, y_off, stroke_width)
|
|
} else {
|
|
add_op(doc, op, args.non_zero, &top, x_off, y_off, stroke_width)
|
|
}
|
|
};
|
|
|
|
document = add_one(document, Op::Union, one_width, 0.0);
|
|
document = add_one(document, Op::Intersection, one_width * 2.0, 0.0);
|
|
document = add_one(document, Op::Xor, 0.0, one_height);
|
|
document = add_one(document, Op::Difference, one_width, one_height);
|
|
document = add_one(document, Op::ReverseDifference, one_width * 2.0, one_height);
|
|
|
|
svg::save(&args.output, &document)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn add_op(
|
|
mut doc: Document,
|
|
op: Op,
|
|
non_zero: bool,
|
|
top: &Topology<BinaryWindingNumber>,
|
|
x_off: f64,
|
|
y_off: f64,
|
|
stroke_width: f64,
|
|
) -> Document {
|
|
let contours = top.contours(|w| {
|
|
let inside = |winding| {
|
|
if non_zero {
|
|
winding != 0
|
|
} else {
|
|
winding % 2 != 0
|
|
}
|
|
};
|
|
|
|
match op {
|
|
Op::Union => inside(w.shape_a) || inside(w.shape_b),
|
|
Op::Intersection => inside(w.shape_a) && inside(w.shape_b),
|
|
Op::Xor => inside(w.shape_a) != inside(w.shape_b),
|
|
Op::Difference => inside(w.shape_a) && !inside(w.shape_b),
|
|
Op::ReverseDifference => inside(w.shape_b) && !inside(w.shape_a),
|
|
}
|
|
});
|
|
|
|
let colors = [
|
|
"#005F73", "#0A9396", "#94D2BD", "#E9D8A6", "#EE9B00", "#CA6702", "#BB3E03", "#AE2012",
|
|
"#9B2226",
|
|
];
|
|
|
|
let mut color_idx = 0;
|
|
for group in contours.grouped() {
|
|
let mut data = svg::node::element::path::Data::new();
|
|
|
|
for contour_idx in group {
|
|
let path = &contours[contour_idx].path;
|
|
for el in path.iter() {
|
|
data = match el {
|
|
kurbo::PathEl::MoveTo(p) => data.move_to((p.x + x_off, p.y + y_off)),
|
|
kurbo::PathEl::LineTo(p) => data.line_to((p.x + x_off, p.y + y_off)),
|
|
kurbo::PathEl::QuadTo(p0, p1) => data.quadratic_curve_to((
|
|
(p0.x + x_off, p0.y + y_off),
|
|
(p1.x + x_off, p1.y + y_off),
|
|
)),
|
|
kurbo::PathEl::CurveTo(p0, p1, p2) => data.cubic_curve_to((
|
|
(p0.x + x_off, p0.y + y_off),
|
|
(p1.x + x_off, p1.y + y_off),
|
|
(p2.x + x_off, p2.y + y_off),
|
|
)),
|
|
kurbo::PathEl::ClosePath => data.close(),
|
|
};
|
|
}
|
|
data = data.close();
|
|
}
|
|
let path = svg::node::element::Path::new()
|
|
.set("d", data)
|
|
.set("stroke", "black")
|
|
.set("stroke-width", stroke_width)
|
|
.set("stroke-linecap", "round")
|
|
.set("stroke-linejoin", "round")
|
|
.set("fill", colors[color_idx]);
|
|
doc = doc.add(path);
|
|
color_idx = (color_idx + 1) % colors.len();
|
|
}
|
|
doc
|
|
}
|
|
|
|
fn bezpath_to_path_bool(p: &BezPath) -> Vec<path_bool::PathSegment> {
|
|
let mut ret = Vec::new();
|
|
for seg in p.segments() {
|
|
let dv = |p: kurbo::Point| glam::DVec2::from((p.x, p.y));
|
|
let seg = match seg {
|
|
kurbo::PathSeg::Line(line) => path_bool::PathSegment::Line(dv(line.p0), dv(line.p1)),
|
|
kurbo::PathSeg::Quad(q) => {
|
|
path_bool::PathSegment::Quadratic(dv(q.p0), dv(q.p1), dv(q.p2))
|
|
}
|
|
kurbo::PathSeg::Cubic(c) => {
|
|
path_bool::PathSegment::Cubic(dv(c.p0), dv(c.p1), dv(c.p2), dv(c.p3))
|
|
}
|
|
};
|
|
ret.push(seg);
|
|
}
|
|
ret
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn add_path_bool_op(
|
|
mut doc: Document,
|
|
op: Op,
|
|
shape_a: &BezPath,
|
|
shape_b: &BezPath,
|
|
x_off: f64,
|
|
y_off: f64,
|
|
stroke_width: f64,
|
|
) -> Document {
|
|
let mut shape_a = bezpath_to_path_bool(shape_a);
|
|
let mut shape_b = bezpath_to_path_bool(shape_b);
|
|
let fill = path_bool::FillRule::EvenOdd;
|
|
let op = match op {
|
|
Op::Union => path_bool::PathBooleanOperation::Union,
|
|
Op::Intersection => path_bool::PathBooleanOperation::Intersection,
|
|
Op::Xor => path_bool::PathBooleanOperation::Exclusion,
|
|
Op::Difference => path_bool::PathBooleanOperation::Difference,
|
|
Op::ReverseDifference => {
|
|
std::mem::swap(&mut shape_a, &mut shape_b);
|
|
path_bool::PathBooleanOperation::Difference
|
|
}
|
|
};
|
|
let result = path_bool::path_boolean(&shape_a, fill, &shape_b, fill, op).unwrap();
|
|
|
|
let colors = [
|
|
"#005F73", "#0A9396", "#94D2BD", "#E9D8A6", "#EE9B00", "#CA6702", "#BB3E03", "#AE2012",
|
|
"#9B2226",
|
|
];
|
|
|
|
let mut color_idx = 0;
|
|
for path in result {
|
|
let mut data = svg::node::element::path::Data::new();
|
|
|
|
if path.is_empty() {
|
|
continue;
|
|
}
|
|
let start = path[0].start();
|
|
data = data.move_to((start.x + x_off, start.y + y_off));
|
|
for seg in path {
|
|
data = match seg {
|
|
path_bool::PathSegment::Line(_, p) => data.line_to((p.x + x_off, p.y + y_off)),
|
|
path_bool::PathSegment::Quadratic(_, p0, p1) => data.quadratic_curve_to((
|
|
(p0.x + x_off, p0.y + y_off),
|
|
(p1.x + x_off, p1.y + y_off),
|
|
)),
|
|
path_bool::PathSegment::Cubic(_, p0, p1, p2) => data.cubic_curve_to((
|
|
(p0.x + x_off, p0.y + y_off),
|
|
(p1.x + x_off, p1.y + y_off),
|
|
(p2.x + x_off, p2.y + y_off),
|
|
)),
|
|
path_bool::PathSegment::Arc(..) => unimplemented!(),
|
|
};
|
|
}
|
|
data = data.close();
|
|
let path = svg::node::element::Path::new()
|
|
.set("d", data)
|
|
.set("stroke", "black")
|
|
.set("stroke-width", stroke_width)
|
|
.set("stroke-linecap", "round")
|
|
.set("stroke-linejoin", "round")
|
|
.set("fill", colors[color_idx]);
|
|
doc = doc.add(path);
|
|
color_idx = (color_idx + 1) % colors.len();
|
|
}
|
|
doc
|
|
}
|