mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-15 03:36:48 -04:00
Layer Style dialog previews effects live on the canvas (#30)
The canvas only showed effects after OK. While the dialog is open, the canvas now renders the document with the dialog's style applied: the same commands OK runs, replayed on a scratch session (no history), cached by document revision + style fields so switching pages doesn't re-render. Cancel drops the preview. Works on both GPU and CPU canvas paths (display_doc). Tests cover the preview, re-render on edit, and cancel. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8d21ca349d
commit
159c11feb4
3 files changed
+82
-4
No files matched your search
@@ -144,7 +144,7 @@ fn downsample(buf: &photocraft_compose::Buffer, factor: u32) -> photocraft_compo
|
||||
}
|
||||
|
||||
/// The document to render: the committed one, or a clone with the live adjustment preview applied.
|
||||
fn display_doc(app: &PhotocraftApp, idx: usize) -> (std::sync::Arc<Document>, u64) {
|
||||
fn display_doc(app: &mut PhotocraftApp, idx: usize) -> (std::sync::Arc<Document>, u64) {
|
||||
let st = &app.session.documents()[idx];
|
||||
// Puppet / Perspective Warp previews hide the layer they draw on a mesh.
|
||||
if let Some(shown) = crate::distort_ui::display_doc(app, idx) {
|
||||
@@ -156,6 +156,23 @@ fn display_doc(app: &PhotocraftApp, idx: usize) -> (std::sync::Arc<Document>, u6
|
||||
{
|
||||
return (pv.doc.clone(), (1 << 40) + pv.session);
|
||||
}
|
||||
// Layer Style dialog: show its effects live (Cancel just drops the preview).
|
||||
let style = app.ui.dialogs.iter().find(|d| d.kind == crate::state::DialogKind::LayerStyle);
|
||||
if style.is_none() {
|
||||
app.style_preview = None;
|
||||
}
|
||||
if let Some(d) = style
|
||||
&& app.session.active_index() == Some(idx)
|
||||
{
|
||||
let key = (crate::layer_style::preview_hash(&d.fields) ^ st.revision.wrapping_mul(0x9e37_79b9_7f4a_7c15)) | 1 << 63;
|
||||
if app.style_preview.as_ref().map(|p| p.0) != Some(key) {
|
||||
let shown = crate::layer_style::preview_document(&st.doc, &app.session.patterns, &d.fields).map(std::sync::Arc::new);
|
||||
app.style_preview = Some((key, shown));
|
||||
}
|
||||
if let Some((_, Some(doc))) = &app.style_preview {
|
||||
return (doc.clone(), key);
|
||||
}
|
||||
}
|
||||
if let Some((layer, params)) = &app.live_adjust
|
||||
&& let Some(l) = st.doc.layer(*layer)
|
||||
&& let LayerContent::Adjustment(a) = &l.content
|
||||
@@ -1508,6 +1525,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_style_dialog_previews_live_and_cancel_restores() {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 16, "height": 16})).unwrap();
|
||||
app.run("layer.new.layer", json!({})).unwrap();
|
||||
let fx = |d: &Document| d.layers.iter().map(|l| l.effects.items.len()).sum::<usize>();
|
||||
let id = crate::layer_style::open(&mut app, Some("colorOverlay")).unwrap();
|
||||
let (shown, key) = display_doc(&mut app, 0);
|
||||
assert_eq!((fx(&shown), fx(&app.session.documents()[0].doc)), (1, 0), "previewed, not committed");
|
||||
app.ui.dialog_mut(id).unwrap().fields.insert("on:stroke".into(), json!(true));
|
||||
let (shown, key2) = display_doc(&mut app, 0);
|
||||
assert_eq!(fx(&shown), 2);
|
||||
assert_ne!(key, key2, "an edit re-renders the canvas");
|
||||
app.ui.close_dialog(id);
|
||||
let (shown, key) = display_doc(&mut app, 0);
|
||||
assert_eq!((fx(&shown), key), (0, 0));
|
||||
assert!(app.style_preview.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_steps_monotone() {
|
||||
assert_eq!(zoom_step(1.0, 1), 2.0);
|
||||
|
||||
@@ -215,23 +215,45 @@ pub fn pattern_list(app: &PhotocraftApp) -> Value {
|
||||
|
||||
/// Apply the dialog: replace the layer's effects with the enabled ones.
|
||||
pub fn confirm(app: &mut PhotocraftApp, f: &Map<String, Value>) -> Result<Value, String> {
|
||||
apply(f, |id, p| app.run(id, p))
|
||||
}
|
||||
|
||||
/// Runs the dialog's commands through `run`: blending options, clear, then each enabled effect.
|
||||
fn apply(f: &Map<String, Value>, mut run: impl FnMut(&str, Value) -> Result<Value, String>) -> Result<Value, String> {
|
||||
let layer = f.get("layer").cloned().unwrap_or(Value::Null);
|
||||
if let Some(Value::Object(bo)) = f.get(&format!("p:{BLENDING}")) {
|
||||
let mut p = Value::Object(bo.clone());
|
||||
p["layer"] = layer.clone();
|
||||
app.run("layer.layerStyle.blendingOptions", p)?;
|
||||
run("layer.layerStyle.blendingOptions", p)?;
|
||||
}
|
||||
let _ = app.run("layer.layerStyle.clear", json!({"layer": layer}));
|
||||
let _ = run("layer.layerStyle.clear", json!({"layer": layer}));
|
||||
for &(kind, _) in KINDS {
|
||||
if f.get(&format!("on:{kind}")).and_then(Value::as_bool) == Some(true) {
|
||||
let mut p = f.get(&format!("p:{kind}")).cloned().unwrap_or_else(|| json!({}));
|
||||
p["layer"] = layer.clone();
|
||||
app.run(&format!("layer.layerStyle.{kind}"), p)?;
|
||||
run(&format!("layer.layerStyle.{kind}"), p)?;
|
||||
}
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Hash of the fields that change the rendered style (not the selected page).
|
||||
pub fn preview_hash(f: &Map<String, Value>) -> u64 {
|
||||
f.iter()
|
||||
.filter(|(k, _)| k.as_str() == "layer" || k.starts_with("on:") || k.starts_with("p:"))
|
||||
.flat_map(|(k, v)| k.bytes().chain(v.to_string().into_bytes()))
|
||||
.fold(0xcbf2_9ce4_8422_2325, |h, b| (h ^ b as u64).wrapping_mul(0x100_0000_01b3))
|
||||
}
|
||||
|
||||
/// `doc` with the dialog's style applied, run on a scratch session (no history) for the live preview.
|
||||
pub fn preview_document(doc: &photocraft_doc::Document, patterns: &photocraft_engine::pattern_cmds::PatternLibrary, f: &Map<String, Value>) -> Option<photocraft_doc::Document> {
|
||||
let mut s = photocraft_engine::Session::new();
|
||||
s.patterns = patterns.clone();
|
||||
s.add_document(doc.clone(), None);
|
||||
apply(f, |id, p| s.execute(id, p).map_err(|e| e.to_string())).ok()?;
|
||||
s.active().map(|d| (*d.doc).clone())
|
||||
}
|
||||
|
||||
/// Dialog body (left list, right parameters).
|
||||
pub fn body(ui: &mut egui::Ui, f: &mut Map<String, Value>) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
@@ -394,6 +416,23 @@ mod tests {
|
||||
assert_eq!(f["on:dropShadow"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_applies_the_style_without_touching_the_document() {
|
||||
let mut s = photocraft_engine::Session::new();
|
||||
s.execute("file.new", json!({"width": 16, "height": 16})).unwrap();
|
||||
s.execute("layer.new.layer", json!({})).unwrap();
|
||||
let st = s.active().unwrap();
|
||||
let mut f = initial_fields(st.doc.layer(st.active_layer.unwrap()).unwrap(), Some("colorOverlay"));
|
||||
let h = preview_hash(&f);
|
||||
f.insert("selected".into(), json!("stroke"));
|
||||
assert_eq!(preview_hash(&f), h, "switching pages doesn't re-render");
|
||||
f.insert("on:stroke".into(), json!(true));
|
||||
assert_ne!(preview_hash(&f), h);
|
||||
let shown = preview_document(&st.doc, &s.patterns, &f).unwrap();
|
||||
let fx = |d: &photocraft_doc::Document| d.layer(st.active_layer.unwrap()).unwrap().effects.items.len();
|
||||
assert_eq!((fx(&shown), fx(&st.doc)), (2, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_fields_round_trip_through_the_engine() {
|
||||
let mut s = photocraft_engine::Session::new();
|
||||
|
||||
@@ -191,6 +191,8 @@ pub struct PhotocraftApp {
|
||||
pub(crate) doc_hist: Option<(DocId, u64, f64, std::sync::Arc<tone::Histograms>)>,
|
||||
/// Free Transform preview (document without the moving pixels + their texture).
|
||||
pub(crate) transform_preview: Option<transform_tool::TransformPreview>,
|
||||
/// Live Layer Style dialog preview: (key over revision + style fields, document with the style applied).
|
||||
pub(crate) style_preview: Option<(u64, Option<std::sync::Arc<Document>>)>,
|
||||
/// Liquify dialog, Puppet Warp and Perspective Warp sessions (distort_ui).
|
||||
pub(crate) distort: distort_ui::Distort,
|
||||
/// Filter › Camera Raw Filter dialog (camera_raw_ui).
|
||||
@@ -259,6 +261,7 @@ impl PhotocraftApp {
|
||||
info_sample: None,
|
||||
os_clip_sig: None,
|
||||
transform_preview: None,
|
||||
style_preview: None,
|
||||
distort: Default::default(),
|
||||
camera_raw: None,
|
||||
wide_angle: None,
|
||||
|
||||
Reference in new issue
Block a user