G1: Add Warp Options controls for live effects

Expose existing Warp styles, orientation and percentage controls through the shared live effect dialog while preserving preview history and effect targets. Keep ordinary Enter owned by focused non-searchable combo controls.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
sanyamsingh001
2026-10-16 04:45:03 +07:00
committed by bflatastic
co-authored by Codex
parent ed6d474042
commit 848d902354
10 changed files with 789 additions and 16 deletions
+66 -2
View File
@@ -50,7 +50,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Effect Options",
[],
None,
"{index: int, params?: {…} (merged into the current parameters), visible?: bool, item?: appearance item index|null (as effect.remove), ids?|id?: [..], target?: \"object\"|\"contents\"} → {ids}",
"{index: int, params?: {…} (merged into the current parameters), warpStyle?: string (an existing Warp style suffix, e.g. arc or flag; changes only an applied Warp in place), visible?: bool, item?: appearance item index|null (as effect.remove), ids?|id?: [..], target?: \"object\"|\"contents\"} → {ids}",
has_doc,
set_params
),
@@ -301,12 +301,23 @@ fn set_params(s: &mut Session, p: &Value) -> Result<Value> {
if !params.is_null() && !params.is_object() {
return Err(bad(C, "`params` must be an object"));
}
let warp_id = p
.get("warpStyle")
.map(|v| -> Result<String> {
let style = v.as_str().filter(|style| effects::WarpStyle::from_id(style).is_some()).ok_or_else(|| bad(C, "unknown Warp style"))?;
Ok(format!("warp.{style}"))
})
.transpose()?;
let visible = p.get("visible").and_then(Value::as_bool);
let item = item_target(s, p, C)?;
for id in appearance_targets(s, p)? {
let n = s.doc()?.doc.node(id).ok_or(EngineError::NoNode(id))?;
let target = item.effects_item(&n.appearance, C)?;
if let Some(e) = n.appearance.effects_at(target).and_then(|fx| fx.get(index))
let effect = n.appearance.effects_at(target).and_then(|fx| fx.get(index));
if warp_id.is_some() && effect.is_some_and(|e| e.id.strip_prefix("warp.").and_then(effects::WarpStyle::from_id).is_none()) {
return Err(bad(C, "warpStyle can only edit an applied Warp effect"));
}
if let Some(e) = effect
&& effects::is_three_d(&e.id)
&& visible != Some(false)
{
@@ -323,6 +334,9 @@ fn set_params(s: &mut Session, p: &Value) -> Result<Value> {
}
let ids = edit_effects(s, p, item, C, "Effect Options", &format!("{C}: no effect at index {index}"), |fx| {
let Some(e) = fx.get_mut(index) else { return false };
if let Some(id) = &warp_id {
e.id.clone_from(id);
}
if let (Value::Object(new), cur) = (effects::upgrade_params(&e.id, &params), &mut e.params) {
if !cur.is_object() {
*cur = effects::merged_params(&e.id, &Value::Null);
@@ -783,6 +797,56 @@ mod tests {
assert!(s.execute("effect.apply", &json!({"effect": "nope"})).is_err());
}
#[test]
fn warp_options_style_change_preserves_slot_and_visibility() {
let (mut s, id) = session_with_rect();
for effect in ["path.offsetPath", "warp.arc", "stylize.roundCorners"] {
s.execute("effect.apply", &json!({"effect": effect, "item": 1})).unwrap();
}
s.execute("effect.setParams", &json!({"index": 1, "item": 1, "visible": false})).unwrap();
s.doc_mut().unwrap().mark_saved();
let before = s.doc().unwrap().doc.clone();
let undo = s.doc().unwrap().history.undo.len();
let result = s.execute("effect.setParams", &json!({"index": 1, "item": 1, "warpStyle": "flag", "params": {"bend": 25.0}}));
assert!(result.is_ok(), "an applied warp can change style in place: {result:?}");
let old = before.node(id).unwrap().appearance.effects_at(Some(1)).unwrap();
let n = node(&s, id);
let new = n.appearance.effects_at(Some(1)).unwrap();
assert_eq!(new.len(), 3);
assert_eq!(new[0], old[0]);
assert_eq!(new[2], old[2]);
assert_eq!(new[1].id, "warp.flag");
assert_eq!(new[1].params["bend"], 25.0);
assert!(!new[1].visible);
assert!(n.appearance.effects.is_empty());
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
let after = s.doc().unwrap().doc.clone();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc, before);
assert!(!s.doc().unwrap().is_dirty());
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc, after);
}
#[test]
fn warp_options_non_warp_target_rejection_is_atomic() {
let (mut s, first) = session_with_rect();
s.execute("effect.apply", &json!({"effect": "warp.arc", "item": null})).unwrap();
let second = s.execute("shape.rectangle", &json!({"x": 250, "y": 100, "width": 100, "height": 100})).unwrap()["id"].clone();
s.execute("effect.apply", &json!({"effect": "distort.twist", "item": null})).unwrap();
s.execute("select.set", &json!({"ids": [first.0, second]})).unwrap();
s.doc_mut().unwrap().mark_saved();
let before = s.doc().unwrap().doc.clone();
let selection = s.doc().unwrap().selection.clone();
let undo = s.doc().unwrap().history.undo.len();
assert!(s.execute("effect.setParams", &json!({"index": 0, "item": null, "warpStyle": "flag"})).is_err());
let state = s.doc().unwrap();
assert_eq!(state.doc, before);
assert_eq!(state.selection, selection);
assert_eq!(state.history.undo.len(), undo);
assert!(!state.is_dirty());
}
#[test]
fn list_remove_and_set_params() {
let (mut s, id) = session_with_rect();