From 05f5bed1ad7c0b56d2ac438ea5daf741f32a1e1d Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 30 Sep 2026 11:38:47 -0400 Subject: [PATCH] continued work --- crates/engine/src/tests_colormgmt.rs | 13 +++++++++---- crates/tools/src/distort/arap.rs | 9 +++++---- crates/tools/src/distort/liquify.rs | 8 +++----- crates/tools/src/distort/perspective.rs | 5 ++++- crates/ui-egui/src/workspaces.rs | 3 ++- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/crates/engine/src/tests_colormgmt.rs b/crates/engine/src/tests_colormgmt.rs index f9465f26..f98e165d 100644 --- a/crates/engine/src/tests_colormgmt.rs +++ b/crates/engine/src/tests_colormgmt.rs @@ -292,6 +292,12 @@ fn view_toggles_redraw_without_dirtying() { s.execute("view.overprintPreview", &json!({"on": false})).unwrap(); } +/// Whether the content has a DeviceCMYK fill/stroke operator (`c m y k k` / `K`). +fn has_cmyk_op(pdf: &str) -> bool { + let toks: Vec<&str> = pdf.split_ascii_whitespace().collect(); + toks.windows(5).any(|w| (w[4] == "k" || w[4] == "K") && w[..4].iter().all(|t| t.parse::().is_ok())) +} + fn pdf_text(doc: &Document) -> String { let bytes = drawcraft_pdf::export(doc, &drawcraft_pdf::PdfOptions { compress: false, created: Some(0), ..Default::default() }).unwrap(); String::from_utf8_lossy(&bytes).into_owned() @@ -302,17 +308,16 @@ fn pdf_cmyk_document_writes_device_cmyk() { let mut s = session(); rect(&mut s, 0.0, 0.0, 10.0, 10.0, json!("#ff0000")); let rgb_pdf = pdf_text(&s.doc().unwrap().doc); - assert!(!rgb_pdf.contains("DeviceCMYK")); + assert!(!has_cmyk_op(&rgb_pdf) && rgb_pdf.contains(" rg")); s.execute("object.convertDocumentColorMode", &json!({"mode": "cmyk"})).unwrap(); let pdf = pdf_text(&s.doc().unwrap().doc); - std::fs::write("/tmp/claude-dbg.pdf", &pdf).ok(); - assert!(pdf.contains("DeviceCMYK"), "CMYK colours are written as DeviceCMYK"); + assert!(has_cmyk_op(&pdf), "CMYK colours are written with the DeviceCMYK `k` operator"); // RGB colours pasted into a CMYK document are separated on export. let mut s2 = session(); rect(&mut s2, 0.0, 0.0, 10.0, 10.0, json!("#336699")); let mut d = (*s2.doc().unwrap().doc).clone(); d.color_mode = ColorMode::Cmyk; - assert!(pdf_text(&d).contains("DeviceCMYK")); + assert!(has_cmyk_op(&pdf_text(&d))); } #[test] diff --git a/crates/tools/src/distort/arap.rs b/crates/tools/src/distort/arap.rs index 00db7406..6504263a 100644 --- a/crates/tools/src/distort/arap.rs +++ b/crates/tools/src/distort/arap.rs @@ -82,15 +82,16 @@ impl BandMatrix { let (n, bw) = (self.n, self.bw); for i in 0..n { let mut s = b[i]; - for k in i.saturating_sub(bw)..i { - s -= self.a[self.idx(i, k)] * b[k]; + let k0 = i.saturating_sub(bw); + for (k, bk) in b[k0..i].iter().enumerate() { + s -= self.a[self.idx(i, k0 + k)] * bk; } b[i] = s / self.a[self.idx(i, i)]; } for i in (0..n).rev() { let mut s = b[i]; - for k in i + 1..(i + bw + 1).min(n) { - s -= self.a[self.idx(k, i)] * b[k]; + for (k, bk) in b[i + 1..(i + bw + 1).min(n)].iter().enumerate() { + s -= self.a[self.idx(i + 1 + k, i)] * bk; } b[i] = s / self.a[self.idx(i, i)]; } diff --git a/crates/tools/src/distort/liquify.rs b/crates/tools/src/distort/liquify.rs index 0ef99df9..effac442 100644 --- a/crates/tools/src/distort/liquify.rs +++ b/crates/tools/src/distort/liquify.rs @@ -602,12 +602,10 @@ mod tests { let mut pd = PathData::single(SubPath::polyline(&[Point::new(80.0, 100.0), Point::new(120.0, 100.0)], false)); let p = LiquifyParams { detail: 1.0, simplify: 0.0, ..prm(LiquifyKind::Twirl) }; apply_stroke(&mut pd, &[c], &p, 0); - // Rotation preserves the distance to the centre. + // Rotation preserves every anchor's distance to the centre (ends at 20, the midpoint at 0). for (_, _, a) in pd.anchors() { - let d0 = if a.p.x < 100.0 { 20.0 } else { 20.0 }; - if a.p.distance(c) > 1.0 { - assert!((a.p.distance(c) - d0).abs() < 1e-6 || a.p.distance(c) < 20.0); - } + let d = a.p.distance(c); + assert!((d - 20.0).abs() < 1e-9 || d < 1e-9, "{d}"); } let first = pd.subpaths[0].anchors[0].p; assert!(first.y != 100.0 && (first.distance(c) - 20.0).abs() < 1e-9); diff --git a/crates/tools/src/distort/perspective.rs b/crates/tools/src/distort/perspective.rs index 03fb6e1e..eedbfd8e 100644 --- a/crates/tools/src/distort/perspective.rs +++ b/crates/tools/src/distort/perspective.rs @@ -68,6 +68,9 @@ impl Plane { pub const ALL: [Plane; 3] = [Plane::Left, Plane::Right, Plane::Ground]; } +/// Plane Switching widget: (plane, face quad) list and the "no plane" circle (centre, radius). +pub type WidgetGeom = (Vec<(Plane, [Point; 4])>, (Point, f64)); + /// A 3×3 projective transform (row-major). #[derive(Clone, Copy, Debug, PartialEq)] pub struct Homography(pub [[f64; 3]; 3]); @@ -350,7 +353,7 @@ impl PerspectiveGrid { } /// Plane Switching widget geometry: (plane, quad) faces and the "no plane" circle (centre, r). - pub fn widget(&self, doc: &Document, tol: f64) -> (Vec<(Plane, [Point; 4])>, (Point, f64)) { + pub fn widget(&self, doc: &Document, tol: f64) -> WidgetGeom { let ab = doc.artboards.first().map(|a| a.rect).unwrap_or(Rect::new(0.0, 0.0, 612.0, 792.0)); let s = 40.0 * tol; let (cx, cy) = (ab.x0 + 10.0 * tol + s / 2.0, ab.y0 + 10.0 * tol + s / 2.0); diff --git a/crates/ui-egui/src/workspaces.rs b/crates/ui-egui/src/workspaces.rs index 3a0b94f7..b4053415 100644 --- a/crates/ui-egui/src/workspaces.rs +++ b/crates/ui-egui/src/workspaces.rs @@ -235,6 +235,7 @@ pub fn menu_items() -> Vec { /// The app-bar workspace switcher's menu. pub fn popup(app: &mut DrawcraftApp, ui: &mut egui::Ui) { ui.set_min_width(210.0); + ui.set_max_width(240.0); let mut clicked: Option<(&'static str, Value)> = None; for it in menu_items() { match it { @@ -248,7 +249,7 @@ pub fn popup(app: &mut DrawcraftApp, ui: &mut egui::Ui) { Some(false) => format!(" {label}"), None => format!(" {label}"), }; - if ui.add(egui::Button::new(text).min_size(egui::vec2(ui.available_width(), 0.0))).clicked() { + if ui.add(egui::Button::new(text).min_size(egui::vec2(220.0, 0.0))).clicked() { clicked = Some((id, if p.is_null() { json!({}) } else { p })); ui.close(); }