M2/M8/M9/M4.4: drawing + transform tools, live effects, PDF export/import, file/batch commands; UI restyled to measured Illustrator metrics (Medium Dark, categorized toolbar, hint bar, task bar, Layers)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-09-30 06:55:49 -04:00
co-authored by Claude Opus 5.5
parent d0007a604d
commit 6d21d36d16
64 changed files with 11318 additions and 865 deletions
+39 -190
View File
@@ -63,14 +63,9 @@ pub const INTRA_LAYER_ORDER: &[&[&str]] = &[&["geom", "color"], &["pathops", "ef
fn intra_layer_allowed(from: &str, to: &str) -> bool {
let (from, to) = (short_name(from), short_name(to));
INTRA_LAYER_ORDER.iter().any(|chain| {
match (
chain.iter().position(|n| *n == from),
chain.iter().position(|n| *n == to),
) {
(Some(f), Some(t)) => t < f,
_ => false,
}
INTRA_LAYER_ORDER.iter().any(|chain| match (chain.iter().position(|n| *n == from), chain.iter().position(|n| *n == to)) {
(Some(f), Some(t)) => t < f,
_ => false,
})
}
@@ -120,64 +115,30 @@ pub struct Crate {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Violation {
Unregistered {
krate: String,
},
Upward {
krate: String,
dep: String,
from: u8,
to: u8,
kind: DepKind,
},
StandaloneHasWorkspaceDep {
krate: String,
dep: String,
},
TestkitAsNormalDep {
krate: String,
},
UiBelowL6 {
krate: String,
dep: String,
layer: u8,
},
Unregistered { krate: String },
Upward { krate: String, dep: String, from: u8, to: u8, kind: DepKind },
StandaloneHasWorkspaceDep { krate: String, dep: String },
TestkitAsNormalDep { krate: String },
UiBelowL6 { krate: String, dep: String, layer: u8 },
}
impl std::fmt::Display for Violation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Violation::Unregistered { krate } => write!(
f,
"{krate}: unknown workspace crate; register it in xtask/src/layers.rs TABLE (see plan/architecture.md §3)"
),
Violation::Upward {
krate,
dep,
from,
to,
kind,
} => write!(
f,
"{krate} (L{from}) -> {dep} (L{to}) [{kind:?}]: may only depend on strictly lower layers"
),
Violation::Unregistered { krate } => {
write!(f, "{krate}: unknown workspace crate; register it in xtask/src/layers.rs TABLE (see plan/architecture.md §3)")
}
Violation::Upward { krate, dep, from, to, kind } => {
write!(f, "{krate} (L{from}) -> {dep} (L{to}) [{kind:?}]: may only depend on strictly lower layers")
}
Violation::StandaloneHasWorkspaceDep { krate, dep } => {
write!(
f,
"{krate}: standalone crate must not depend on workspace crate {dep}"
)
write!(f, "{krate}: standalone crate must not depend on workspace crate {dep}")
}
Violation::TestkitAsNormalDep { krate } => {
write!(
f,
"{krate}: drawcraft-testkit may only be a dev-dependency"
)
write!(f, "{krate}: drawcraft-testkit may only be a dev-dependency")
}
Violation::UiBelowL6 { krate, dep, layer } => {
write!(
f,
"{krate} (L{layer}) depends on UI crate `{dep}`; UI toolkits are only allowed in L6+"
)
write!(f, "{krate} (L{layer}) depends on UI crate `{dep}`; UI toolkits are only allowed in L6+")
}
}
}
@@ -188,9 +149,7 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
let mut out = Vec::new();
for c in crates {
let Some(class) = classify(&c.name) else {
out.push(Violation::Unregistered {
krate: c.name.clone(),
});
out.push(Violation::Unregistered { krate: c.name.clone() });
continue;
};
if class == Class::Exempt {
@@ -204,10 +163,7 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
}
if d.workspace {
if class == Class::Standalone {
out.push(Violation::StandaloneHasWorkspaceDep {
krate: c.name.clone(),
dep: d.name.clone(),
});
out.push(Violation::StandaloneHasWorkspaceDep { krate: c.name.clone(), dep: d.name.clone() });
continue;
}
match classify(&d.name) {
@@ -215,9 +171,7 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
None => {}
Some(Class::Testkit) => {
if d.kind != DepKind::Dev {
out.push(Violation::TestkitAsNormalDep {
krate: c.name.clone(),
});
out.push(Violation::TestkitAsNormalDep { krate: c.name.clone() });
}
}
Some(dc) => {
@@ -234,11 +188,7 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
}
}
} else if layer < UI_MIN_LAYER && is_ui_crate(&d.name) {
out.push(Violation::UiBelowL6 {
krate: c.name.clone(),
dep: d.name.clone(),
layer,
});
out.push(Violation::UiBelowL6 { krate: c.name.clone(), dep: d.name.clone(), layer });
}
}
}
@@ -249,9 +199,7 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
/// Build the model from `cargo metadata --format-version 1 --no-deps`.
pub fn from_metadata(meta: &Value) -> Result<Vec<Crate>, String> {
let pkgs = meta["packages"]
.as_array()
.ok_or("metadata: no packages array")?;
let pkgs = meta["packages"].as_array().ok_or("metadata: no packages array")?;
let members: Vec<&str> = pkgs.iter().filter_map(|p| p["name"].as_str()).collect();
let mut out = Vec::new();
for p in pkgs {
@@ -265,11 +213,7 @@ pub fn from_metadata(meta: &Value) -> Result<Vec<Crate>, String> {
_ => DepKind::Normal,
};
let workspace = members.contains(&dname.as_str()) || d["path"].is_string();
deps.push(Dep {
name: dname,
kind,
workspace,
});
deps.push(Dep { name: dname, kind, workspace });
}
out.push(Crate { name, deps });
}
@@ -292,17 +236,7 @@ mod tests {
use super::*;
fn c(name: &str, deps: &[(&str, DepKind, bool)]) -> Crate {
Crate {
name: name.into(),
deps: deps
.iter()
.map(|(n, k, w)| Dep {
name: (*n).into(),
kind: *k,
workspace: *w,
})
.collect(),
}
Crate { name: name.into(), deps: deps.iter().map(|(n, k, w)| Dep { name: (*n).into(), kind: *k, workspace: *w }).collect() }
}
use DepKind::*;
@@ -311,17 +245,8 @@ mod tests {
let g = [
c("drawcraft-geom", &[("kurbo", Normal, false)]),
c("drawcraft-doc", &[("drawcraft-geom", Normal, true)]),
c(
"drawcraft-engine",
&[
("drawcraft-doc", Normal, true),
("drawcraft-testkit", Dev, true),
],
),
c(
"drawcraft-ui-egui",
&[("drawcraft-engine", Normal, true), ("egui", Normal, false)],
),
c("drawcraft-engine", &[("drawcraft-doc", Normal, true), ("drawcraft-testkit", Dev, true)]),
c("drawcraft-ui-egui", &[("drawcraft-engine", Normal, true), ("egui", Normal, false)]),
c("drawcraft-cli", &[("drawcraft-ui-egui", Normal, true)]),
];
assert!(check(&g).is_empty(), "{:?}", check(&g));
@@ -335,30 +260,20 @@ mod tests {
#[test]
fn sideways_dependency_flagged() {
let v = check(&[c("drawcraft-ops", &[("drawcraft-algo", Normal, true)])]);
let v = check(&[c("drawcraft-text", &[("drawcraft-pathops", Normal, true)])]);
assert!(matches!(v[..], [Violation::Upward { from: 2, to: 2, .. }]));
}
#[test]
fn l0_foundation_chain_allowed_one_way() {
assert!(
check(&[c(
"drawcraft-raster",
&[
("drawcraft-color", Normal, true),
("drawcraft-geom", Normal, true)
]
)])
.is_empty()
);
assert!(check(&[c("drawcraft-color", &[("drawcraft-geom", Normal, true)])]).is_empty());
let v = check(&[c("drawcraft-geom", &[("drawcraft-raster", Normal, true)])]);
let v = check(&[c("drawcraft-geom", &[("drawcraft-color", Normal, true)])]);
assert!(matches!(v[..], [Violation::Upward { from: 0, to: 0, .. }]));
}
#[test]
fn self_dev_dependency_ignored() {
assert!(check(&[c("drawcraft-psd", &[("drawcraft-psd", Dev, true)])]).is_empty());
assert!(check(&[c("drawcraft-doc", &[("drawcraft-doc", Dev, true)])]).is_empty());
}
#[test]
@@ -367,103 +282,37 @@ mod tests {
assert!(matches!(v[..], [Violation::Upward { kind: Dev, .. }]));
}
#[test]
fn standalone_crates_have_no_workspace_deps() {
for s in [
"drawcraft-psd",
"drawcraft-codecs",
"drawcraft-adobe-assets",
] {
let v = check(&[c(s, &[("drawcraft-geom", Normal, true)])]);
assert!(
matches!(v[..], [Violation::StandaloneHasWorkspaceDep { .. }]),
"{s}"
);
assert!(check(&[c(s, &[("image", Normal, false)])]).is_empty());
}
}
#[test]
fn standalone_is_usable_from_higher_layers() {
assert!(check(&[c("drawcraft-io", &[("drawcraft-psd", Normal, true)])]).is_empty());
let v = check(&[c("drawcraft-geom", &[("drawcraft-codecs", Normal, true)])]);
assert!(matches!(v[..], [Violation::Upward { from: 0, to: 0, .. }]));
}
#[test]
fn ui_crates_below_l6_flagged() {
for dep in [
"egui",
"eframe",
"winit",
"egui_kittest",
"rfd",
"bevy_ecs",
"bevy",
] {
for dep in ["egui", "eframe", "winit", "egui_kittest", "rfd", "bevy_ecs", "bevy"] {
let v = check(&[c("drawcraft-engine", &[(dep, Normal, false)])]);
assert!(
matches!(v[..], [Violation::UiBelowL6 { layer: 5, .. }]),
"{dep}"
);
assert!(matches!(v[..], [Violation::UiBelowL6 { layer: 5, .. }]), "{dep}");
}
assert!(
check(&[c(
"drawcraft-engine",
&[("egui_extras_not", Normal, false)]
)])
.is_empty()
);
assert!(check(&[c("drawcraft-platform", &[("winit", Normal, false)])]).is_empty());
assert!(check(&[c("drawcraft-engine", &[("egui_extras_not", Normal, false)])]).is_empty());
assert!(check(&[c("drawcraft-mcp", &[("winit", Normal, false)])]).is_empty());
}
#[test]
fn unregistered_crate_is_error() {
let v = check(&[c("drawcraft-mystery", &[])]);
assert!(
matches!(&v[..], [Violation::Unregistered { krate }] if krate == "drawcraft-mystery")
);
assert!(matches!(&v[..], [Violation::Unregistered { krate }] if krate == "drawcraft-mystery"));
assert!(v[0].to_string().contains("register"));
}
#[test]
fn testkit_only_as_dev_dependency() {
let v = check(&[c(
"drawcraft-raster",
&[("drawcraft-testkit", Normal, true)],
)]);
let v = check(&[c("drawcraft-render", &[("drawcraft-testkit", Normal, true)])]);
assert!(matches!(v[..], [Violation::TestkitAsNormalDep { .. }]));
assert!(check(&[c("drawcraft-raster", &[("drawcraft-testkit", Dev, true)])]).is_empty());
assert!(check(&[c("drawcraft-render", &[("drawcraft-testkit", Dev, true)])]).is_empty());
// testkit itself may use anything up to L5 but not L6 crates.
assert!(
check(&[c(
"drawcraft-testkit",
&[("drawcraft-engine", Normal, true)]
)])
.is_empty()
);
assert!(
!check(&[c(
"drawcraft-testkit",
&[("drawcraft-ui-egui", Normal, true)]
)])
.is_empty()
);
assert!(check(&[c("drawcraft-testkit", &[("drawcraft-engine", Normal, true)])]).is_empty());
assert!(!check(&[c("drawcraft-testkit", &[("drawcraft-ui-egui", Normal, true)])]).is_empty());
}
#[test]
fn apps_and_xtask_exempt() {
for app in ["drawcraft", "drawcraft-cli", "drawcraft-web", "xtask"] {
assert!(
check(&[c(
app,
&[
("egui", Normal, false),
("drawcraft-ui-egui", Normal, true)
]
)])
.is_empty()
);
assert!(check(&[c(app, &[("egui", Normal, false), ("drawcraft-ui-egui", Normal, true)])]).is_empty());
}
}
+12 -61
View File
@@ -47,10 +47,7 @@ fn main() -> ExitCode {
/// Workspace root (parent of the xtask crate).
pub fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask has a parent dir")
.to_path_buf()
Path::new(env!("CARGO_MANIFEST_DIR")).parent().expect("xtask has a parent dir").to_path_buf()
}
pub fn cargo() -> Command {
@@ -61,26 +58,14 @@ pub fn cargo() -> Command {
fn run(mut cmd: Command, what: &str) -> Result<(), String> {
eprintln!("$ {what}");
let status = cmd
.status()
.map_err(|e| format!("{what}: failed to spawn: {e}"))?;
if status.success() {
Ok(())
} else {
Err(format!("{what}: exited with {status}"))
}
let status = cmd.status().map_err(|e| format!("{what}: failed to spawn: {e}"))?;
if status.success() { Ok(()) } else { Err(format!("{what}: exited with {status}")) }
}
pub fn metadata() -> Result<serde_json::Value, String> {
let out = cargo()
.args(["metadata", "--format-version", "1", "--no-deps"])
.output()
.map_err(|e| format!("cargo metadata: {e}"))?;
let out = cargo().args(["metadata", "--format-version", "1", "--no-deps"]).output().map_err(|e| format!("cargo metadata: {e}"))?;
if !out.status.success() {
return Err(format!(
"cargo metadata failed:\n{}",
String::from_utf8_lossy(&out.stderr)
));
return Err(format!("cargo metadata failed:\n{}", String::from_utf8_lossy(&out.stderr)));
}
serde_json::from_slice(&out.stdout).map_err(|e| format!("cargo metadata: bad JSON: {e}"))
}
@@ -103,12 +88,7 @@ fn cmd_layers() -> Result<(), String> {
format!("{}{k}", layers::short_name(&d.name))
})
.collect();
println!(
"{:<28} {:<14} {}",
c.name,
layers::describe(layers::classify(&c.name)),
ws.join(", ")
);
println!("{:<28} {:<14} {}", c.name, layers::describe(layers::classify(&c.name)), ws.join(", "));
}
let violations = layers::check(&crates);
println!();
@@ -145,11 +125,7 @@ fn cmd_wasm() -> Result<(), String> {
for pkg in &set {
let mut c = cargo();
c.args(["check", "--target", "wasm32-unknown-unknown", "-p", pkg]);
let ok = run(
c,
&format!("cargo check --target wasm32-unknown-unknown -p {pkg}"),
)
.is_ok();
let ok = run(c, &format!("cargo check --target wasm32-unknown-unknown -p {pkg}")).is_ok();
results.push((pkg.clone(), ok));
}
println!("\nwasm32-unknown-unknown check:");
@@ -157,11 +133,7 @@ fn cmd_wasm() -> Result<(), String> {
println!(" {:<6} {p}", if *ok { "ok" } else { "FAIL" });
}
let failed = results.iter().filter(|r| !r.1).count();
if failed == 0 {
Ok(())
} else {
Err(format!("{failed} crate(s) failed the wasm check"))
}
if failed == 0 { Ok(()) } else { Err(format!("{failed} crate(s) failed the wasm check")) }
}
fn cmd_ci() -> Result<(), String> {
@@ -179,14 +151,7 @@ fn cmd_ci() -> Result<(), String> {
"clippy",
Box::new(|| {
let mut c = cargo();
c.args([
"clippy",
"--workspace",
"--all-targets",
"--",
"-D",
"warnings",
]);
c.args(["clippy", "--workspace", "--all-targets", "--", "-D", "warnings"]);
run(c, "cargo clippy --workspace --all-targets -- -D warnings")
}),
),
@@ -217,11 +182,7 @@ fn cmd_ci() -> Result<(), String> {
}
done.push(*name);
}
println!(
"\nCI summary: all {} steps passed ({})",
done.len(),
done.join(", ")
);
println!("\nCI summary: all {} steps passed ({})", done.len(), done.join(", "));
Ok(())
}
@@ -256,17 +217,7 @@ Tests that use a corpus skip cleanly when it is absent.
tar.arg("-xzf").arg(&tgz).arg("-C").arg(&dest);
run(tar, "tar -xzf PngSuite-2017jul19.tgz")?;
let _ = std::fs::remove_file(&tgz);
let n = std::fs::read_dir(&dest)
.map(|d| {
d.flatten()
.filter(|e| e.path().extension().is_some_and(|x| x == "png"))
.count()
})
.unwrap_or(0);
let n = std::fs::read_dir(&dest).map(|d| d.flatten().filter(|e| e.path().extension().is_some_and(|x| x == "png")).count()).unwrap_or(0);
println!("PngSuite: {n} PNG files in {}", dest.display());
if n == 0 {
Err("no PNG files extracted".into())
} else {
Ok(())
}
if n == 0 { Err("no PNG files extracted".into()) } else { Ok(()) }
}
+10 -41
View File
@@ -55,19 +55,11 @@ fn count_tests(src: &str) -> (usize, usize) {
}
fn exact_tests(pkg: &str) -> Option<usize> {
let out = crate::cargo()
.args(["test", "-q", "-p", pkg, "--", "--list"])
.output()
.ok()?;
let out = crate::cargo().args(["test", "-q", "-p", pkg, "--", "--list"]).output().ok()?;
if !out.status.success() {
return None;
}
Some(
String::from_utf8_lossy(&out.stdout)
.lines()
.filter(|l| l.ends_with(": test"))
.count(),
)
Some(String::from_utf8_lossy(&out.stdout).lines().filter(|l| l.ends_with(": test")).count())
}
pub fn run(_root: &Path, exact: bool) -> Result<(), String> {
@@ -75,10 +67,7 @@ pub fn run(_root: &Path, exact: bool) -> Result<(), String> {
let mut rows = Vec::new();
for p in meta["packages"].as_array().into_iter().flatten() {
let name = p["name"].as_str().unwrap_or("?").to_owned();
let Some(dir) = p["manifest_path"]
.as_str()
.and_then(|m| Path::new(m).parent().map(Path::to_path_buf))
else {
let Some(dir) = p["manifest_path"].as_str().and_then(|m| Path::new(m).parent().map(Path::to_path_buf)) else {
continue;
};
let mut c = Counts::default();
@@ -88,15 +77,8 @@ pub fn run(_root: &Path, exact: bool) -> Result<(), String> {
}
rows.sort_by(|a, b| a.0.cmp(&b.0));
let exact_col = if exact {
format!(" {:>8}", "harness")
} else {
String::new()
};
println!(
"{:<26} {:>6} {:>8} {:>7} {:>10}{exact_col}",
"crate", "files", "lines", "#[test]", "proptest!"
);
let exact_col = if exact { format!(" {:>8}", "harness") } else { String::new() };
println!("{:<26} {:>6} {:>8} {:>7} {:>10}{exact_col}", "crate", "files", "lines", "#[test]", "proptest!");
println!("{}", "-".repeat(62 + if exact { 9 } else { 0 }));
let mut total = Counts::default();
let mut total_exact = 0;
@@ -109,29 +91,17 @@ pub fn run(_root: &Path, exact: bool) -> Result<(), String> {
}
(true, None) => format!(" {:>8}", "error"),
};
println!(
"{name:<26} {:>6} {:>8} {:>7} {:>10}{ex_s}",
c.files, c.lines, c.tests, c.proptests
);
println!("{name:<26} {:>6} {:>8} {:>7} {:>10}{ex_s}", c.files, c.lines, c.tests, c.proptests);
total.files += c.files;
total.lines += c.lines;
total.tests += c.tests;
total.proptests += c.proptests;
}
println!("{}", "-".repeat(62 + if exact { 9 } else { 0 }));
let ex_s = if exact {
format!(" {total_exact:>8}")
} else {
String::new()
};
println!(
"{:<26} {:>6} {:>8} {:>7} {:>10}{ex_s}",
"TOTAL", total.files, total.lines, total.tests, total.proptests
);
let ex_s = if exact { format!(" {total_exact:>8}") } else { String::new() };
println!("{:<26} {:>6} {:>8} {:>7} {:>10}{ex_s}", "TOTAL", total.files, total.lines, total.tests, total.proptests);
if !exact {
println!(
"\n(#[test] counts source attributes; macro-generated tests count once. Use --exact for harness counts.)"
);
println!("\n(#[test] counts source attributes; macro-generated tests count once. Use --exact for harness counts.)");
}
Ok(())
}
@@ -142,8 +112,7 @@ mod tests {
#[test]
fn counts_attributes_and_proptest_blocks() {
let src =
"#[test]\nfn a() {}\n// #[test] commented\n #[test]\nfn b() {}\nproptest! {\n}\n";
let src = "#[test]\nfn a() {}\n// #[test] commented\n #[test]\nfn b() {}\nproptest! {\n}\n";
assert_eq!(count_tests(src), (2, 1));
}
}