mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
* feat(ui): expose Mixer Brush tool (#213) Wire the existing Mixer Brush command into the selectable UI tool model, options bar, and control-driven painting path. Cover shortcut cycling, options persistence, and selection clipping. Co-authored-by: Copilot <[email protected]> Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d * docs(scorecard): mark Mixer Brush slice complete (#213) Reflect the new Mixer Brush toolbar, options, control dispatch, and selection tests in TOOL-213-2 and regenerate the scorecard. Co-authored-by: Copilot <[email protected]> Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d * fix(i18n): translate Mixer Brush labels in zh-Hans Add Simplified Chinese entries for the Mixer Brush tool, its options, and the brush-settings heading. Test all Mixer Brush UI strings against every registered language catalog. Co-authored-by: Copilot <[email protected]> Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d * Polish Mixer Brush UI controls Co-authored-by: Copilot <[email protected]> Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc * Regenerate docs/scorecard.md Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
This commit is contained in:
16 files changed
+163
-46
No files matched your search
@@ -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};
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -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 ウェットのジッター
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,6 +468,7 @@
|
||||
Quick Selection Быстрое выделение
|
||||
Magnetic Lasso Магнитное лассо
|
||||
Mixer Brush Смешивающая кисть
|
||||
Mixer Brush Tool Инструмент «Смешивающая кисть»
|
||||
History Brush Кисть истории
|
||||
Art History Brush Кисть истории искусств
|
||||
Pattern Stamp Узорный штамп
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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 缩放工具
|
||||
|
||||
|
@@ -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 濕度快速變換
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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",
|
||||
|
||||
@@ -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<f32>, 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));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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 |
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user