mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
G2: Effect Gallery six categories and per-filter settings
Limit gallery folders to the six Photoshop-style sets and edit stack params via the existing effect dialog body before apply. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Cursor
parent
ae3a8fe4c3
commit
dd9c285252
@@ -2,10 +2,10 @@
|
||||
//! apply it as separate appearance effects in one undo step.
|
||||
|
||||
use egui::{Color32, Rect, Sense, Stroke};
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_effects::{EffectInfo, PIXEL_EFFECTS};
|
||||
|
||||
use super::DialogSpec;
|
||||
use super::{DialogSpec, effect, form};
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
use crate::{VectorcraftApp, widgets};
|
||||
@@ -13,18 +13,14 @@ use crate::{VectorcraftApp, widgets};
|
||||
/// Dialog kind for the Effect Gallery.
|
||||
pub const KIND: &str = "effectGallery";
|
||||
|
||||
/// Raster effect submenus shown in the gallery (same paths as the Effect menu).
|
||||
const CATEGORIES: [(&str, &[&str]); 10] = [
|
||||
/// Raster effect submenus in the gallery (the six Photoshop-style folders it holds).
|
||||
const CATEGORIES: [(&str, &[&str]); 6] = [
|
||||
("Artistic", &["Effect", "Artistic"]),
|
||||
("Blur", &["Effect", "Blur"]),
|
||||
("Brush Strokes", &["Effect", "Brush Strokes"]),
|
||||
("Distort", &["Effect", "Distort"]),
|
||||
("Pixelate", &["Effect", "Pixelate"]),
|
||||
("Sharpen", &["Effect", "Sharpen"]),
|
||||
("Sketch", &["Effect", "Sketch"]),
|
||||
("Stylize", &["Effect", "Stylize"]),
|
||||
("Texture", &["Effect", "Texture"]),
|
||||
("Video", &["Effect", "Video"]),
|
||||
];
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
@@ -40,6 +36,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
struct StackEntry {
|
||||
effect: String,
|
||||
visible: bool,
|
||||
params: Map<String, Value>,
|
||||
}
|
||||
|
||||
/// Open the gallery on the current selection.
|
||||
@@ -48,7 +45,10 @@ pub fn open(app: &mut VectorcraftApp, _p: &Value) -> Result<Value, String> {
|
||||
return Err(tl!("Select artwork to apply effects").into());
|
||||
}
|
||||
let selected = effects_in_category(0).first().map(|e| e.id).unwrap_or("");
|
||||
app.ui.dialog = Some(Dialog::new(KIND, json!({ "category": 0, "selected": selected, "stack": [], "stackSelected": 0 })));
|
||||
app.ui.dialog = Some(Dialog::new(
|
||||
KIND,
|
||||
json!({ "category": 0, "selected": selected, "stack": [], "stackSelected": 0, "stackFocus": false }),
|
||||
));
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,20 @@ fn effects_in_category(cat: usize) -> Vec<EffectInfo> {
|
||||
out
|
||||
}
|
||||
|
||||
fn default_params(id: &str) -> Map<String, Value> {
|
||||
match vectorcraft_effects::effect_info(id).map(|e| e.defaults) {
|
||||
Some(Value::Object(m)) => m,
|
||||
_ => Map::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn merged_params(id: &str, stored: &Map<String, Value>) -> Map<String, Value> {
|
||||
match vectorcraft_effects::merged_params(id, &Value::Object(stored.clone())) {
|
||||
Value::Object(m) => m,
|
||||
_ => default_params(id),
|
||||
}
|
||||
}
|
||||
|
||||
fn stack_of(d: &Dialog) -> Vec<StackEntry> {
|
||||
d.fields
|
||||
.get("stack")
|
||||
@@ -71,7 +85,13 @@ fn stack_of(d: &Dialog) -> Vec<StackEntry> {
|
||||
.filter_map(|row| {
|
||||
let effect = row.get("effect").and_then(Value::as_str)?;
|
||||
let visible = row.get("visible").and_then(Value::as_bool).unwrap_or(true);
|
||||
Some(StackEntry { effect: effect.to_string(), visible })
|
||||
let params = row
|
||||
.get("params")
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.map(|m| merged_params(effect, &m))
|
||||
.unwrap_or_else(|| default_params(effect));
|
||||
Some(StackEntry { effect: effect.to_string(), visible, params })
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
@@ -79,7 +99,10 @@ fn stack_of(d: &Dialog) -> Vec<StackEntry> {
|
||||
}
|
||||
|
||||
fn write_stack(d: &mut Dialog, stack: &[StackEntry]) {
|
||||
let rows: Vec<Value> = stack.iter().map(|e| json!({ "effect": e.effect, "visible": e.visible })).collect();
|
||||
let rows: Vec<Value> = stack
|
||||
.iter()
|
||||
.map(|e| json!({ "effect": e.effect, "visible": e.visible, "params": e.params }))
|
||||
.collect();
|
||||
d.fields.insert("stack".into(), Value::Array(rows));
|
||||
}
|
||||
|
||||
@@ -95,6 +118,82 @@ fn set_selected(d: &mut Dialog, id: &str) {
|
||||
d.fields.insert("selected".into(), json!(id));
|
||||
}
|
||||
|
||||
fn stack_focus(d: &Dialog) -> bool {
|
||||
d.fields.get("stackFocus").and_then(Value::as_bool).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn set_stack_focus(d: &mut Dialog, on: bool) {
|
||||
d.fields.insert("stackFocus".into(), json!(on));
|
||||
}
|
||||
|
||||
fn browse_params(d: &Dialog) -> Map<String, Value> {
|
||||
let effect = d.fields.get("browseEffect").and_then(Value::as_str).unwrap_or("");
|
||||
let stored = d.fields.get("browseParams").and_then(Value::as_object).cloned().unwrap_or_default();
|
||||
if effect.is_empty() {
|
||||
stored
|
||||
} else {
|
||||
merged_params(effect, &stored)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_browse_params(d: &mut Dialog, effect: &str, params: Map<String, Value>) {
|
||||
d.fields.insert("browseEffect".into(), json!(effect));
|
||||
d.fields.insert("browseParams".into(), Value::Object(params));
|
||||
}
|
||||
|
||||
/// The effect whose parameters the panel edits, and its values.
|
||||
fn param_target(d: &Dialog, stack: &[StackEntry], stack_sel: usize, browse: &str) -> (String, Map<String, Value>) {
|
||||
if stack_focus(d) {
|
||||
let entry = stack.get(stack_sel);
|
||||
if let Some(e) = entry {
|
||||
return (e.effect.clone(), e.params.clone());
|
||||
}
|
||||
}
|
||||
let id = browse.to_string();
|
||||
let params = if d.fields.get("browseEffect").and_then(Value::as_str) == Some(browse) {
|
||||
browse_params(d)
|
||||
} else {
|
||||
default_params(browse)
|
||||
};
|
||||
(id, params)
|
||||
}
|
||||
|
||||
fn write_param_target(d: &mut Dialog, stack: &mut [StackEntry], stack_sel: usize, browse: &str, effect: &str, params: Map<String, Value>) {
|
||||
if stack_focus(d) {
|
||||
if let Some(e) = stack.get_mut(stack_sel) {
|
||||
if e.effect == effect {
|
||||
e.params = params;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if browse == effect {
|
||||
set_browse_params(d, effect, params);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reuse the live effect dialog's fields for `effect_id` and `params`.
|
||||
fn effect_param_fields(app: &mut VectorcraftApp, ui: &mut egui::Ui, effect_id: &str, params: &mut Map<String, Value>) {
|
||||
if effect_id.is_empty() {
|
||||
return;
|
||||
}
|
||||
let label = vectorcraft_effects::effect_info(effect_id)
|
||||
.map(|e| e.label.trim_end_matches('…').to_string())
|
||||
.unwrap_or_else(|| effect_id.to_string());
|
||||
let mut fields = params.clone();
|
||||
fields.insert("__effect".into(), json!(effect_id));
|
||||
fields.insert("__label".into(), json!(label));
|
||||
fields.insert("preview".into(), json!(false));
|
||||
let mut temp = Dialog::new("effect", Value::Object(fields));
|
||||
(effect::SPEC.body)(app, ui, &mut temp);
|
||||
if let Value::Object(m) = form::params(&temp) {
|
||||
*params = m;
|
||||
}
|
||||
if let Value::Object(m) = vectorcraft_effects::upgrade_params(effect_id, &Value::Object(params.clone())) {
|
||||
*params = m;
|
||||
}
|
||||
}
|
||||
|
||||
/// A small procedural thumbnail from the effect id (no copied artwork).
|
||||
fn thumb(ui: &mut egui::Ui, id: &str, size: f32) {
|
||||
let (rect, _) = ui.allocate_exact_size(egui::vec2(size, size), Sense::hover());
|
||||
@@ -128,7 +227,7 @@ fn effect_label(id: &str) -> String {
|
||||
vectorcraft_effects::effect_info(id).map(|e| tl!(e.label).trim_end_matches('…').to_string()).unwrap_or_else(|| id.to_string())
|
||||
}
|
||||
|
||||
fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let cat = category_ix(d);
|
||||
let effects = effects_in_category(cat);
|
||||
@@ -138,6 +237,20 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
set_selected(d, &sel);
|
||||
}
|
||||
|
||||
let mut stack = stack_of(d);
|
||||
let mut stack_sel = d.fields.get("stackSelected").and_then(Value::as_u64).unwrap_or(0) as usize;
|
||||
if stack_sel >= stack.len() && !stack.is_empty() {
|
||||
stack_sel = stack.len() - 1;
|
||||
}
|
||||
let (param_effect, mut param_values) = param_target(d, &stack, stack_sel, &sel);
|
||||
if !param_effect.is_empty() {
|
||||
widgets::subheader(ui, tl!("Settings"));
|
||||
egui::ScrollArea::vertical().max_height(200.0).show(ui, |ui| {
|
||||
effect_param_fields(app, ui, ¶m_effect, &mut param_values);
|
||||
});
|
||||
write_param_target(d, &mut stack, stack_sel, &sel, ¶m_effect, param_values);
|
||||
}
|
||||
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
widgets::subheader(ui, tl!("Categories"));
|
||||
@@ -152,6 +265,7 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
d.fields.insert("category".into(), json!(i));
|
||||
let first = effects_in_category(i).first().map(|e| e.id).unwrap_or("");
|
||||
set_selected(d, first);
|
||||
set_stack_focus(d, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -168,25 +282,30 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let resp = ui.add(egui::Button::selectable(picked, name));
|
||||
if resp.clicked() {
|
||||
set_selected(d, e.id);
|
||||
set_stack_focus(d, false);
|
||||
}
|
||||
if resp.double_clicked() {
|
||||
set_selected(d, e.id);
|
||||
set_stack_focus(d, false);
|
||||
add_selected(d, e.id);
|
||||
stack = stack_of(d);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if widgets::flat_button(ui, tl!("Add to Stack"), 120.0).clicked() && !sel.is_empty() {
|
||||
add_selected(d, &sel);
|
||||
stack = stack_of(d);
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.vertical(|ui| {
|
||||
widgets::subheader(ui, tl!("Preview"));
|
||||
ui.set_width(200.0);
|
||||
if !sel.is_empty() {
|
||||
thumb(ui, &sel, 160.0);
|
||||
ui.label(egui::RichText::new(effect_label(&sel)).color(t.text_strong));
|
||||
let (param_effect, _) = param_target(d, &stack, stack_sel, &sel);
|
||||
if !param_effect.is_empty() {
|
||||
thumb(ui, ¶m_effect, 160.0);
|
||||
ui.label(egui::RichText::new(effect_label(¶m_effect)).color(t.text_strong));
|
||||
} else {
|
||||
ui.label(egui::RichText::new(tl!("Choose a filter")).color(t.text_dim));
|
||||
}
|
||||
@@ -195,7 +314,6 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
|
||||
ui.add_space(10.0);
|
||||
widgets::subheader(ui, tl!("Applied Filters"));
|
||||
let mut stack = stack_of(d);
|
||||
let mut stack_sel = d.fields.get("stackSelected").and_then(Value::as_u64).unwrap_or(0) as usize;
|
||||
if stack_sel >= stack.len() && !stack.is_empty() {
|
||||
stack_sel = stack.len() - 1;
|
||||
@@ -238,7 +356,10 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
});
|
||||
match op {
|
||||
Some(StackOp::Toggle(i)) if stack.get(i).is_some() => stack[i].visible = !stack[i].visible,
|
||||
Some(StackOp::Select(i)) => stack_sel = i,
|
||||
Some(StackOp::Select(i)) => {
|
||||
stack_sel = i;
|
||||
set_stack_focus(d, true);
|
||||
}
|
||||
Some(StackOp::Up(i)) if i > 0 => {
|
||||
stack.swap(i, i - 1);
|
||||
stack_sel = i - 1;
|
||||
@@ -261,10 +382,16 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
}
|
||||
|
||||
fn add_selected(d: &mut Dialog, id: &str) {
|
||||
let params = if !stack_focus(d) && d.fields.get("browseEffect").and_then(Value::as_str) == Some(id) {
|
||||
browse_params(d)
|
||||
} else {
|
||||
default_params(id)
|
||||
};
|
||||
let mut stack = stack_of(d);
|
||||
stack.push(StackEntry { effect: id.to_string(), visible: true });
|
||||
stack.push(StackEntry { effect: id.to_string(), visible: true, params });
|
||||
write_stack(d, &stack);
|
||||
d.fields.insert("stackSelected".into(), json!(stack.len().saturating_sub(1)));
|
||||
set_stack_focus(d, true);
|
||||
}
|
||||
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
@@ -275,7 +402,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
}
|
||||
let mut commands = Vec::with_capacity(stack.len() * 2);
|
||||
for (i, entry) in stack.iter().enumerate() {
|
||||
let params = vectorcraft_effects::effect_info(&entry.effect).map(|e| e.defaults.clone()).unwrap_or(json!({}));
|
||||
let params = Value::Object(entry.params.clone());
|
||||
commands.push(json!({ "command": "effect.apply", "params": { "effect": entry.effect, "params": params } }));
|
||||
if !entry.visible {
|
||||
commands.push(json!({ "command": "effect.setParams", "params": { "index": i, "visible": false } }));
|
||||
@@ -314,7 +441,10 @@ mod tests {
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
d.fields.insert(
|
||||
"stack".into(),
|
||||
json!([{ "effect": "blur.gaussian", "visible": true }, { "effect": "stylize.glowingEdges", "visible": false }]),
|
||||
json!([
|
||||
{ "effect": "artistic.cutout", "visible": true, "params": { "levels": 3 } },
|
||||
{ "effect": "stylize.glowingEdges", "visible": false, "params": { "edgeWidth": 2 } }
|
||||
]),
|
||||
);
|
||||
frame(&mut app);
|
||||
let steps = app.session.active().unwrap().history.undo.len();
|
||||
@@ -322,7 +452,8 @@ mod tests {
|
||||
let st = app.session.active().unwrap();
|
||||
let n = st.doc.node(vectorcraft_doc::NodeId(id)).unwrap();
|
||||
assert_eq!(n.appearance.effects.len(), 2);
|
||||
assert_eq!(n.appearance.effects[0].id, "blur.gaussian");
|
||||
assert_eq!(n.appearance.effects[0].id, "artistic.cutout");
|
||||
assert_eq!(n.appearance.effects[0].params.get("levels").and_then(|v| v.as_i64()), Some(3));
|
||||
assert_eq!(n.appearance.effects[1].id, "stylize.glowingEdges");
|
||||
assert!(!n.appearance.effects[1].visible);
|
||||
assert_eq!(st.history.undo.len(), steps + 1);
|
||||
|
||||
Reference in New Issue
Block a user