mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 14:16:56 -06:00
Distortion tools (Width, Liquify, Puppet Warp, Perspective Grid), Patterns + Repeat, colour management (CMS, proofing, separations), Preferences/Shortcuts/Workspaces; original brand mark; fix filter panic in thumbnails; README demo scenes
- New original DrawCraft mark (gradient + pen nib) replaces the two-letter badge
- render: thumbnails and pattern tiles use a single-threaded context (vello's default is
multithreaded and panics on filters); per-item raster effects detected
- Type tool state no longer leaks across documents
- ui.set {status} for clean screenshots
- README: new hero, feature shots, Made in DrawCraft gallery; examples/neon-drive, ribbons,
feature-sheet; ASSETS.md updated; heading renamed to Crafting Apps
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
05f5bed1ad
commit
acb2566d89
@@ -72,7 +72,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
[],
|
||||
None,
|
||||
"{ids?, colors?: [color]} colours that can't be printed in the working CMYK (selection, or the whole document when nothing is selected) → {checked, outOfGamut: [{hex, deltaE, count}]}",
|
||||
always,
|
||||
has_doc,
|
||||
gamut_check
|
||||
),
|
||||
cmd!(
|
||||
@@ -410,7 +410,10 @@ fn plates_json(s: &Session) -> Value {
|
||||
fn separations(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "view.separationsPreview";
|
||||
let mut v = proof::view();
|
||||
let all: Vec<String> = s.active().map(|d| proof::plates(&d.doc).into_iter().map(|p| p.name).collect()).unwrap_or_else(|| cms::PROCESS_PLATES.iter().map(|s| s.to_string()).collect());
|
||||
let all: Vec<String> = s
|
||||
.active()
|
||||
.map(|d| proof::plates(&d.doc).into_iter().map(|p| p.name).collect())
|
||||
.unwrap_or_else(|| cms::PROCESS_PLATES.iter().map(|s| s.to_string()).collect());
|
||||
let check = |n: &str| if all.iter().any(|a| a == n) { Ok(n.to_string()) } else { Err(bad(C, format!("unknown plate `{n}`"))) };
|
||||
if let Some(on) = p.get("on").and_then(Value::as_bool) {
|
||||
v.separations = if on { Some(v.separations.take().unwrap_or_else(|| all.clone())) } else { None };
|
||||
|
||||
@@ -251,7 +251,9 @@ fn liquify(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let sweep = dab_pts.iter().map(|c| prm.brush_bounds(*c)).reduce(|a, b| a.union(b)).unwrap();
|
||||
let mut all = vec![];
|
||||
doc.walk(|n| {
|
||||
if matches!(n.kind, NodeKind::Path { guide: false, .. }) && n.geometric_bounds().is_some_and(|b| b.inflate(1e-6, 1e-6).intersect(sweep).area() > 0.0) {
|
||||
if matches!(n.kind, NodeKind::Path { guide: false, .. })
|
||||
&& n.geometric_bounds().is_some_and(|b| b.inflate(1e-6, 1e-6).intersect(sweep).area() > 0.0)
|
||||
{
|
||||
all.push(n.id);
|
||||
}
|
||||
});
|
||||
@@ -345,7 +347,11 @@ fn grid_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn grid_preset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let kind = p.get("kind").and_then(Value::as_u64).filter(|k| (1..=3).contains(k)).ok_or_else(|| bad("perspective.grid.preset", "kind must be 1, 2 or 3"))? as u8;
|
||||
let kind = p
|
||||
.get("kind")
|
||||
.and_then(Value::as_u64)
|
||||
.filter(|k| (1..=3).contains(k))
|
||||
.ok_or_else(|| bad("perspective.grid.preset", "kind must be 1, 2 or 3"))? as u8;
|
||||
let old = grid_of(&s.doc()?.doc);
|
||||
let ab = s.doc()?.doc.artboards.first().map(|a| a.rect).ok_or_else(|| EngineError::Other("no artboard".into()))?;
|
||||
let g = PerspectiveGrid { attached: old.attached, ..PerspectiveGrid::preset(kind, ab) };
|
||||
|
||||
@@ -16,10 +16,10 @@ mod live;
|
||||
pub(crate) mod menucmds;
|
||||
mod object;
|
||||
mod paint;
|
||||
mod patterncmds;
|
||||
mod panelcmds;
|
||||
mod path;
|
||||
mod pathops;
|
||||
mod patterncmds;
|
||||
pub mod prefscmds;
|
||||
mod recolor;
|
||||
mod select;
|
||||
|
||||
@@ -660,10 +660,10 @@ mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_brushsym;
|
||||
#[cfg(test)]
|
||||
mod tests_colormgmt;
|
||||
#[cfg(test)]
|
||||
mod tests_build;
|
||||
#[cfg(test)]
|
||||
mod tests_colormgmt;
|
||||
#[cfg(test)]
|
||||
mod tests_distort;
|
||||
#[cfg(test)]
|
||||
mod tests_draw2;
|
||||
@@ -676,12 +676,12 @@ mod tests_menucmds;
|
||||
#[cfg(test)]
|
||||
mod tests_panelcmds;
|
||||
#[cfg(test)]
|
||||
mod tests_prefs;
|
||||
#[cfg(test)]
|
||||
mod tests_pathops;
|
||||
#[cfg(test)]
|
||||
mod tests_pattern;
|
||||
#[cfg(test)]
|
||||
mod tests_prefs;
|
||||
#[cfg(test)]
|
||||
mod tests_textedit;
|
||||
#[cfg(test)]
|
||||
mod tests_xform;
|
||||
|
||||
@@ -202,11 +202,20 @@ fn separation_plates_render_ink_coverage() {
|
||||
assert!((v - 128).abs() <= 2, "50% magenta renders as 50% grey: {v}");
|
||||
assert_eq!(plate("Black").pixel(25, 50), [255, 255, 255, 255]);
|
||||
// Composite of C+M only shows the magenta object in magenta ink.
|
||||
let cm = render(&doc, RenderOptions { proof: Some(ProofSetup { separations: Some(vec!["Magenta".into()]), ..Default::default() }), ..Default::default() });
|
||||
let cm = render(
|
||||
&doc,
|
||||
RenderOptions { proof: Some(ProofSetup { separations: Some(vec!["Magenta".into()]), ..Default::default() }), ..Default::default() },
|
||||
);
|
||||
assert_eq!(cm.pixel(25, 50)[0], 255);
|
||||
let all = render(
|
||||
&doc,
|
||||
RenderOptions { proof: Some(ProofSetup { separations: Some(vec!["Cyan".into(), "Magenta".into(), "Yellow".into(), "Black".into()]), ..Default::default() }), ..Default::default() },
|
||||
RenderOptions {
|
||||
proof: Some(ProofSetup {
|
||||
separations: Some(vec!["Cyan".into(), "Magenta".into(), "Yellow".into(), "Black".into()]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let p = all.pixel(25, 50);
|
||||
assert!(p[0] < 40 && p[2] > 200, "composite cyan: {p:?}");
|
||||
@@ -217,9 +226,17 @@ fn rgb_art_separates_with_gcr() {
|
||||
let mut s = session();
|
||||
rect(&mut s, 0.0, 0.0, 100.0, 100.0, json!("#000000"));
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
let k = render(&doc, RenderOptions { proof: Some(ProofSetup { separations: Some(vec!["Black".into()]), ..Default::default() }), ..Default::default() }).pixel(50, 50);
|
||||
let k = render(
|
||||
&doc,
|
||||
RenderOptions { proof: Some(ProofSetup { separations: Some(vec!["Black".into()]), ..Default::default() }), ..Default::default() },
|
||||
)
|
||||
.pixel(50, 50);
|
||||
assert!(k[0] < 20, "RGB black is mostly K: {k:?}");
|
||||
let c = render(&doc, RenderOptions { proof: Some(ProofSetup { separations: Some(vec!["Cyan".into()]), ..Default::default() }), ..Default::default() }).pixel(50, 50);
|
||||
let c = render(
|
||||
&doc,
|
||||
RenderOptions { proof: Some(ProofSetup { separations: Some(vec!["Cyan".into()]), ..Default::default() }), ..Default::default() },
|
||||
)
|
||||
.pixel(50, 50);
|
||||
assert!(c[0] > 40 && c[0] < 200, "rich black has some cyan: {c:?}");
|
||||
}
|
||||
|
||||
@@ -236,7 +253,10 @@ fn spot_colours_get_their_own_plate() {
|
||||
let names: Vec<&str> = plates["plates"].as_array().unwrap().iter().map(|p| p["name"].as_str().unwrap()).collect();
|
||||
assert_eq!(names, ["Cyan", "Magenta", "Yellow", "Black", "Brand Orange"]);
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
let plate = |n: &str| render(&doc, RenderOptions { proof: Some(ProofSetup { separations: Some(vec![n.into()]), ..Default::default() }), ..Default::default() }).pixel(50, 50);
|
||||
let plate = |n: &str| {
|
||||
render(&doc, RenderOptions { proof: Some(ProofSetup { separations: Some(vec![n.into()]), ..Default::default() }), ..Default::default() })
|
||||
.pixel(50, 50)
|
||||
};
|
||||
assert_eq!(plate("Brand Orange"), [0, 0, 0, 255]);
|
||||
assert_eq!(plate("Magenta"), [255, 255, 255, 255], "spot ink isn't on the process plates");
|
||||
// Panel commands: only / toggle.
|
||||
@@ -285,9 +305,9 @@ fn view_toggles_redraw_without_dirtying() {
|
||||
assert_eq!(drawcraft_render::proof::active_proof().unwrap().target, ProofTarget::Deuteranopia);
|
||||
assert!(s.execute("view.proofSetup", &json!({"target": "cmyk:Nope"})).is_err());
|
||||
s.execute("view.proofSetup", &json!({"target": "workingCmyk", "intent": "relative"})).unwrap();
|
||||
s.execute("view.proofColors", &json!({})).unwrap();
|
||||
assert!(drawcraft_render::proof::active_proof().is_none());
|
||||
let r = s.execute("view.overprintPreview", &json!({})).unwrap();
|
||||
let r = s.execute("view.proofColors", &json!({"on": false})).unwrap();
|
||||
assert_eq!(r["proofColors"], false);
|
||||
let r = s.execute("view.overprintPreview", &json!({"on": true})).unwrap();
|
||||
assert_eq!(r["overprintPreview"], true);
|
||||
s.execute("view.overprintPreview", &json!({"on": false})).unwrap();
|
||||
}
|
||||
|
||||
@@ -184,11 +184,8 @@ fn puppet_warp_satisfies_pins_and_keeps_area() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 100.0, 100.0, 300.0, 100.0);
|
||||
let n = undo_len(&s);
|
||||
s.execute(
|
||||
"object.puppetWarp",
|
||||
&json!({"id": a.0, "pins": [[120, 150], [250, 150], [380, 150]], "moved": [[120, 150], [250, 150], [370, 90]]}),
|
||||
)
|
||||
.unwrap();
|
||||
s.execute("object.puppetWarp", &json!({"id": a.0, "pins": [[120, 150], [250, 150], [380, 150]], "moved": [[120, 150], [250, 150], [370, 90]]}))
|
||||
.unwrap();
|
||||
let p = path(&s, a);
|
||||
let area0 = 300.0 * 100.0;
|
||||
let area1 = p.to_bezpath().area().abs();
|
||||
|
||||
@@ -115,9 +115,7 @@ fn list_describes_every_pref() {
|
||||
|
||||
#[test]
|
||||
fn prefs_serde_round_trip_and_tolerates_missing_fields() {
|
||||
let mut p = Prefs::default();
|
||||
p.ui_scaling = 1.25;
|
||||
p.units_general = "millimeters".into();
|
||||
let p = Prefs { ui_scaling: 1.25, units_general: "millimeters".into(), ..Default::default() };
|
||||
let back: Prefs = serde_json::from_value(p.to_json()).unwrap();
|
||||
assert_eq!(back, p);
|
||||
let partial: Prefs = serde_json::from_value(json!({"keyboardIncrement": 4})).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user