mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
Merge PR #5199: Brush Settings: disable Wetness/Mix controls outside Mixer Brush
docs/brush-parity.md: kept main's header, added the PR's change item and its newer date. docs/gaps.md: kept main's header, added the PR's change item; change-log row appended after main's rows.
This commit is contained in:
@@ -368,9 +368,11 @@ fn settings_tab(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
// The picked preset is recorded by name (`brush.presets.setCurrent`, identity, so
|
||||
// look-alike duplicates stay distinct), since the engine can't infer it from the
|
||||
// brush afterwards.
|
||||
ui.add_enabled_ui(on, |ui| section_body(ui, &mut b, section, &app.session.tools.presets, app.session.tools.current_preset.as_deref()))
|
||||
.inner
|
||||
.inspect(|name| run_or_status(app, "brush.presets.setCurrent", json!({ "name": name })));
|
||||
ui.add_enabled_ui(on, |ui| {
|
||||
section_body(ui, &mut b, section, &app.session.tools.presets, app.session.tools.current_preset.as_deref(), app.ui.tool)
|
||||
})
|
||||
.inner
|
||||
.inspect(|name| run_or_status(app, "brush.presets.setCurrent", json!({ "name": name })));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -620,6 +620,65 @@ fn harness(tab: usize, section: usize) -> Harness<'static, PhotocraftApp> {
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_mixer_edits_use_the_brush_command_and_survive_real_tool_swaps() {
|
||||
use crate::state::Tool;
|
||||
use egui_kittest::kittest::{NodeT, Queryable};
|
||||
|
||||
let mut h = harness(0, 6);
|
||||
let a = h.state_mut();
|
||||
a.ui.tool = Tool::Brush;
|
||||
a.session.tools.brush.transfer.enabled = true;
|
||||
a.session.tools.brush.transfer.wetness =
|
||||
paint::Dynamic { jitter: 0.41, minimum: 0.43, control: Control::PenPressure, fade_steps: 339, ..Default::default() };
|
||||
a.session.tools.brush.transfer.mix = paint::Dynamic { jitter: 0.63, minimum: 0.67, control: Control::PenPressure, fade_steps: 449, ..Default::default() };
|
||||
crate::paint_mouse::sync_tool_brush(a);
|
||||
a.ui.tool = Tool::MixerBrush;
|
||||
crate::paint_mouse::sync_tool_brush(a);
|
||||
h.run_steps(2);
|
||||
h.hover_at(h.get_by_label("Opacity Jitter").rect().center());
|
||||
h.event(egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(0.0, -250.0),
|
||||
phase: egui::TouchPhase::Move,
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
});
|
||||
h.run_steps(20);
|
||||
let journal = h.state().session.journal.len();
|
||||
let field = h.query_all_by_role(egui::accesskit::Role::SpinButton).find(|node| node.accesskit_node().numeric_value() == Some(41.0)).unwrap();
|
||||
field.click();
|
||||
h.key_press(egui::Key::ArrowUp);
|
||||
h.run_steps(2);
|
||||
assert!((h.state().session.tools.brush.transfer.wetness.jitter - 0.42).abs() < 1e-6);
|
||||
assert_eq!(h.state().session.journal.len(), journal + 1);
|
||||
let (id, params) = last_journal(h.state()).unwrap();
|
||||
assert_eq!(id, "tools.setBrush");
|
||||
assert_eq!(params["brush"], json!({"transfer": {"wetness": {"jitter": 0.42_f32}}}));
|
||||
let mixer = h.state().session.tools.brush.clone();
|
||||
|
||||
let a = h.state_mut();
|
||||
a.ui.tool = Tool::Brush;
|
||||
crate::paint_mouse::sync_tool_brush(a);
|
||||
h.run_steps(2);
|
||||
let brush = h.state().session.tools.brush.clone();
|
||||
let field = h.query_all_by_role(egui::accesskit::Role::SpinButton).find(|node| node.accesskit_node().numeric_value() == Some(41.0)).unwrap();
|
||||
assert!(field.accesskit_node().is_disabled());
|
||||
field.click();
|
||||
h.key_press(egui::Key::ArrowUp);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().session.tools.brush, brush);
|
||||
assert_eq!(h.state().session.journal.len(), journal + 1, "inactive edits and tool swaps add no brush command");
|
||||
let a = h.state_mut();
|
||||
a.ui.tool = Tool::MixerBrush;
|
||||
crate::paint_mouse::sync_tool_brush(a);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().session.tools.brush, mixer, "the Mixer Brush keeps its own edited settings");
|
||||
assert!(
|
||||
h.query_all_by_role(egui::accesskit::Role::SpinButton)
|
||||
.any(|node| node.accesskit_node().numeric_value() == Some(42.0) && !node.accesskit_node().is_disabled())
|
||||
);
|
||||
}
|
||||
|
||||
fn toggle_value(app: &PhotocraftApp, section: usize) -> bool {
|
||||
let brush = serde_json::to_value(&app.session.tools.brush).unwrap();
|
||||
let path = match section {
|
||||
|
||||
@@ -8,6 +8,7 @@ use photocraft_engine::BrushSettings;
|
||||
use photocraft_engine::paint::{self, BrushPreset, Control, Dynamic, MAX_BRUSH_SIZE, MaskMode, Pattern, PatternStyle, TipShape};
|
||||
|
||||
use crate::brush_preview;
|
||||
use crate::state::Tool;
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::widgets;
|
||||
|
||||
@@ -542,12 +543,16 @@ fn color_dynamics(ui: &mut egui::Ui, b: &mut BrushSettings) {
|
||||
signed_pct(ui, tl!("Purity"), &mut c.purity, -1.0, 1.0);
|
||||
}
|
||||
|
||||
fn transfer(ui: &mut egui::Ui, b: &mut BrushSettings) {
|
||||
fn transfer(ui: &mut egui::Ui, b: &mut BrushSettings, tool: Tool) {
|
||||
let tr = &mut b.transfer;
|
||||
dynamic(ui, "brush-opacity-ctl", tl!("Opacity Jitter"), &mut tr.opacity, 1.0, Some(tl!("Minimum")), false);
|
||||
dynamic(ui, "brush-flow-ctl", tl!("Flow Jitter"), &mut tr.flow, 1.0, Some(tl!("Minimum")), false);
|
||||
dynamic(ui, "brush-wet-ctl", tl!("Wetness Jitter"), &mut tr.wetness, 1.0, Some(tl!("Minimum")), false);
|
||||
dynamic(ui, "brush-mix-ctl", tl!("Mix Jitter"), &mut tr.mix, 1.0, Some(tl!("Minimum")), false);
|
||||
// Wetness and mix only affect the Mixer Brush. Keep their settings visible and saved
|
||||
// for other tools, but disable the whole blocks, including controllers and minimums.
|
||||
ui.add_enabled_ui(tool == Tool::MixerBrush, |ui| {
|
||||
dynamic(ui, "brush-wet-ctl", tl!("Wetness Jitter"), &mut tr.wetness, 1.0, Some(tl!("Minimum")), false);
|
||||
dynamic(ui, "brush-mix-ctl", tl!("Mix Jitter"), &mut tr.mix, 1.0, Some(tl!("Minimum")), false);
|
||||
});
|
||||
}
|
||||
|
||||
/// The Mixer Brush options the brush carries (Photoshop shows them in the Mixer Brush's options
|
||||
@@ -602,7 +607,8 @@ fn smoothing(ui: &mut egui::Ui, b: &mut BrushSettings) {
|
||||
/// Controls for section `i` (see [`crate::brush_panel::SECTIONS`]). `presets` feed the tip grids.
|
||||
/// Returns the name of a preset whose tip was picked in the Brush Tip Shape grid, for
|
||||
/// `brush.presets.setCurrent`.
|
||||
pub fn section_body(ui: &mut egui::Ui, b: &mut BrushSettings, i: usize, presets: &[BrushPreset], current: Option<&str>) -> Option<String> {
|
||||
/// The active `tool` determines which Transfer controls apply.
|
||||
pub fn section_body(ui: &mut egui::Ui, b: &mut BrushSettings, i: usize, presets: &[BrushPreset], current: Option<&str>, tool: Tool) -> Option<String> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let note = |ui: &mut egui::Ui, s: &str| {
|
||||
ui.label(RichText::new(s).color(t.text_faint).font(theme::medium(11.5)));
|
||||
@@ -617,7 +623,7 @@ pub fn section_body(ui: &mut egui::Ui, b: &mut BrushSettings, i: usize, presets:
|
||||
4 => dual_brush(ui, b, presets),
|
||||
5 => color_dynamics(ui, b),
|
||||
6 => {
|
||||
transfer(ui, b);
|
||||
transfer(ui, b, tool);
|
||||
mixer_options(ui, b);
|
||||
}
|
||||
7 => pose(ui, b),
|
||||
@@ -663,7 +669,7 @@ mod tests {
|
||||
ready = true;
|
||||
return;
|
||||
}
|
||||
section_body(ui, b, section, &[], None);
|
||||
section_body(ui, b, section, &[], None, Tool::MixerBrush);
|
||||
},
|
||||
b,
|
||||
);
|
||||
@@ -694,6 +700,134 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
struct TransferState {
|
||||
brush: BrushSettings,
|
||||
tool: Tool,
|
||||
}
|
||||
|
||||
fn transfer_mixer_brush() -> BrushSettings {
|
||||
let mut b = BrushSettings::default();
|
||||
b.transfer.enabled = true;
|
||||
for (dynamic, jitter, minimum, fade_steps) in [
|
||||
(&mut b.transfer.opacity, 0.13, 0.17, 119),
|
||||
(&mut b.transfer.flow, 0.27, 0.29, 229),
|
||||
(&mut b.transfer.wetness, 0.41, 0.43, 339),
|
||||
(&mut b.transfer.mix, 0.63, 0.67, 449),
|
||||
] {
|
||||
*dynamic = Dynamic { control: Control::Fade, jitter, minimum, fade_steps, ..Default::default() };
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
fn transfer_mixer_harness(kind: theme::ThemeKind, tool: Tool) -> Harness<'static, TransferState> {
|
||||
let mut ready = false;
|
||||
let mut h = Harness::builder().with_size(vec2(340.0, 1400.0)).build_ui_state(
|
||||
move |ui, state: &mut TransferState| {
|
||||
if !ready {
|
||||
crate::PhotocraftApp::setup_context(ui.ctx(), kind);
|
||||
ready = true;
|
||||
return;
|
||||
}
|
||||
ui.add_enabled_ui(state.brush.transfer.enabled, |ui| section_body(ui, &mut state.brush, 6, &[], None, state.tool));
|
||||
},
|
||||
TransferState { brush: transfer_mixer_brush(), tool },
|
||||
);
|
||||
h.run_steps(3);
|
||||
h
|
||||
}
|
||||
|
||||
/// #3825: Mixer-only blocks follow the tool, including sliders, minimums and Fade steps.
|
||||
#[test]
|
||||
fn transfer_mixer_controls_follow_the_tool_and_section_in_every_theme() {
|
||||
for kind in theme::ThemeKind::ALL {
|
||||
let mut h = transfer_mixer_harness(kind, Tool::Brush);
|
||||
let before = h.state().brush.clone();
|
||||
for tool in Tool::ALL {
|
||||
h.state_mut().tool = tool;
|
||||
h.run_steps(2);
|
||||
for (mixer_only, values) in [(false, [13.0, 17.0, 119.0, 27.0, 29.0, 229.0]), (true, [41.0, 43.0, 339.0, 63.0, 67.0, 449.0])] {
|
||||
for value in values {
|
||||
let field = h.query_all_by_role(Role::SpinButton).find(|node| node.accesskit_node().numeric_value() == Some(value)).unwrap();
|
||||
assert_eq!(field.accesskit_node().is_disabled(), mixer_only && tool != Tool::MixerBrush, "{kind:?}, {tool:?}, {value}");
|
||||
}
|
||||
}
|
||||
for (index, node) in h.query_all_by_role(Role::ComboBox).enumerate() {
|
||||
assert_eq!(node.accesskit_node().is_disabled(), index >= 2 && tool != Tool::MixerBrush, "{kind:?}, {tool:?}: controller {index}");
|
||||
}
|
||||
assert_eq!(h.state().brush, before, "changing tool context only changes availability");
|
||||
}
|
||||
h.state_mut().tool = Tool::MixerBrush;
|
||||
h.state_mut().brush.transfer.enabled = false;
|
||||
h.run_steps(2);
|
||||
assert!(h.query_all_by_role(Role::SpinButton).all(|node| node.accesskit_node().is_disabled()), "section Off still disables every field");
|
||||
assert!(h.query_all_by_role(Role::ComboBox).all(|node| node.accesskit_node().is_disabled()));
|
||||
h.state_mut().brush.transfer.enabled = true;
|
||||
h.state_mut().brush.transfer.wetness.control = Control::Off;
|
||||
h.run_steps(2);
|
||||
let minimum = h.query_all_by_role(Role::SpinButton).find(|node| node.accesskit_node().numeric_value() == Some(43.0)).unwrap();
|
||||
assert!(minimum.accesskit_node().is_disabled(), "Control Off still disables the applicable minimum");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_mixer_controls_ignore_disabled_edits_and_keep_values_on_reactivation() {
|
||||
let mut h = transfer_mixer_harness(theme::ThemeKind::ProMedium, Tool::Brush);
|
||||
let before = h.state().brush.clone();
|
||||
for value in [41.0, 43.0, 339.0, 63.0, 67.0, 449.0] {
|
||||
let field = h.query_all_by_role(Role::SpinButton).find(|node| node.accesskit_node().numeric_value() == Some(value)).unwrap();
|
||||
let at = field.rect().center();
|
||||
field.click();
|
||||
h.key_press(egui::Key::ArrowUp);
|
||||
h.event(egui::Event::Text("999".into()));
|
||||
h.key_press(egui::Key::Enter);
|
||||
h.run_steps(2);
|
||||
h.hover_at(at);
|
||||
h.drag_at(at);
|
||||
h.run_steps(1);
|
||||
h.drop_at(at + vec2(60.0, 0.0));
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().brush, before, "inactive field {value} ignores keyboard and pointer input");
|
||||
}
|
||||
for label in ["Wetness Jitter", "Mix Jitter"] {
|
||||
let row = h.get_by_label(label).rect();
|
||||
let from = pos2(row.left() + 120.0, row.bottom() + 16.0);
|
||||
h.hover_at(from);
|
||||
h.drag_at(from);
|
||||
h.run_steps(1);
|
||||
h.hover_at(from + vec2(150.0, 0.0));
|
||||
h.run_steps(1);
|
||||
h.drop_at(from + vec2(150.0, 0.0));
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().brush, before, "the inactive {label} track ignores a drag");
|
||||
}
|
||||
h.state_mut().tool = Tool::MixerBrush;
|
||||
h.run_steps(2);
|
||||
let row = h.get_by_label("Wetness Jitter").rect();
|
||||
let from = pos2(row.left() + 120.0, row.bottom() + 16.0);
|
||||
h.hover_at(from);
|
||||
h.drag_at(from);
|
||||
h.run_steps(1);
|
||||
h.hover_at(from + vec2(150.0, 0.0));
|
||||
h.run_steps(1);
|
||||
h.drop_at(from + vec2(150.0, 0.0));
|
||||
h.run_steps(2);
|
||||
assert!(h.state().brush.transfer.wetness.jitter > 0.6, "the same track accepts a drag when applicable");
|
||||
h.state_mut().brush.transfer.wetness.jitter = 0.41;
|
||||
h.run_steps(2);
|
||||
let field = h.query_all_by_role(Role::SpinButton).find(|node| node.accesskit_node().numeric_value() == Some(41.0)).unwrap();
|
||||
field.click();
|
||||
h.key_press(egui::Key::ArrowUp);
|
||||
h.run_steps(2);
|
||||
assert!((h.state().brush.transfer.wetness.jitter - 0.42).abs() < 1e-6);
|
||||
let after = h.state().brush.clone();
|
||||
h.state_mut().tool = Tool::Brush;
|
||||
h.run_steps(2);
|
||||
h.state_mut().tool = Tool::MixerBrush;
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().brush, after);
|
||||
assert!(h.query_all_by_role(Role::SpinButton).any(|node| node.accesskit_node().numeric_value() == Some(42.0) && !node.accesskit_node().is_disabled()));
|
||||
}
|
||||
|
||||
fn set_controls(b: &mut BrushSettings, control: Control) {
|
||||
for d in [
|
||||
&mut b.shape_dynamics.size,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Brush and painting parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (dragged preset tip preview; Hue Jitter amount track; Brush Settings minimum fields follow their Control; checkbox-only rows and non-destructive Smoothing switch; shaped-tip cursor follows the view; Color Replacement is a live toolbar tool; no brush-feel estimate changed; 2026-10-11: Pattern Stamp and Background Eraser draw live, TOOL-213-7) · **Target:** Adobe Photoshop 2026
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Transfer Wetness/Mix tool applicability; dragged preset tip preview; Hue Jitter amount track; Brush Settings minimum fields follow their Control; checkbox-only rows and non-destructive Smoothing switch; shaped-tip cursor follows the view; Color Replacement is a live toolbar tool; no brush-feel estimate changed; 2026-10-11: Pattern Stamp and Background Eraser draw live, TOOL-213-7) · **Target:** Adobe Photoshop 2026
|
||||
|
||||
Painting is where Photoshop users notice differences first, so it gets its own checklist: the
|
||||
brush engine (`crates/paint/src/brush.rs`), the Brush Settings UI
|
||||
@@ -26,6 +26,7 @@ tablets against Photoshop side by side.
|
||||
| Color Dynamics (FG/BG, hue/sat/brightness jitter, purity, per tip) | done | |
|
||||
| Hue Jitter uses a standard percentage-amount track instead of a hue spectrum | done | #3825 (Hue Jitter appearance only); rendered track checked in all 14 themes; numeric and pointer edits retain percentage units and engine command routing. |
|
||||
| Transfer (opacity/flow/wetness/mix jitter and controls) | done | |
|
||||
| Transfer Wetness/Mix blocks disabled outside Mixer Brush, retaining their settings | done | #3825 (tool applicability only); UI regressions cover all 14 themes and 67 tools, disabled input, section/Control Off, command routing and per-tool settings |
|
||||
| Shape Dynamics and Transfer minimum fields disabled when their Control is Off, retaining their values | done | #3825 (minimum-field interaction only); `brush_sections.rs` UI regressions cover all six fields, reactivation and disabled input |
|
||||
| Brush Pose (tilt, rotation, pressure overrides) | done | |
|
||||
| Noise, Wet Edges, Build-up, Protect Texture | done | |
|
||||
@@ -124,4 +125,5 @@ tablets against Photoshop side by side.
|
||||
| 2026-10-12 | minor | A shaped tip's cursor outline turns and flips with a rotated or flipped view, and follows pixel aspect correction |
|
||||
| 2026-10-13 | minor | Shape Dynamics and Transfer minimum inputs follow their Control and retain their values while disabled (#3825, partial). No brush-feel estimate changed. |
|
||||
| 2026-10-13 | minor | Hue Jitter uses the same themed amount slider as other jitter controls (#3825, partial). No brush-feel estimate changed. |
|
||||
| 2026-10-13 | minor | Transfer Wetness/Mix controls follow the active tool and retain their settings while disabled (#3825, partial). No brush-feel estimate changed. |
|
||||
| 2026-10-13 | minor | Dragging a brush preset shows its cached tip thumbnail and name in the Brushes panel and preset picker (#4225, partial). No brush-feel estimate changed. |
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
# Where PhotoCraft falls short of Photoshop
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (G9: Preferences › File Handling › Save in Background takes effect, #6207, 24 of 169 preferences do nothing; G7: Content-Aware Move previews every selection the command takes, coarse then exact, TOOL-213-4 done, #6680; G4: Pattern Stamp and Background Eraser draw live, TOOL-213-7; G9: Animated Zoom pref read: holding the Zoom tool zooms continuously, #6210, prefs that do nothing 25 of 166; G2: a cancelled 24 MP Gaussian Blur job stops in at most 44 ms, P25 under its 200 ms budget in local runs, baseline not re-run, #6205; G1: generated large-file fixtures, a 4.38 GB PSB opens on Windows through io, FILE-216-2 done, #5107; G9: fill and adjustment layers copy and paste whole, #2344; G1: smart filter masks show on PSD open, Photoshop-oracle floor 188/258; G10: DPX read and write, #1049, Cineon read; G1: CMYK Hue/Saturation and Color Balance keep the black ink; G1: CMYK Posterize on the stored values, psd-tools oracle floor 242/309; G1: CMYK adjustment layers composite on the document's inks, Photoshop-oracle floor 165/258; G1: CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; G1: modern Brightness/Contrast's brightness curve matches Photoshop, Photoshop-oracle floor 151/258; G1: Lab documents composite on their stored L, a, b, Photoshop-oracle floor 152/258; G1: Levels on whole levels at every step, Photoshop-oracle floor 146/258, psd-tools 238/309; G1: 32-bit Brightness/Contrast legacy, Channel Mixer, Photo Filter and Black & White tint unclipped as in Photoshop, Photoshop-oracle floor 149/258; G1: mask feathers blur with their exact variance, psd-tools oracle floor 240/309; G1: Threshold splits on Photoshop's luminosity, psd-tools oracle floor 241/309; G1: pass-through clipping base with an adjustment renders isolated, psd-tools oracle floor 240/309; G1: 16-bit Posterize as Photoshop bins it, psd-tools oracle floor 241/309; G1: Gradient Map driven by the W3C luminosity, Photoshop-oracle floor 147/258; G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed; stale missing-tool rows corrected: Color Replacement, Red Eye and Add/Delete Anchor Point are live tools, so only Art History Brush is missing; no estimate changed) · **Target:** Adobe Photoshop 2026
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (G4: Transfer Mixer-only controls, #3825 partial; G9: Preferences › File Handling › Save in Background takes effect, #6207, 24 of 169 preferences do nothing; G7: Content-Aware Move previews every selection the command takes, coarse then exact, TOOL-213-4 done, #6680; G4: Pattern Stamp and Background Eraser draw live, TOOL-213-7; G9: Animated Zoom pref read: holding the Zoom tool zooms continuously, #6210, prefs that do nothing 25 of 166; G2: a cancelled 24 MP Gaussian Blur job stops in at most 44 ms, P25 under its 200 ms budget in local runs, baseline not re-run, #6205; G1: generated large-file fixtures, a 4.38 GB PSB opens on Windows through io, FILE-216-2 done, #5107; G9: fill and adjustment layers copy and paste whole, #2344; G1: smart filter masks show on PSD open, Photoshop-oracle floor 188/258; G10: DPX read and write, #1049, Cineon read; G1: CMYK Hue/Saturation and Color Balance keep the black ink; G1: CMYK Posterize on the stored values, psd-tools oracle floor 242/309; G1: CMYK adjustment layers composite on the document's inks, Photoshop-oracle floor 165/258; G1: CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; G1: modern Brightness/Contrast's brightness curve matches Photoshop, Photoshop-oracle floor 151/258; G1: Lab documents composite on their stored L, a, b, Photoshop-oracle floor 152/258; G1: Levels on whole levels at every step, Photoshop-oracle floor 146/258, psd-tools 238/309; G1: 32-bit Brightness/Contrast legacy, Channel Mixer, Photo Filter and Black & White tint unclipped as in Photoshop, Photoshop-oracle floor 149/258; G1: mask feathers blur with their exact variance, psd-tools oracle floor 240/309; G1: Threshold splits on Photoshop's luminosity, psd-tools oracle floor 241/309; G1: pass-through clipping base with an adjustment renders isolated, psd-tools oracle floor 240/309; G1: 16-bit Posterize as Photoshop bins it, psd-tools oracle floor 241/309; G1: Gradient Map driven by the W3C luminosity, Photoshop-oracle floor 147/258; G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed; stale missing-tool rows corrected: Color Replacement, Red Eye and Add/Delete Anchor Point are live tools, so only Art History Brush is missing; no estimate changed) · **Target:** Adobe Photoshop 2026
|
||||
|
||||
Every known shortfall against Photoshop 2026 (27.11.0), one entry each, ranked by user impact
|
||||
on the way to beta. This is the work list agents pick from: take the highest-ranked entry that
|
||||
@@ -168,6 +168,9 @@ what an agent can't finish alone.
|
||||
- **Estimate:** 250–400 h (many overlap with other gaps; count each fix once).
|
||||
|
||||
### G4. Brush feel
|
||||
- **Progress (2026-10-13, #3825 partial):** Transfer’s Wetness Jitter and Mix Jitter
|
||||
blocks are disabled outside the Mixer Brush, retaining their saved settings. Opacity
|
||||
and Flow keep their existing availability; other layout and brush-feel gaps remain open.
|
||||
- **Progress (2026-10-13, #3825 partial):** Hue Jitter shows a standard themed
|
||||
percentage-amount track rather than a hue spectrum. Other Brush Settings layout
|
||||
and brush-behaviour gaps remain open.
|
||||
@@ -296,6 +299,7 @@ what an agent can't finish alone.
|
||||
| 2026-10-15 | minor | G9: Animated Zoom preference takes effect (hold the Zoom tool to zoom continuously, #6210); prefs that do nothing 25 of 166; no estimate changed |
|
||||
| 2026-10-16 | minor | G7: Content-Aware Move's live preview covers every selection the command takes (over 256 × 256: a coarse result solved scaled down, then the exact one); TOOL-213-4 done (#6680) |
|
||||
| 2026-10-15 | minor | G9: Save in Background off saves in the foreground under a modal progress dialog (#6207); preferences that do nothing 25 → 24 of 169 |
|
||||
| 2026-10-13 | minor | G4: Transfer Wetness/Mix tool applicability (#3825, partial); no readiness percentage or estimate changed |
|
||||
| 2026-10-14 | minor | G1: a PSD smart object's filter mask shows when the file opens (Photoshop stores the filtered pixels unmasked); export stores them that way too; Photoshop-oracle floor 185 → 188 of 258 |
|
||||
| 2026-10-13 | minor | G14: D90 image area measured (4288 × 2848 at (10, 10)); gap remains open |
|
||||
| 2026-10-14 | minor | Corpus floors raised to the measured results of the CMYK, Lab, 32-bit, Levels, Threshold, Posterize, feather and pass-through fixes landed together: Photoshop-authored 165 → 185 of 258, io 146 → 148 of 170, psd-tools 242 → 249 of 309 |
|
||||
|
||||
Reference in New Issue
Block a user