From 6547fadccd87ca2f8ffb4f7a73a970a6121865ff Mon Sep 17 00:00:00 2001 From: David Mann Date: Sun, 8 Nov 2026 01:24:18 +0000 Subject: [PATCH] Floor a restored brush's opacity at 1 % A brush remembered at 0 % opacity (saved before the 1 % floor) read back at 0: the options bar showed 1 % but strokes painted nothing. Restoring now floors opacity at 1 %, like tools.setBrush (#11812). Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/brush_cmds.rs | 7 +++++-- crates/engine/src/cmd/brush_cmds/tests.rs | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/brush_cmds.rs b/crates/engine/src/cmd/brush_cmds.rs index 9f371bcd17..61a35e5150 100644 --- a/crates/engine/src/cmd/brush_cmds.rs +++ b/crates/engine/src/cmd/brush_cmds.rs @@ -1172,8 +1172,8 @@ pub fn remembered_settings(b: &BrushSettings) -> Option { /// A brush remembered by [`remembered_settings`], read back at launch: `None` when the value is /// not a brush or fails the checks `tools.setBrush` makes (a size or scatter out of range). The -/// options-bar values are clamped to the ranges their controls allow (size 1..max px; hardness, -/// opacity, flow and smoothing 0..100 %), and stored tips load when the store is attached (else +/// options-bar values are clamped to the ranges their controls allow (size 1..max px; opacity +/// 1..100 %; hardness, flow and smoothing 0..100 %), and stored tips load when the store is attached (else /// they paint with their previews until `Session::attach_preset_store`). pub fn restored_settings(s: &Session, v: &Value) -> Option { if !v.is_object() { @@ -1185,6 +1185,9 @@ pub fn restored_settings(s: &Session, v: &Value) -> Option { for f in [&mut b.hardness, &mut b.opacity, &mut b.flow, &mut b.smoothing.amount] { *f = if f.is_nan() { 1.0 } else { f.clamp(0.0, 1.0) }; } + // Opacity takes 1-100 %, as `tools.setBrush` floors it: a brush remembered at 0 % (saved + // before that floor) would show 1 % but paint nothing (#11812). + b.opacity = b.opacity.max(0.01); let _ = s.load_brush_tips(&mut b); Some(b) } diff --git a/crates/engine/src/cmd/brush_cmds/tests.rs b/crates/engine/src/cmd/brush_cmds/tests.rs index f72a0e207b..3abf932823 100644 --- a/crates/engine/src/cmd/brush_cmds/tests.rs +++ b/crates/engine/src/cmd/brush_cmds/tests.rs @@ -1380,6 +1380,22 @@ fn restored_brush_settings_are_clamped_or_rejected() { } } +/// #11812: a brush remembered at 0 % opacity (saved before the 1 % floor) reads back at 1 %, the +/// value its options-bar field shows, so it paints instead of laying down nothing. +#[test] +fn restored_brush_opacity_floors_at_one_percent() { + let s = Session::new(); + for opacity in [0.0, -1.0, 0.004] { + let b = restored_settings(&s, &json!({"opacity": opacity})).unwrap(); + assert_eq!(b.opacity, 0.01, "{opacity}"); + } + assert_eq!(restored_settings(&s, &json!({"opacity": 0.37})).unwrap().opacity, 0.37, "values above the floor are kept"); + let mut t = Session::new(); + t.prefs.edit(|p| p.last_brush = json!({"brush": {"opacity": 0.0}})); + restore_brush(&mut t); + assert_eq!(t.tools.brush.opacity, 0.01, "the remembered session brush"); +} + #[test] fn remembered_brushes_never_embed_large_bitmaps() { use std::sync::Arc;