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:
Brandon Thomas
2026-09-30 22:44:39 -04:00
co-authored by Claude Opus 5.5
parent 05f5bed1ad
commit acb2566d89
46 changed files with 24072 additions and 88 deletions
+5 -2
View File
@@ -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 };
+8 -2
View File
@@ -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) };
+1 -1
View File
@@ -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;