M1.6/M12.5: Magic Wand panel (fill/stroke colour, stroke weight, opacity, blending mode with tolerances; settings in the engine, select.magicWand), Image Trace panel (presets, mode, threshold/colours, advanced, live re-trace), Glyphs reachable from the menus

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-01 09:01:56 -04:00
parent f673491bd2
commit e5959de6cc
3 changed files with 60 additions and 6 deletions

View File

@ -7,8 +7,8 @@
use std::collections::BTreeSet;
use drawcraft_doc::{AnchorRef, NodeId, NodeKind};
use drawcraft_doc::hit::hit_test;
use drawcraft_doc::{AnchorRef, NodeId, NodeKind};
use drawcraft_geom::Point;
use serde_json::{Value, json};

View File

@ -21,13 +21,17 @@ struct TraceUi {
fn presets(app: &mut DrawcraftApp) -> Vec<(String, Value)> {
let v = app.session.execute("imageTrace.presets", &json!({})).unwrap_or_default();
v["presets"].as_array().map(|a| a.iter().map(|p| (p["name"].as_str().unwrap_or("").to_string(), p["params"].clone())).collect()).unwrap_or_default()
v["presets"]
.as_array()
.map(|a| a.iter().map(|p| (p["name"].as_str().unwrap_or("").to_string(), p["params"].clone())).collect())
.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) };
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(_))));
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(_)))
}
@ -42,7 +46,11 @@ fn slider(ui: &mut Ui, label: &str, v: &mut f64, range: std::ops::RangeInclusive
let mut out = (false, false);
ui.horizontal(|ui| {
ui.add_sized([74.0, 22.0], egui::Label::new(egui::RichText::new(label).size(12.0)));
let r = ui.add(egui::Slider::new(v, range).suffix(suffix).integer());
// The theme's widget fill matches the panel, which would hide the rail.
let t = crate::theme::Tokens::get(ui.ctx());
ui.visuals_mut().widgets.inactive.bg_fill = t.input_border;
ui.visuals_mut().selection.bg_fill = t.accent;
let r = ui.add(egui::Slider::new(v, range).suffix(suffix).integer().trailing_fill(true));
out = (r.changed(), r.drag_stopped() || (r.changed() && !r.dragged()));
});
out
@ -79,7 +87,11 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) {
}
});
let bw = st.params["mode"].as_str() == Some("blackAndWhite");
let (key, label, range) = if bw { ("threshold", "Threshold", 0.0..=255.0) } else { ("colors", if st.params["mode"] == "grayscale" { "Grays" } else { "Colors" }, 2.0..=256.0) };
let (key, label, range) = if bw {
("threshold", "Threshold", 0.0..=255.0)
} else {
("colors", if st.params["mode"] == "grayscale" { "Grays" } else { "Colors" }, 2.0..=256.0)
};
let mut v = st.params[key].as_f64().unwrap_or(0.0);
let (changed, release) = slider(ui, label, &mut v, range, "");
if changed {
@ -93,7 +105,9 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) {
set_pstate(ui.ctx(), "it-advanced-closed", open);
}
if open {
for (key, label, range, suffix) in [("paths", "Paths", 0.0..=100.0, "%"), ("corners", "Corners", 0.0..=100.0, "%"), ("noise", "Noise", 1.0..=100.0, " px")] {
for (key, label, range, suffix) in
[("paths", "Paths", 0.0..=100.0, "%"), ("corners", "Corners", 0.0..=100.0, "%"), ("noise", "Noise", 1.0..=100.0, " px")]
{
let mut v = st.params[key].as_f64().unwrap_or(0.0);
let (changed, release) = slider(ui, label, &mut v, range, suffix);
if changed {
@ -153,3 +167,23 @@ pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) {
set_pstate(ui.ctx(), "image-trace", TraceUi::default());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn panel_and_menu_draw_headless() {
let mut app = DrawcraftApp::new(drawcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
for _ in 0..2 {
let ctx = egui::Context::default();
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
show(&mut app, ui);
menu(&mut app, ui);
});
out.textures_delta.clear();
}
}
}

View File

@ -62,3 +62,23 @@ pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) {
app.run("magicWand.set", json!({ "reset": true })).ok();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn panel_and_menu_draw_headless() {
let mut app = DrawcraftApp::new(drawcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
for _ in 0..2 {
let ctx = egui::Context::default();
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
show(&mut app, ui);
menu(&mut app, ui);
});
out.textures_delta.clear();
}
}
}