mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
M12.5: Image Trace objects remember their preset and parameters (saved in the file); the panel shows the selected trace's settings; Expand clears them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e5959de6cc
commit
c8f157cc87
@@ -260,6 +260,9 @@ pub struct Node {
|
||||
/// Opacity mask (Transparency panel). Its art lives here, outside the layer tree.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mask: Option<Box<OpacityMask>>,
|
||||
/// Image Trace object: `{preset, params}` it was traced with (the Image Trace panel shows them).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub trace: Option<Box<serde_json::Value>>,
|
||||
pub kind: NodeKind,
|
||||
}
|
||||
|
||||
@@ -301,6 +304,7 @@ impl Node {
|
||||
knockout: false,
|
||||
appearance: Appearance::default(),
|
||||
mask: None,
|
||||
trace: None,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,6 +533,7 @@ fn lp_expand(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
for g in &targets {
|
||||
let n = d.node_mut(*g).ok_or(EngineError::NoNode(*g))?;
|
||||
n.name = None;
|
||||
n.trace = None;
|
||||
if let Some(ch) = n.children_mut() {
|
||||
ch.retain(|c| match c.name.as_deref() {
|
||||
Some(SOURCES_NAME) => false,
|
||||
@@ -640,6 +641,13 @@ fn trace_params(p: &Value) -> Result<tr::TraceParams> {
|
||||
fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
|
||||
const C: &str = "imageTrace.make";
|
||||
let params = trace_params(p)?;
|
||||
let params_json = serde_json::to_value(¶ms).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
// The preset's name if the parameters are exactly that preset's, else "Custom".
|
||||
let named = str_param(p, "preset").unwrap_or("Default");
|
||||
let preset_label = match tr::preset(named) {
|
||||
Some(b) if serde_json::to_value(&b).ok().as_ref() == Some(¶ms_json) => named.to_string(),
|
||||
_ => "Custom".to_string(),
|
||||
};
|
||||
let roots = root_ids(s, p)?;
|
||||
// The image (inside a trace group, or selected directly).
|
||||
let (target, img) = {
|
||||
@@ -687,6 +695,9 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
|
||||
let gid = d.alloc_id();
|
||||
let mut g = Node::group(gid, children);
|
||||
g.name = if expand { None } else { Some(TRACE_NAME.into()) };
|
||||
if !expand {
|
||||
g.trace = Some(Box::new(json!({ "preset": preset_label, "params": params_json })));
|
||||
}
|
||||
let (par, idx, _) = d.position(target).ok_or(EngineError::NoNode(target))?;
|
||||
d.remove(target)?;
|
||||
d.insert(par, idx, g)?;
|
||||
@@ -727,6 +738,7 @@ fn trace_expand(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
for g in &targets {
|
||||
let n = d.node_mut(*g).ok_or(EngineError::NoNode(*g))?;
|
||||
n.name = None;
|
||||
n.trace = None;
|
||||
if let Some(ch) = n.children_mut() {
|
||||
ch.retain(|c| !matches!(c.kind, NodeKind::Image(_)));
|
||||
}
|
||||
|
||||
@@ -416,3 +416,26 @@ fn image_trace_presets_query_and_errors() {
|
||||
assert!(find_command("imageTrace.make").unwrap().menu == ["Object", "Image Trace"]);
|
||||
assert!(find_command("livePaint.make").unwrap().menu == ["Object", "Live Paint"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_trace_object_remembers_its_settings() {
|
||||
let mut s = session();
|
||||
image_doc(&mut s);
|
||||
let r = s.execute("imageTrace.make", &json!({"preset": "6 Colors"})).unwrap();
|
||||
let g = NodeId(r["id"].as_u64().unwrap());
|
||||
let t = node(&s, g).trace.clone().expect("settings stored");
|
||||
assert_eq!(t["preset"], "6 Colors");
|
||||
assert_eq!(t["params"]["colors"], 6);
|
||||
// Changing a parameter makes it Custom; the settings survive save/open.
|
||||
let r = s.execute("imageTrace.make", &json!({"id": g.0, "preset": "6 Colors", "params": {"colors": 9}})).unwrap();
|
||||
let g = NodeId(r["id"].as_u64().unwrap());
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let back = drawcraft_format::load(&drawcraft_format::save(&d, false)).unwrap();
|
||||
let t = back.node(g).unwrap().trace.clone().unwrap();
|
||||
assert_eq!((t["preset"].as_str(), t["params"]["colors"].as_u64()), (Some("Custom"), Some(9)));
|
||||
// Expanded traces are plain groups.
|
||||
s.execute("imageTrace.expand", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let id = st.selection.in_paint_order(&st.doc)[0];
|
||||
assert!(st.doc.node(id).unwrap().trace.is_none());
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ struct TraceUi {
|
||||
preset: String,
|
||||
params: Value,
|
||||
info: Option<(u64, u64, u64)>,
|
||||
/// The selected Image Trace object's stored settings last adopted (so edits in progress
|
||||
/// aren't overwritten until the object changes).
|
||||
synced: Value,
|
||||
}
|
||||
|
||||
fn presets(app: &mut DrawcraftApp) -> Vec<(String, Value)> {
|
||||
@@ -27,16 +30,17 @@ fn presets(app: &mut DrawcraftApp) -> Vec<(String, Value)> {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// What the selection is: an Image Trace object, a plain image, or neither.
|
||||
fn target(app: &DrawcraftApp) -> (bool, bool) {
|
||||
let Some(n) = first_selected(app) else { return (false, false) };
|
||||
/// What the selection is: an Image Trace object (with its stored settings), a plain image, or neither.
|
||||
fn target(app: &DrawcraftApp) -> (bool, bool, Option<Value>) {
|
||||
let Some(n) = first_selected(app) else { return (false, false, None) };
|
||||
let trace = n.name.as_deref() == Some("Image Trace")
|
||||
&& n.children().is_some_and(|c| c.first().is_some_and(|i| matches!(i.kind, drawcraft_doc::NodeKind::Image(_))));
|
||||
(trace, matches!(n.kind, drawcraft_doc::NodeKind::Image(_)))
|
||||
(trace, matches!(n.kind, drawcraft_doc::NodeKind::Image(_)), n.trace.map(|t| *t))
|
||||
}
|
||||
|
||||
fn trace(app: &mut DrawcraftApp, st: &mut TraceUi) {
|
||||
match app.run("imageTrace.make", json!({ "preset": "Default", "params": st.params })) {
|
||||
let preset = if st.preset == "Custom" { "Default" } else { st.preset.as_str() };
|
||||
match app.run("imageTrace.make", json!({ "preset": preset, "params": st.params })) {
|
||||
Ok(r) => st.info = Some((r["paths"].as_u64().unwrap_or(0), r["anchors"].as_u64().unwrap_or(0), r["colors"].as_u64().unwrap_or(0))),
|
||||
Err(e) => app.ui.status = e,
|
||||
}
|
||||
@@ -63,7 +67,12 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) {
|
||||
st.preset = "Default".into();
|
||||
st.params = all.first().map(|p| p.1.clone()).unwrap_or_default();
|
||||
}
|
||||
let (is_trace, is_image) = target(app);
|
||||
let (is_trace, is_image, stored) = target(app);
|
||||
if let Some(t) = stored.filter(|t| *t != st.synced) {
|
||||
st.preset = t["preset"].as_str().unwrap_or("Custom").to_string();
|
||||
st.params = t["params"].clone();
|
||||
st.synced = t;
|
||||
}
|
||||
let mut retrace = false;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
@@ -155,7 +164,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
|
||||
pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) {
|
||||
let (is_trace, _) = target(app);
|
||||
let (is_trace, _, _) = target(app);
|
||||
if menu_item(ui, "Release", is_trace, false) {
|
||||
app.run("imageTrace.release", json!({})).ok();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user