diff --git a/crates/ui-egui/src/brush_panel_tests.rs b/crates/ui-egui/src/brush_panel_tests.rs index b6eb2581..ecfcdeb3 100644 --- a/crates/ui-egui/src/brush_panel_tests.rs +++ b/crates/ui-egui/src/brush_panel_tests.rs @@ -286,6 +286,30 @@ fn options_bar_edits_are_one_set_brush_per_gesture() { assert_eq!(s.tools.brush, h.state().session.tools.brush); } +#[test] +fn mixer_brush_options_update_the_persisted_mixer_settings() { + use egui_kittest::kittest::Queryable; + let mut h = options_bar_harness(crate::state::Tool::MixerBrush); + for label in ["Wet", "Load", "Mix", "Flow", "Sample All Layers"] { + assert!(h.get_by_label(label).rect().is_positive(), "missing Mixer Brush option {label}"); + } + + let initial = h.state().session.journal.len(); + let at = h.get_by_label("Sample All Layers").rect().center(); + h.hover_at(at); + h.run(); + h.drag_at(at); + h.run(); + h.drop_at(at); + h.run_steps(2); + + assert!(h.state().session.tools.brush.mixer.sample_all_layers); + assert_eq!(h.state().session.journal.len(), initial + 1); + let (id, params) = h.state().session.journal.last().unwrap(); + assert_eq!(id, "tools.setBrush"); + assert_eq!(params["brush"]["mixer"]["sampleAllLayers"], true); +} + #[test] fn drop_targets_and_actions_reorder_presets() { use crate::brushes_tab::{Action, apply, drop_target, group_key}; diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 5ef369a0..2f7c3556 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -268,6 +268,7 @@ pub(crate) fn freehand_tool(tool: Tool) -> bool { tool, Tool::Brush | Tool::Pencil + | Tool::MixerBrush | Tool::Eraser | Tool::BackgroundEraser | Tool::HistoryBrush @@ -2516,9 +2517,18 @@ mod tests { #[test] fn freehand_tools_are_the_ones_that_follow_a_path() { - for t in - [Tool::Brush, Tool::Pencil, Tool::Eraser, Tool::BackgroundEraser, Tool::CloneStamp, Tool::Smudge, Tool::Dodge, Tool::Lasso, Tool::QuickSelection] - { + for t in [ + Tool::Brush, + Tool::Pencil, + Tool::MixerBrush, + Tool::Eraser, + Tool::BackgroundEraser, + Tool::CloneStamp, + Tool::Smudge, + Tool::Dodge, + Tool::Lasso, + Tool::QuickSelection, + ] { assert!(freehand_tool(t), "{t:?} paints or retouches along a path"); } for t in [Tool::Move, Tool::Eyedropper, Tool::Gradient, Tool::Crop, Tool::RectMarquee, Tool::Type, Tool::Hand] { diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 5dc85213..9f4ee8a9 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1951,6 +1951,7 @@ Brush Projection Projekce štětce Apply the pen's tilt and rotation to the tip shape Použít náklon a natočení pera na tvar hrotu Mixer Brush Míchací štětec + Mixer Brush Tool Nástroj Míchací štětec Wet Vlhkost Mix Míchání Wetness Jitter Kolísání vlhkosti diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 6e195375..63ebbd95 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1937,6 +1937,7 @@ Brush Projection Proyección del pincel Apply the pen's tilt and rotation to the tip shape Aplicar la inclinación y la rotación del lápiz a la forma de la punta Mixer Brush Pincel mezclador + Mixer Brush Tool Herramienta Pincel mezclador Wet Húmedo Mix Mezcla Wetness Jitter Variación de humedad diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index d912a749..9ce3867f 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1942,6 +1942,7 @@ Brush Projection ブラシの投影 Apply the pen's tilt and rotation to the tip shape ペンの傾きと回転を先端のシェイプに適用します Mixer Brush 混合ブラシ + Mixer Brush Tool 混合ブラシツール Wet ウェット Mix 混合 Wetness Jitter ウェットのジッター diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 5b7c49db..2f0dd4dc 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -538,4 +538,19 @@ mod tests { } } } + + #[test] + fn mixer_brush_ui_strings_have_translations_in_every_registered_language() { + const STRINGS: &[&str] = &["Mixer Brush", "Mixer Brush Tool", "Wet", "Load", "Mix", "Flow", "Sample All Layers"]; + for lang in Lang::all() { + for source in STRINGS { + let translated = tr(lang, source); + if lang == Lang::EN { + assert_eq!(translated, *source, "English source string {source}"); + } else { + assert_ne!(translated, *source, "{} is missing {source:?}", lang.code()); + } + } + } + } } diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index a5919d87..8e622618 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -468,6 +468,7 @@ Quick Selection Быстрое выделение Magnetic Lasso Магнитное лассо Mixer Brush Смешивающая кисть + Mixer Brush Tool Инструмент «Смешивающая кисть» History Brush Кисть истории Art History Brush Кисть истории искусств Pattern Stamp Узорный штамп diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 2b7c37c4..d5f0672a 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1135,6 +1135,9 @@ Midtones 中间调 Midtones from: 中间调下限: Midtones to: 中间调上限: + Mix 混合 + Mixer Brush 混合笔刷 + Mixer Brush Tool 混合画笔工具 Minimum Depth 最小深度 Minimum Diameter 最小直径 Minimum Roundness 最小圆度 @@ -1665,6 +1668,7 @@ Web Minimum Web 最小尺寸 Web Most Common Web 常用尺寸 WebP (lossless) WebP(无损) + Wet 湿度 Wet Edges 湿边 Yellows 黄色 Zoom Tool 缩放工具 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 29d8a9df..3bac4fb8 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1933,6 +1933,7 @@ Brush Projection 筆刷投影 Apply the pen's tilt and rotation to the tip shape 將筆的傾斜和旋轉套用至筆尖形狀 Mixer Brush 混合器筆刷 + Mixer Brush Tool 混合器筆刷工具 Wet 濕度 Mix 混合 Wetness Jitter 濕度快速變換 diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index ecd902e3..9fd02fa8 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -64,6 +64,7 @@ pub fn tool_icon(t: Tool) -> &'static str { Tool::EllipseMarquee => "circle-dashed", Tool::Brush => "brush", Tool::Pencil => "pencil", + Tool::MixerBrush => "palette", Tool::Eraser => "eraser", Tool::BackgroundEraser => "eraser-background", Tool::MagicEraser => "eraser-magic", diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 754d4a4a..4d3bb233 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -25,7 +25,7 @@ const TOOL_SECTIONS: &[&[&[Tool]]] = &[ ], &[ &[Tool::SpotHealing, Tool::Healing], - &[Tool::Brush, Tool::Pencil], + &[Tool::Brush, Tool::Pencil, Tool::MixerBrush], &[Tool::CloneStamp], &[Tool::HistoryBrush], &[Tool::Eraser, Tool::BackgroundEraser, Tool::MagicEraser], @@ -423,7 +423,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { } let tool = app.ui.tool; // Brush edits here go through `tools.setBrush`, one journal entry per gesture (Rule 1). - if (tool.is_brushlike() && !matches!(tool, Tool::Brush | Tool::Pencil | Tool::Eraser)) || tool == Tool::QuickSelection { + if (tool.is_brushlike() && !matches!(tool, Tool::Brush | Tool::Pencil | Tool::MixerBrush | Tool::Eraser)) || tool == Tool::QuickSelection { let before = app.session.tools.brush.clone(); let mut b = before.clone(); let pick = brush_preset_chip(ui, &mut b, &app.session.tools.presets); @@ -456,19 +456,11 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { if widgets::dropdown(ui, "brush-mode", &mut mode, &opts, 96.0) { b.mode = mode; } - opt_label(ui, tl!("Opacity")); - let mut o = b.opacity * 100.0; - if widgets::value_field(ui, &mut o, 0.0..=100.0, "%", 62.0).changed() { - b.opacity = o / 100.0; - } + percent_field(ui, tl!("Opacity"), &mut b.opacity, 0.0..=100.0, 62.0); if icons::button(ui, "circle-dot", 24.0, b.pressure_opacity, tl!("Always use pressure for opacity")).clicked() { b.pressure_opacity = !b.pressure_opacity; } - opt_label(ui, tl!("Flow")); - let mut f = b.flow * 100.0; - if widgets::value_field(ui, &mut f, 1.0..=100.0, "%", 62.0).changed() { - b.flow = f / 100.0; - } + percent_field(ui, tl!("Flow"), &mut b.flow, 1.0..=100.0, 62.0); let _ = icons::button(ui, "sparkles", 24.0, false, tl!("Enable airbrush-style build-up effects")); opt_label(ui, tl!("Smoothing")); smoothing_field(ui, b, 58.0); @@ -495,11 +487,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { if widgets::dropdown(ui, "pencil-mode", &mut mode, &opts, 96.0) { b.mode = mode; } - opt_label(ui, tl!("Opacity")); - let mut o = b.opacity * 100.0; - if widgets::value_field(ui, &mut o, 0.0..=100.0, "%", if t.pro { 62.0 } else { 66.0 }).changed() { - b.opacity = o / 100.0; - } + percent_field(ui, tl!("Opacity"), &mut b.opacity, 0.0..=100.0, if t.pro { 62.0 } else { 66.0 }); opt_label(ui, tl!("Smoothing")); smoothing_field(ui, b, if t.pro { 58.0 } else { 66.0 }); widgets::vline(ui, 22.0); @@ -512,27 +500,30 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { opt_label(ui, tl!("Size")); widgets::value_field(ui, &mut b.size, 1.0..=2500.0, "px", 76.0); widgets::vline(ui, 22.0); - opt_label(ui, tl!("Hardness")); - let mut h = b.hardness * 100.0; - if widgets::value_field(ui, &mut h, 0.0..=100.0, "%", 66.0).changed() { - b.hardness = h / 100.0; - } - opt_label(ui, tl!("Opacity")); - let mut o = b.opacity * 100.0; - if widgets::value_field(ui, &mut o, 0.0..=100.0, "%", 66.0).changed() { - b.opacity = o / 100.0; - } - opt_label(ui, tl!("Flow")); - let mut f = b.flow * 100.0; - if widgets::value_field(ui, &mut f, 1.0..=100.0, "%", 66.0).changed() { - b.flow = f / 100.0; - } + percent_field(ui, tl!("Hardness"), &mut b.hardness, 0.0..=100.0, 66.0); + percent_field(ui, tl!("Opacity"), &mut b.opacity, 0.0..=100.0, 66.0); + percent_field(ui, tl!("Flow"), &mut b.flow, 1.0..=100.0, 66.0); opt_label(ui, tl!("Smoothing")); smoothing_field(ui, b, 66.0); widgets::vline(ui, 22.0); widgets::toggle(ui, &mut b.pressure_size, tl!("Pressure for Size")); widgets::toggle(ui, &mut b.pressure_opacity, tl!("Pressure for Opacity")); } + Tool::MixerBrush => { + picked = brush_preset_chip(ui, b, &app.session.tools.presets); + crate::brush_picker::settings_toggle(app, ui); + widgets::vline(ui, 22.0); + for (label, value) in [ + (tl!("Wet"), &mut b.mixer.wet), + (tl!("Load"), &mut b.mixer.load), + (tl!("Mix"), &mut b.mixer.mix), + (tl!("Flow"), &mut b.mixer.flow), + ] { + percent_field(ui, label, value, 0.0..=100.0, 62.0); + } + widgets::vline(ui, 22.0); + widgets::checkbox(ui, &mut b.mixer.sample_all_layers, tl!("Sample All Layers")); + } Tool::RectMarquee | Tool::EllipseMarquee if t.pro => { ui.spacing_mut().item_spacing.x = 2.0; for (i, (icon, tip)) in [ @@ -836,6 +827,14 @@ fn opt_label(ui: &mut egui::Ui, s: &str) { ui.label(RichText::new(tl!(&text)).color(t.text_dim)); } +fn percent_field(ui: &mut egui::Ui, label: &str, value: &mut f32, range: std::ops::RangeInclusive, width: f32) { + opt_label(ui, label); + let mut percent = *value * 100.0; + if widgets::value_field(ui, &mut percent, range, "%", width).changed() { + *value = percent / 100.0; + } +} + fn hint(ui: &mut egui::Ui, s: &str) { let t = Tokens::get(ui.ctx()); ui.label(RichText::new(s).color(t.text_faint)); diff --git a/crates/ui-egui/src/pencil_tests.rs b/crates/ui-egui/src/pencil_tests.rs index 57ce98ff..f1919619 100644 --- a/crates/ui-egui/src/pencil_tests.rs +++ b/crates/ui-egui/src/pencil_tests.rs @@ -134,7 +134,7 @@ fn square_cursor_sits_on_the_pixel_grid() { } #[test] -fn b_cycles_brush_and_pencil() { +fn b_cycles_brush_pencil_and_mixer_brush() { let mut h = Harness::builder().with_size(vec2(1280.0, 800.0)).with_max_steps(64).build_eframe(|cc| { PhotocraftApp::setup_context(&cc.egui_ctx, Default::default()); let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default()); @@ -152,13 +152,18 @@ fn b_cycles_brush_and_pencil() { assert_eq!(h.state().ui.tool, Tool::Brush); press(&mut h, Modifiers::NONE); assert_eq!(h.state().ui.tool, Tool::Pencil); + press(&mut h, Modifiers::NONE); + assert_eq!(h.state().ui.tool, Tool::MixerBrush); // Use Shift Key for Tool Switch: ⇧B cycles, B keeps the group's tool. h.state_mut().session.edit_prefs(|p| p.tools.use_shift_key_for_tool_switch = true); press(&mut h, Modifiers::SHIFT); assert_eq!(h.state().ui.tool, Tool::Brush); press(&mut h, Modifiers::SHIFT); assert_eq!(h.state().ui.tool, Tool::Pencil); + press(&mut h, Modifiers::SHIFT); + assert_eq!(h.state().ui.tool, Tool::MixerBrush); press(&mut h, Modifiers::NONE); - assert_eq!(h.state().ui.tool, Tool::Pencil); + assert_eq!(h.state().ui.tool, Tool::MixerBrush); assert_eq!(Tool::from_name("pencil"), Some(Tool::Pencil)); + assert_eq!(Tool::from_name("mixerBrush"), Some(Tool::MixerBrush)); } diff --git a/crates/ui-egui/src/retouch_ui.rs b/crates/ui-egui/src/retouch_ui.rs index 8afb4bce..d349690c 100644 --- a/crates/ui-egui/src/retouch_ui.rs +++ b/crates/ui-egui/src/retouch_ui.rs @@ -1,6 +1,6 @@ //! UI for the retouching tools (healing, clone, history brush, blur/sharpen/smudge, dodge/burn/ -//! sponge) and the smart selection tools (Quick Selection, Object Selection): gesture → engine -//! command, options bars, and the clone-source marker. +//! sponge, Mixer Brush) and the smart selection tools (Quick Selection, Object Selection): gesture +//! → engine command, options bars, and the clone-source marker. use egui::{Color32, Stroke, vec2}; use serde_json::{Value, json}; @@ -16,6 +16,7 @@ pub fn finish_stroke(app: &mut PhotocraftApp, tool: Tool, points: &[[f64; 3]], m let pts = json!(points); let (cmd, mut p): (&str, Value) = match tool { Tool::SpotHealing => ("paint.spotHealing", json!({"type": o.spot_type})), + Tool::MixerBrush => ("paint.mixerBrush", json!({})), Tool::Healing | Tool::CloneStamp => { let mut p = json!({"aligned": o.clone_aligned, "sampleLayer": o.clone_sample}); // The Clone Source panel's active slot (set by ⌥-click) drives the stroke: the engine @@ -137,7 +138,9 @@ fn pct(ui: &mut egui::Ui, label: &str, v: &mut f32) { /// Options bar for the retouching and smart-selection tools. Returns false for other tools. pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui, tool: Tool) -> bool { - if !tool.is_brushlike() && !matches!(tool, Tool::QuickSelection | Tool::ObjectSelection) || matches!(tool, Tool::Brush | Tool::Pencil | Tool::Eraser) { + if !tool.is_brushlike() && !matches!(tool, Tool::QuickSelection | Tool::ObjectSelection) + || matches!(tool, Tool::Brush | Tool::Pencil | Tool::MixerBrush | Tool::Eraser) + { return false; } let o = &mut app.ui.tool_options; @@ -293,4 +296,48 @@ mod tests { } } } + + #[test] + fn mixer_brush_is_selectable_and_paints_only_inside_the_selection_via_control() { + use crate::control::{ControlRequest, Outcome, handle}; + + let mut app = app(); + app.run("paint.pencil", json!({"points": [[50, 30]], "size": 200, "color": "#204080"})).unwrap(); + app.run("tools.setColors", json!({"foreground": "#f02010"})).unwrap(); + app.run( + "tools.setBrush", + json!({ + "pressureSize": false, + "size": 12, + "mixer": {"wet": 0.0, "load": 1.0, "mix": 0.0, "flow": 1.0} + }), + ) + .unwrap(); + app.run("select.rect", json!({"x": 35, "y": 20, "width": 30, "height": 20})).unwrap(); + let before = { + let surface = active(&app).surface().unwrap(); + [surface.rgba(20, 30), surface.rgba(50, 30), surface.rgba(80, 30)] + }; + + let ctx = egui::Context::default(); + let (req, _rx) = ControlRequest::new( + "ui.pointer", + json!({ + "tool": "mixerBrush", + "events": [ + {"kind": "down", "x": 8, "y": 30}, + {"kind": "move", "x": 92, "y": 30}, + {"kind": "up", "x": 92, "y": 30} + ] + }), + ); + assert!(matches!(handle(&mut app, &ctx, &req), Outcome::Done(_))); + assert_eq!(app.ui.tool, Tool::MixerBrush); + assert_eq!(app.session.journal.last().map(|(id, _)| id.as_str()), Some("paint.mixerBrush")); + + let surface = active(&app).surface().unwrap(); + assert_ne!(surface.rgba(50, 30), before[1], "the selected pixels are mixed"); + assert_eq!(surface.rgba(20, 30), before[0], "outside the selection is unchanged"); + assert_eq!(surface.rgba(80, 30), before[2], "the far side outside the selection is unchanged"); + } } diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 31989e0d..7d510e69 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -18,6 +18,7 @@ pub enum Tool { Count, Brush, Pencil, + MixerBrush, Eraser, BackgroundEraser, MagicEraser, @@ -51,7 +52,7 @@ pub enum Tool { } impl Tool { - pub const ALL: [Tool; 43] = [ + pub const ALL: [Tool; 44] = [ Tool::Move, Tool::RectMarquee, Tool::EllipseMarquee, @@ -65,6 +66,7 @@ impl Tool { Tool::Count, Tool::Brush, Tool::Pencil, + Tool::MixerBrush, Tool::Eraser, Tool::BackgroundEraser, Tool::MagicEraser, @@ -104,6 +106,7 @@ impl Tool { Tool::EllipseMarquee => "Elliptical Marquee Tool", Tool::Brush => "Brush Tool", Tool::Pencil => "Pencil Tool", + Tool::MixerBrush => "Mixer Brush Tool", Tool::Eraser => "Eraser Tool", Tool::BackgroundEraser => "Background Eraser Tool", Tool::MagicEraser => "Magic Eraser Tool", @@ -150,6 +153,7 @@ impl Tool { self, Tool::Brush | Tool::Pencil + | Tool::MixerBrush | Tool::Eraser | Tool::BackgroundEraser | Tool::SpotHealing @@ -169,7 +173,7 @@ impl Tool { match self { Tool::Move => 'V', Tool::RectMarquee | Tool::EllipseMarquee => 'M', - Tool::Brush | Tool::Pencil => 'B', + Tool::Brush | Tool::Pencil | Tool::MixerBrush => 'B', Tool::Eraser | Tool::BackgroundEraser | Tool::MagicEraser => 'E', Tool::Eyedropper | Tool::Ruler | Tool::Note | Tool::Count => 'I', Tool::Lasso | Tool::PolygonLasso => 'L', @@ -197,6 +201,7 @@ impl Tool { Tool::RectMarquee => "⬚", Tool::EllipseMarquee => "◌", Tool::Brush => "🖌", + Tool::MixerBrush => "🖌", Tool::Eraser => "⌫", Tool::Eyedropper => "💧", Tool::Lasso | Tool::PolygonLasso => "L", @@ -765,6 +770,8 @@ mod tests { assert_eq!(Tool::from_name("Rect"), Some(Tool::RectMarquee)); assert_eq!(Tool::from_name("RectMarquee"), Some(Tool::RectMarquee)); assert_eq!(Tool::from_name("Eraser Tool"), Some(Tool::Eraser)); + assert_eq!(Tool::from_name("mixerBrush"), Some(Tool::MixerBrush)); + assert_eq!(Tool::from_name("Mixer Brush Tool"), Some(Tool::MixerBrush)); assert_eq!(Tool::from_name("nope"), None); } diff --git a/docs/scorecard.md b/docs/scorecard.md index 5bcc897e..53290092 100644 --- a/docs/scorecard.md +++ b/docs/scorecard.md @@ -8,7 +8,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js |---|---:|---:|---:|---| | [Performance](#performance) | 3 | 22 | 12 | 46 scenarios: 3 within budget, 22 over budget or failing, 13 tracked only, 4 not run, 8 not measurable yet (baseline 2026-10-05) | | [File compatibility](#file-compatibility) | 1 | 8 | 18 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 229/309, round trip ≥ 307/309; photoshop oracles: oracle ≥ 132/256, round trip ≥ 258/256 | -| [Tools](#tools) | 4 | 14 | 12 | | +| [Tools](#tools) | 5 | 13 | 12 | | | [Workspace and UI](#workspace-and-ui) | 0 | 4 | 19 | settings that do nothing: 60 of 134 (target 0) | | [Type](#type) | 0 | 3 | 6 | | | [Automation](#automation) | 1 | 3 | 7 | 626 of 626 menu items live (100.0%) | @@ -121,7 +121,7 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t | Id | Target | Status | Issue | Note | |---|---|---|---|---| | TOOL-213-1 | Pencil tool: aliased 1 px painting, square pixel-aligned cursor | done | [#213](https://github.com/storytold/photocraft/issues/213) | Tool::Pencil (B group): live aliased strokes on the pixel grid, square cursor, Auto Erase; pencil_tests.rs | -| TOOL-213-2 | Mixer Brush tool on the toolbar | partial | [#213](https://github.com/storytold/photocraft/issues/213) | paint.mixerBrush (brush_cmds.rs:337) + panel options; no Tool variant | +| TOOL-213-2 | Mixer Brush tool on the toolbar | done | [#213](https://github.com/storytold/photocraft/issues/213) | Tool::MixerBrush in the Brush (B) flyout; Wet/Load/Mix/Flow/Sample All Layers options, command/control dispatch and selection-clipping tests | | TOOL-213-3 | Patch tool with a live preview of the patched area | missing | [#213](https://github.com/storytold/photocraft/issues/213) | No patch command or tool | | TOOL-213-4 | Content-Aware Move tool reusing inpaint completion | missing | [#213](https://github.com/storytold/photocraft/issues/213) | inpaint::complete only used by spot healing (retouch_cmds.rs:386) | | TOOL-213-5 | Painting a type/shape/smart/fill layer offers to rasterize, then continues the stroke | done | [#213](https://github.com/storytold/photocraft/issues/213) | rasterize_prompt.rs: OK rasterizes then paints (two history states), Cancel does nothing; drivable via ui.dialog.confirm | diff --git a/scorecard/tools.toml b/scorecard/tools.toml index ff1b797b..9aa9a899 100644 --- a/scorecard/tools.toml +++ b/scorecard/tools.toml @@ -15,9 +15,9 @@ note = "Tool::Pencil (B group): live aliased strokes on the pixel grid, square c [[item]] id = "TOOL-213-2" target = "Mixer Brush tool on the toolbar" -status = "partial" +status = "done" issue = 213 -note = "paint.mixerBrush (brush_cmds.rs:337) + panel options; no Tool variant" +note = "Tool::MixerBrush in the Brush (B) flyout; Wet/Load/Mix/Flow/Sample All Layers options, command/control dispatch and selection-clipping tests" [[item]] id = "TOOL-213-3"