Part of #641: Image Trace: Advanced as a section that stays open, and View modes
- The Image Trace panel's Advanced options sit under a full-width collapsible header (widgets::section_toggle, also used by New Document's Advanced Options); whether it is open is kept with the UI preferences. - View: Tracing Result, Tracing Result with Outlines, Outlines, Outlines with Source Image and Source Image, in the panel, the Control bar and the Properties panel. The view is saved in the Image Trace object's settings (`trace.view`), kept when it is traced again, set with the new `imageTrace.setView` command (or `view` on imageTrace.make) and drawn by the canvas only (RenderOptions::trace_views): exports, printing and Expand keep the tracing result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
033ceaff41
commit
5be583d1f7
File diff suppressed because one or more lines are too long
|
|
@ -40,6 +40,7 @@ pub mod slices;
|
|||
pub mod style_libs;
|
||||
pub mod swatches;
|
||||
pub mod text;
|
||||
pub mod trace;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -89,6 +90,7 @@ pub use text::{
|
|||
ParaDirection, ParaStyle, PathAlign, PathEffect, ScriptMetrics, TabAlign, TabStop, TextKind, TextObject, TextRun, TextStyleDef, TextWrap,
|
||||
VerticalAlign, WrapShape,
|
||||
};
|
||||
pub use trace::TraceView;
|
||||
pub use vectorcraft_color as color;
|
||||
pub use vectorcraft_geom as geom;
|
||||
|
||||
|
|
|
|||
|
|
@ -463,7 +463,8 @@ 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).
|
||||
/// Image Trace object: `{preset, params, view?}` it was traced with (the Image Trace panel shows
|
||||
/// them; `view` is its [`crate::TraceView`] id, absent for the tracing result).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub trace: Option<Box<serde_json::Value>>,
|
||||
/// Object → Text Wrap: area type below this object (in the same layer) flows around it.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
//! Image Trace objects' View: how a live trace draws on screen.
|
||||
//!
|
||||
//! An Image Trace object is a group whose `trace` holds `{preset, params, view}` (see
|
||||
//! `imageTrace.make`). The view is a viewing aid only: exports, printing and Expand always use the
|
||||
//! tracing result, whatever it is set to.
|
||||
|
||||
use crate::node::Node;
|
||||
|
||||
/// Image Trace › View.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum TraceView {
|
||||
/// The traced shapes as they are filled.
|
||||
#[default]
|
||||
Result,
|
||||
/// The traced shapes, with their outlines over them.
|
||||
ResultWithOutlines,
|
||||
/// Only the traced shapes' outlines.
|
||||
Outlines,
|
||||
/// The source image, with the traced shapes' outlines over it.
|
||||
OutlinesWithSource,
|
||||
/// Only the source image.
|
||||
Source,
|
||||
}
|
||||
|
||||
impl TraceView {
|
||||
/// Panel order.
|
||||
pub const ALL: [Self; 5] = [Self::Result, Self::ResultWithOutlines, Self::Outlines, Self::OutlinesWithSource, Self::Source];
|
||||
|
||||
/// The id used in command params and in the saved `trace` object.
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
Self::Result => "tracingResult",
|
||||
Self::ResultWithOutlines => "tracingResultWithOutlines",
|
||||
Self::Outlines => "outlines",
|
||||
Self::OutlinesWithSource => "outlinesWithSourceImage",
|
||||
Self::Source => "sourceImage",
|
||||
}
|
||||
}
|
||||
|
||||
/// The view an id names (any case).
|
||||
pub fn from_id(id: &str) -> Option<Self> {
|
||||
Self::ALL.into_iter().find(|v| v.id().eq_ignore_ascii_case(id))
|
||||
}
|
||||
|
||||
/// The view an Image Trace object's `trace` settings ask for (the tracing result when they
|
||||
/// name none, or an unknown one).
|
||||
pub fn of(trace: &serde_json::Value) -> Self {
|
||||
trace.get("view").and_then(serde_json::Value::as_str).and_then(Self::from_id).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Whether the source image shows.
|
||||
pub fn shows_source(self) -> bool {
|
||||
matches!(self, Self::OutlinesWithSource | Self::Source)
|
||||
}
|
||||
|
||||
/// Whether the traced shapes show filled.
|
||||
pub fn shows_result(self) -> bool {
|
||||
matches!(self, Self::Result | Self::ResultWithOutlines)
|
||||
}
|
||||
|
||||
/// Whether the traced shapes' outlines show.
|
||||
pub fn shows_outlines(self) -> bool {
|
||||
matches!(self, Self::ResultWithOutlines | Self::Outlines | Self::OutlinesWithSource)
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// The View of an Image Trace object (the tracing result for anything else, or an unknown id).
|
||||
pub fn trace_view(&self) -> TraceView {
|
||||
self.trace.as_deref().map(TraceView::of).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ids_round_trip_and_unknown_views_show_the_result() {
|
||||
for v in TraceView::ALL {
|
||||
assert_eq!(TraceView::from_id(v.id()), Some(v));
|
||||
assert_eq!(TraceView::from_id(&v.id().to_ascii_uppercase()), Some(v));
|
||||
assert!(v.shows_result() || v.shows_outlines() || v.shows_source(), "{v:?} shows something");
|
||||
}
|
||||
let mut g = Node::group(crate::NodeId(1), vec![]);
|
||||
assert_eq!(g.trace_view(), TraceView::Result);
|
||||
g.trace = Some(Box::new(serde_json::json!({"view": "outlines"})));
|
||||
assert_eq!(g.trace_view(), TraceView::Outlines);
|
||||
g.trace = Some(Box::new(serde_json::json!({"view": "sepia"})));
|
||||
assert_eq!(g.trace_view(), TraceView::Result);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ use std::sync::Arc;
|
|||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::appearance::{AppearanceItem, FillLayer, StrokeLayer};
|
||||
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection};
|
||||
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection, TraceView};
|
||||
use vectorcraft_geom::{FillRule, PathData, Point, Rect, SubPath};
|
||||
use vectorcraft_pathops as po;
|
||||
use vectorcraft_tools::builder::{
|
||||
|
|
@ -120,7 +120,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Make",
|
||||
["Object", "Image Trace"],
|
||||
None,
|
||||
"{id?: image or Image Trace group (default: selection), preset?: name (imageTrace.presets), params?: {mode: \"blackAndWhite\"|\"grayscale\"|\"color\", threshold: 0-255, colors: 2-256, paths: 0-100, corners: 0-100, noise: px, method: \"abutting\"|\"overlapping\", ignoreWhite, snapCurvesToLines}} → {id, paths, anchors, colors}",
|
||||
"{id?: image or Image Trace group (default: selection), preset?: name (imageTrace.presets), params?: {mode: \"blackAndWhite\"|\"grayscale\"|\"color\", threshold: 0-255, colors: 2-256, paths: 0-100, corners: 0-100, noise: px, method: \"abutting\"|\"overlapping\", ignoreWhite, snapCurvesToLines}, view?: as imageTrace.setView (default: the traced object's own, else tracingResult)} → {id, paths, anchors, colors}",
|
||||
has_selection_or_ids,
|
||||
|s, p| trace_make(s, p, false)
|
||||
),
|
||||
|
|
@ -151,6 +151,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
has_selection_or_ids,
|
||||
trace_expand
|
||||
),
|
||||
cmd!(
|
||||
"imageTrace.setView",
|
||||
"Image Trace View",
|
||||
[],
|
||||
None,
|
||||
"{id?|ids?: Image Trace objects (default: selection), view: \"tracingResult\"|\"tracingResultWithOutlines\"|\"outlines\"|\"outlinesWithSourceImage\"|\"sourceImage\"} how they draw on screen; exports, printing and Expand keep the tracing result → {ids, view}",
|
||||
has_selection_or_ids,
|
||||
trace_set_view
|
||||
),
|
||||
cmd!(query "imageTrace.presets", "Image Trace Presets", [], None, "{} → {presets: [{name, params}]}", always, |_, _| {
|
||||
Ok(json!({ "presets": tr::presets().into_iter().map(|(n, p)| json!({ "name": n, "params": p })).collect::<Vec<_>>() }))
|
||||
}),
|
||||
|
|
@ -639,6 +648,13 @@ fn is_trace_group(n: &Node) -> bool {
|
|||
&& n.children().is_some_and(|c| c.first().is_some_and(|i| matches!(i.kind, NodeKind::Image(_))))
|
||||
}
|
||||
|
||||
/// The `view` param ([`TraceView`] id), if given.
|
||||
fn view_param(cmd: &str, p: &Value) -> Result<Option<TraceView>> {
|
||||
let Some(v) = p.get("view").filter(|v| !v.is_null()) else { return Ok(None) };
|
||||
let ids = || TraceView::ALL.map(TraceView::id).join(", ");
|
||||
v.as_str().and_then(TraceView::from_id).map(Some).ok_or_else(|| bad(cmd, format!("view must be one of {}", ids())))
|
||||
}
|
||||
|
||||
fn trace_params(p: &Value) -> Result<tr::TraceParams> {
|
||||
let name = str_param(p, "preset").unwrap_or("Default");
|
||||
let base = tr::preset(name).ok_or_else(|| bad("imageTrace.make", format!("unknown preset `{name}` (see imageTrace.presets)")))?;
|
||||
|
|
@ -659,6 +675,7 @@ pub const MAX_TRACE_ANCHORS: usize = 2_000_000;
|
|||
fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
|
||||
const C: &str = "imageTrace.make";
|
||||
let params = trace_params(p)?;
|
||||
let view = view_param(C, 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");
|
||||
|
|
@ -721,7 +738,9 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
|
|||
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 })));
|
||||
// Tracing again keeps the object's view unless another is asked for.
|
||||
let view = view.unwrap_or_else(|| src.trace_view());
|
||||
g.trace = Some(Box::new(json!({ "preset": preset_label, "params": params_json, "view": view.id() })));
|
||||
}
|
||||
let (par, idx, _) = d.position(target).ok_or(EngineError::NoNode(target))?;
|
||||
d.remove(target)?;
|
||||
|
|
@ -741,6 +760,28 @@ fn trace_targets(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
|
|||
Ok(v)
|
||||
}
|
||||
|
||||
fn trace_set_view(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "imageTrace.setView";
|
||||
let view = view_param(C, p)?.ok_or_else(|| bad(C, "view is required"))?;
|
||||
let targets = trace_targets(s, p)?;
|
||||
let d = &s.doc()?.doc;
|
||||
if targets.iter().all(|g| d.node(*g).is_some_and(|n| n.trace_view() == view)) {
|
||||
return Ok(json!({ "ids": ids_json(&targets), "view": view.id() }));
|
||||
}
|
||||
s.edit("Image Trace View", |d, _| {
|
||||
for g in &targets {
|
||||
let n = d.node_mut(*g).ok_or(EngineError::NoNode(*g))?;
|
||||
if let Some(t) = n.trace.as_deref_mut().and_then(Value::as_object_mut) {
|
||||
t.insert("view".into(), json!(view.id()));
|
||||
} else {
|
||||
n.trace = Some(Box::new(json!({ "preset": "Custom", "view": view.id() })));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "ids": ids_json(&targets), "view": view.id() }))
|
||||
}
|
||||
|
||||
fn trace_release(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let targets = trace_targets(s, p)?;
|
||||
let ids = s.edit("Release Image Trace", |d, sel| {
|
||||
|
|
|
|||
|
|
@ -705,6 +705,85 @@ fn image_trace_object_remembers_its_settings() {
|
|||
assert!(st.doc.node(id).unwrap().trace.is_none());
|
||||
}
|
||||
|
||||
/// A black disc (r = 30 px) on light blue, 100 × 100 px, placed at 2× from (50, 50): traced in
|
||||
/// Black and White with Ignore White, the blue drops out and one disc (r = 60 pt at (150, 150))
|
||||
/// remains.
|
||||
fn disc_on_blue(s: &mut Session) -> NodeId {
|
||||
let r = vectorcraft_trace::Raster::from_fn(100, 100, |x, y| {
|
||||
let (dx, dy) = (x as f64 + 0.5 - 50.0, y as f64 + 0.5 - 50.0);
|
||||
if dx * dx + dy * dy <= 900.0 { [0, 0, 0, 255] } else { [170, 210, 255, 255] }
|
||||
});
|
||||
add_image(s, &r, Affine::translate((50.0, 50.0)) * Affine::scale(2.0));
|
||||
let r = s.execute("imageTrace.make", &json!({"params": {"ignoreWhite": true}})).unwrap();
|
||||
assert_eq!(r["paths"], 1);
|
||||
NodeId(r["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
/// The document rendered as the canvas draws it (`trace_views`) or as exports do.
|
||||
fn render_doc(s: &Session, trace_views: bool) -> vectorcraft_render::Rendered {
|
||||
let opts = vectorcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), trace_views, ..Default::default() };
|
||||
vectorcraft_render::Renderer::new().render(&s.doc().unwrap().doc, 300, 300, Affine::IDENTITY, &opts)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_trace_view_is_stored_kept_and_validated() {
|
||||
let mut s = session();
|
||||
let g = disc_on_blue(&mut s);
|
||||
assert_eq!(node(&s, g).trace.unwrap()["view"], "tracingResult");
|
||||
let r = s.execute("imageTrace.setView", &json!({"view": "outlinesWithSourceImage"})).unwrap();
|
||||
assert_eq!((ids(&r), r["view"].as_str()), (vec![g], Some("outlinesWithSourceImage")));
|
||||
assert_eq!(node(&s, g).trace_view(), vectorcraft_doc::TraceView::OutlinesWithSource);
|
||||
// Setting the view it has already adds no undo step.
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
s.execute("imageTrace.setView", &json!({"id": g.0, "view": "outlinesWithSourceImage"})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps);
|
||||
// Tracing again keeps it, unless another one is asked for; it survives save and open.
|
||||
let g = NodeId(s.execute("imageTrace.make", &json!({"id": g.0, "params": {"ignoreWhite": true, "noise": 30}})).unwrap()["id"].as_u64().unwrap());
|
||||
assert_eq!(node(&s, g).trace_view(), vectorcraft_doc::TraceView::OutlinesWithSource);
|
||||
let g = NodeId(s.execute("imageTrace.make", &json!({"id": g.0, "view": "SourceImage"})).unwrap()["id"].as_u64().unwrap());
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||||
assert_eq!(back.node(g).unwrap().trace_view(), vectorcraft_doc::TraceView::Source);
|
||||
// Bad views and other objects are refused.
|
||||
assert!(s.execute("imageTrace.setView", &json!({"view": "sepia"})).is_err());
|
||||
assert!(s.execute("imageTrace.setView", &json!({})).is_err());
|
||||
assert!(s.execute("imageTrace.make", &json!({"view": 3})).is_err());
|
||||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
assert!(s.execute("imageTrace.setView", &json!({"id": a.0, "view": "outlines"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_trace_views_draw_on_screen_only_and_expand_to_the_result() {
|
||||
let mut s = session();
|
||||
let g = disc_on_blue(&mut s);
|
||||
let dark = |p: [u8; 4]| p[0] < 60 && p[1] < 60 && p[2] < 60;
|
||||
let white = |p: [u8; 4]| p[0] > 245 && p[1] > 245 && p[2] > 245;
|
||||
let blue = |p: [u8; 4]| p[0] < 200 && p[2] > 245;
|
||||
let (centre, beside) = ((150, 150), (70, 70));
|
||||
let at = |img: &vectorcraft_render::Rendered, (x, y): (u32, u32)| img.pixel(x, y);
|
||||
// The outline is a 1 px line where the disc's right side is (x = 210, anti-aliased).
|
||||
let outlined = |img: &vectorcraft_render::Rendered| (205..=215).any(|x| img.pixel(x, 150)[0] < 128);
|
||||
let mut expanded = None;
|
||||
for view in vectorcraft_doc::TraceView::ALL {
|
||||
s.execute("imageTrace.setView", &json!({"id": g.0, "view": view.id()})).unwrap();
|
||||
let screen = render_doc(&s, true);
|
||||
let (c, b) = (at(&screen, centre), at(&screen, beside));
|
||||
assert_eq!(dark(c), view.shows_result() || view.shows_source(), "{view:?}: the disc's middle {c:?}");
|
||||
assert_eq!(blue(b), view.shows_source(), "{view:?}: the image's background {b:?}");
|
||||
assert!(view.shows_source() || white(b), "{view:?}: nothing beside the disc {b:?}");
|
||||
if view == vectorcraft_doc::TraceView::Outlines {
|
||||
assert!(outlined(&screen), "{view:?}: the disc's outline");
|
||||
}
|
||||
// Exports draw the tracing result whatever the view.
|
||||
let export = render_doc(&s, false);
|
||||
assert!(dark(at(&export, centre)) && white(at(&export, beside)), "{view:?}: export");
|
||||
// Expand keeps the traced shapes, as traced, whatever the view.
|
||||
s.execute("imageTrace.expand", &json!({"id": g.0})).unwrap();
|
||||
let shapes: Vec<_> = node(&s, g).children().unwrap().iter().map(|c| (c.path_data().cloned(), c.appearance.clone())).collect();
|
||||
assert_eq!(*expanded.get_or_insert_with(|| shapes.clone()), shapes, "{view:?}: expanded");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// #569: merging the regions of a rosette of unfilled ellipses (each off the centre, petals
|
||||
/// overlapping) merges every region the drags touch into one shape whose outline is only the
|
||||
/// merged area's border: no edge between merged regions stays in, as a stray line or a spur.
|
||||
|
|
|
|||
|
|
@ -298,7 +298,8 @@ Method:
|
|||
color, then threshold, colors, paths, corners, noise, method abutting or overlapping,
|
||||
ignoreWhite, snapCurvesToLines. Run imageTrace.presets to see every preset with its numbers.
|
||||
Lower paths and higher corners give the clean, logo-like result; high fidelity needs paths
|
||||
near 90.
|
||||
near 90. imageTrace.setView {view: \"outlinesWithSourceImage\"} draws the traced outlines
|
||||
over the image on screen to compare them (screenshots show it; exports keep the result).
|
||||
3. apply_effect {effect} on the traced shapes. Call apply_effect with no `effect` for the
|
||||
catalogue and each effect's parameters.
|
||||
4. set_paint over the traced shapes to give the artwork its own colours.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ pub mod proof;
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind, StrokeAlign, StrokeLayer, TextObject};
|
||||
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind, StrokeAlign, StrokeLayer, TextObject, TraceView};
|
||||
use vectorcraft_geom::{Affine, BezPath, FillRule, Rect, Shape};
|
||||
use vello_cpu::kurbo;
|
||||
use vello_cpu::peniko::{self, BlendMode, Compose, Mix};
|
||||
|
|
@ -100,6 +100,9 @@ pub struct RenderOptions {
|
|||
/// Screen view: placed documents draw from cached bitmaps made in the background (see
|
||||
/// [`placed_document`]); off, they draw exactly, read when needed.
|
||||
pub progressive_placed: bool,
|
||||
/// Screen view: Image Trace objects draw as their View asks (outlines, the source image…);
|
||||
/// off, they draw their tracing result, as exports and printing do.
|
||||
pub trace_views: bool,
|
||||
}
|
||||
|
||||
/// How edges are rasterized (raster export option).
|
||||
|
|
@ -165,6 +168,7 @@ impl Default for RenderOptions {
|
|||
highlight_substitutions: false,
|
||||
anti_alias: AntiAlias::Art,
|
||||
progressive_placed: false,
|
||||
trace_views: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -646,8 +650,10 @@ impl Renderer {
|
|||
let b = match &a.kind {
|
||||
NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } if !fx::has_object_fx(a) => {
|
||||
let mut acc: Option<Rect> = None;
|
||||
// An Image Trace object's hidden source image shows in some of its views.
|
||||
let source = a.trace.is_some().then(|| children.first()).flatten();
|
||||
for c in children {
|
||||
if c.visible {
|
||||
if c.visible || source.is_some_and(|s| Arc::ptr_eq(s, c)) {
|
||||
acc = vectorcraft_geom::union_opt(acc, self.bounds_of(c));
|
||||
}
|
||||
}
|
||||
|
|
@ -883,6 +889,9 @@ impl Renderer {
|
|||
/// What `n` draws inside its transparency group (`knockout`: its children knock each other out).
|
||||
fn draw_content(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, knockout: bool) {
|
||||
match &n.kind {
|
||||
NodeKind::Group { children, clip: false } if f.opts.trace_views && !f.opts.outline && n.trace_view() != TraceView::Result => {
|
||||
self.draw_trace_view(ctx, f, children, n.trace_view(), knockout)
|
||||
}
|
||||
NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } => {
|
||||
self.draw_children(ctx, f, children, knockout)
|
||||
}
|
||||
|
|
@ -955,6 +964,25 @@ impl Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
/// An Image Trace object's `children` (the hidden source image, then the traced shapes) as
|
||||
/// `view` shows them on screen.
|
||||
fn draw_trace_view(&mut self, ctx: &mut RenderContext, f: &Frame, children: &[Arc<Node>], view: TraceView, knockout: bool) {
|
||||
let Some((source, shapes)) = children.split_first() else { return };
|
||||
if view.shows_source() && matches!(source.kind, NodeKind::Image(_)) {
|
||||
self.draw_node(ctx, f, source, true);
|
||||
}
|
||||
if view.shows_result() {
|
||||
self.draw_children(ctx, f, shapes, knockout);
|
||||
}
|
||||
if view.shows_outlines() {
|
||||
let opts = RenderOptions { outline: true, ..f.opts.clone() };
|
||||
let frame = Frame { opts: &opts, ..*f };
|
||||
for c in shapes {
|
||||
self.draw_node(ctx, &frame, c, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The children of a group: as the elements of a knockout group when `knockout`.
|
||||
fn draw_children(&mut self, ctx: &mut RenderContext, f: &Frame, children: &[Arc<Node>], knockout: bool) {
|
||||
if knockout {
|
||||
|
|
|
|||
|
|
@ -284,6 +284,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
// pattern tiles stay smooth), as in Illustrator.
|
||||
anti_alias: if app.session.prefs.anti_aliased_artwork { vectorcraft_render::AntiAlias::Art } else { vectorcraft_render::AntiAlias::None },
|
||||
progressive_placed: true,
|
||||
trace_views: true,
|
||||
..opts
|
||||
};
|
||||
// Light documents render synchronously (no lag vs overlays); heavy ones go to the worker.
|
||||
|
|
|
|||
|
|
@ -441,17 +441,8 @@ fn choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str, options: &[(Value, Strin
|
|||
|
||||
/// The collapsible Advanced Options: Color Mode, Raster Effects and Preview Mode.
|
||||
fn advanced(ui: &mut egui::Ui, d: &mut Dialog, label: f32, field: f32) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let open = d.bool("advanced");
|
||||
let resp = ui
|
||||
.horizontal(|ui| {
|
||||
let (r, _) = ui.allocate_exact_size(egui::vec2(14.0, 18.0), egui::Sense::hover());
|
||||
crate::icons::paint(ui, if open { "chevron-down" } else { "chevron-right" }, r, t.icon);
|
||||
ui.label(egui::RichText::new(tl!("Advanced Options")).color(t.text));
|
||||
})
|
||||
.response
|
||||
.interact(egui::Sense::click());
|
||||
if resp.clicked() {
|
||||
if widgets::section_toggle(ui, tl!("Advanced Options"), open) {
|
||||
d.fields.insert("advanced".into(), json!(!open));
|
||||
}
|
||||
if !open {
|
||||
|
|
|
|||
|
|
@ -2487,6 +2487,13 @@
|
|||
Smooth Color Color suavizado
|
||||
Smooth Tool Herramienta Suavizar
|
||||
Snap Curves To Lines Ajustar curvas a líneas
|
||||
View: Vista:
|
||||
Tracing Result Resultado de calco
|
||||
Tracing Result with Outlines Resultado de calco con contornos
|
||||
Outlines Contornos
|
||||
Outlines with Source Image Contornos con imagen de origen
|
||||
Source Image Imagen de origen
|
||||
Image Trace View Vista del calco de imagen
|
||||
Snap Options Opciones de ajuste
|
||||
Snap to Glyph Ajustar a glifo
|
||||
Snap to Grid Ajustar a cuadrícula
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2484,6 +2484,13 @@
|
|||
Smooth Color Couleur lissée
|
||||
Smooth Tool Outil Arrondi
|
||||
Snap Curves To Lines Aimanter les courbes aux lignes
|
||||
View: Affichage :
|
||||
Tracing Result Résultat de la vectorisation
|
||||
Tracing Result with Outlines Résultat de la vectorisation avec contours
|
||||
Outlines Contours
|
||||
Outlines with Source Image Contours avec l’image source
|
||||
Source Image Image source
|
||||
Image Trace View Affichage de la vectorisation de l’image
|
||||
Snap Options Options de magnétisme
|
||||
Snap to Glyph Aimanter au glyphe
|
||||
Snap to Grid Aimanter à la grille
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2487,6 +2487,13 @@
|
|||
Smooth Color Colore graduale
|
||||
Smooth Tool Strumento Smussa
|
||||
Snap Curves To Lines Aggancia curve a linee
|
||||
View: Vista:
|
||||
Tracing Result Risultato ricalco
|
||||
Tracing Result with Outlines Risultato ricalco con contorni
|
||||
Outlines Contorni
|
||||
Outlines with Source Image Contorni con immagine di origine
|
||||
Source Image Immagine di origine
|
||||
Image Trace View Vista del ricalco immagine
|
||||
Snap Options Opzioni di aggancio
|
||||
Snap to Glyph Aggancia a glifo
|
||||
Snap to Grid Aggancia alla griglia
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2314,6 +2314,13 @@
|
|||
Smooth スムーズ
|
||||
Smooth Tool スムーズツール
|
||||
Snap Curves To Lines 曲線を直線にスナップ
|
||||
View: 表示:
|
||||
Tracing Result トレース結果
|
||||
Tracing Result with Outlines トレース結果(アウトライン付き)
|
||||
Outlines アウトライン
|
||||
Outlines with Source Image アウトライン(元画像付き)
|
||||
Source Image 元画像
|
||||
Image Trace View 画像トレースの表示
|
||||
Snap Options スナップオプション
|
||||
Snapping Tolerance スナップ許容値
|
||||
Soft Light ソフトライト
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2483,6 +2483,13 @@
|
|||
Smooth Color Сглаженный цвет
|
||||
Smooth Tool Инструмент «Сглаживание»
|
||||
Snap Curves To Lines Привязывать кривые к линиям
|
||||
View: Просмотр:
|
||||
Tracing Result Результат трассировки
|
||||
Tracing Result with Outlines Результат трассировки с контурами
|
||||
Outlines Контуры
|
||||
Outlines with Source Image Контуры с исходным изображением
|
||||
Source Image Исходное изображение
|
||||
Image Trace View Просмотр трассировки изображения
|
||||
Snap Options Параметры привязки
|
||||
Snap to Glyph Привязать к глифу
|
||||
Snap to Grid Привязать к сетке
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2424,6 +2424,13 @@
|
|||
Smooth Color 平滑颜色
|
||||
Smooth Tool 平滑工具
|
||||
Snap Curves To Lines 将曲线对齐到直线
|
||||
View: 视图:
|
||||
Tracing Result 描摹结果
|
||||
Tracing Result with Outlines 描摹结果(带轮廓)
|
||||
Outlines 轮廓
|
||||
Outlines with Source Image 轮廓(带源图像)
|
||||
Source Image 源图像
|
||||
Image Trace View 图像描摹视图
|
||||
Snap Options 对齐选项
|
||||
Snap to Glyph 靠齐字符
|
||||
Snap to Grid 对齐网格
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2507,6 +2507,13 @@
|
|||
Smooth Color 平滑顏色
|
||||
Smooth Tool 平滑工具
|
||||
Snap Curves To Lines 將曲線靠齊線條
|
||||
View: 檢視:
|
||||
Tracing Result 描圖結果
|
||||
Tracing Result with Outlines 描圖結果(含外框)
|
||||
Outlines 外框
|
||||
Outlines with Source Image 外框(含來源影像)
|
||||
Source Image 來源影像
|
||||
Image Trace View 影像描圖檢視
|
||||
Snap Options 靠齊選項
|
||||
Snap to Glyph 靠齊字符
|
||||
Snap to Grid 靠齊格點
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1,10 +1,12 @@
|
|||
//! Image Trace panel: preset, mode, threshold or colour count, and the advanced fidelity options.
|
||||
//! With an Image Trace object selected, changing a setting re-traces it (one undo step per change;
|
||||
//! sliders apply when released). With an image selected, Trace makes a new Image Trace object.
|
||||
//! Image Trace panel: preset, view, mode, threshold or colour count, and the Advanced section's
|
||||
//! fidelity options (open or closed as it was last left). With an Image Trace object selected,
|
||||
//! changing a setting re-traces it (one undo step per change; sliders apply when released) and
|
||||
//! changing the view redraws it without tracing again. With an image selected, Trace makes a new
|
||||
//! Image Trace object.
|
||||
|
||||
use egui::Ui;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
use vectorcraft_doc::{Node, NodeKind, TraceView};
|
||||
|
||||
use super::{first_selected, pstate, set_pstate};
|
||||
use crate::VectorcraftApp;
|
||||
|
|
@ -12,11 +14,12 @@ use crate::widgets::{self, menu_item};
|
|||
|
||||
const MODES: [(&str, &str); 3] = [("blackAndWhite", "Black and White"), ("grayscale", "Grayscale"), ("color", "Color")];
|
||||
|
||||
/// Panel state: the preset name, the current parameters and the last result's counts.
|
||||
/// Panel state: the preset name, the current parameters, the view and the last result's counts.
|
||||
#[derive(Clone, Default)]
|
||||
struct TraceUi {
|
||||
preset: String,
|
||||
params: Value,
|
||||
view: TraceView,
|
||||
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).
|
||||
|
|
@ -44,12 +47,44 @@ fn target(app: &VectorcraftApp) -> (bool, bool, Option<Value>) {
|
|||
(is_trace(&n), matches!(n.kind, NodeKind::Image(_)), n.trace.map(|t| *t))
|
||||
}
|
||||
|
||||
/// The preset of the one selected Image Trace object ("Custom" once its settings were changed).
|
||||
pub(crate) fn selected_preset(app: &VectorcraftApp) -> Option<String> {
|
||||
/// The one selected Image Trace object's preset ("Custom" once its settings were changed) and view.
|
||||
pub(crate) fn selected_trace(app: &VectorcraftApp) -> Option<(String, TraceView)> {
|
||||
let st = app.session.active()?;
|
||||
let [id] = st.selection.objects[..] else { return None };
|
||||
let n = st.doc.node(id).filter(|n| is_trace(n))?;
|
||||
Some(n.trace.as_ref().and_then(|t| t["preset"].as_str()).unwrap_or("Custom").to_string())
|
||||
Some((n.trace.as_ref().and_then(|t| t["preset"].as_str()).unwrap_or("Custom").to_string(), n.trace_view()))
|
||||
}
|
||||
|
||||
/// Image Trace › View, in the UI language.
|
||||
fn view_label(v: TraceView) -> &'static str {
|
||||
match v {
|
||||
TraceView::Result => tl!("Tracing Result"),
|
||||
TraceView::ResultWithOutlines => tl!("Tracing Result with Outlines"),
|
||||
TraceView::Outlines => tl!("Outlines"),
|
||||
TraceView::OutlinesWithSource => tl!("Outlines with Source Image"),
|
||||
TraceView::Source => tl!("Source Image"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The View dropdown showing `current` → the view chosen.
|
||||
fn view_dropdown(ui: &mut Ui, current: TraceView, width: f32) -> Option<TraceView> {
|
||||
let labels = TraceView::ALL.map(view_label);
|
||||
widgets::dropdown_names(ui, "trace-view", view_label(current), &labels, width).and_then(|i| TraceView::ALL.get(i).copied())
|
||||
}
|
||||
|
||||
/// Show the selected Image Trace objects with `view` (no new trace).
|
||||
fn set_view(app: &mut VectorcraftApp, view: TraceView) {
|
||||
if let Err(e) = app.run("imageTrace.setView", json!({ "view": view.id() })) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
}
|
||||
|
||||
/// The selected Image Trace object's View row (Control bar, Properties).
|
||||
pub(crate) fn view_row(app: &mut VectorcraftApp, ui: &mut Ui, current: TraceView, width: f32) {
|
||||
widgets::dim_label(ui, tl!("View:"));
|
||||
if let Some(v) = view_dropdown(ui, current, width) {
|
||||
set_view(app, v);
|
||||
}
|
||||
}
|
||||
|
||||
/// Trace the selected image, or trace the selected Image Trace object again, with built-in `preset`.
|
||||
|
|
@ -94,12 +129,13 @@ pub(crate) fn preset_dropdown(app: &mut VectorcraftApp, ui: &mut Ui, current: &s
|
|||
}
|
||||
}
|
||||
|
||||
/// The Control bar for one selected Image Trace object: its preset, the Image Trace panel and
|
||||
/// Expand. Whether it is one.
|
||||
/// The Control bar for one selected Image Trace object: its preset, view, the Image Trace panel
|
||||
/// and Expand. Whether it is one.
|
||||
pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) -> bool {
|
||||
let Some(preset) = selected_preset(app) else { return false };
|
||||
let Some((preset, view)) = selected_trace(app) else { return false };
|
||||
widgets::dim_label(ui, tl!("Preset:"));
|
||||
preset_dropdown(app, ui, &preset, 150.0);
|
||||
view_row(app, ui, view, 190.0);
|
||||
if widgets::icon_button(ui, "image", tl!("Image Trace"), false, 24.0).clicked() {
|
||||
app.ui.open_panel = Some("imageTrace".into());
|
||||
}
|
||||
|
|
@ -112,7 +148,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) -> bool {
|
|||
|
||||
fn trace(app: &mut VectorcraftApp, st: &mut TraceUi) {
|
||||
let preset = if st.preset == "Custom" { "Default" } else { st.preset.as_str() };
|
||||
match app.run("imageTrace.make", json!({ "preset": preset, "params": st.params })) {
|
||||
match app.run("imageTrace.make", json!({ "preset": preset, "params": st.params, "view": st.view.id() })) {
|
||||
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,
|
||||
}
|
||||
|
|
@ -143,6 +179,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
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.view = TraceView::of(&t);
|
||||
st.synced = t;
|
||||
}
|
||||
let mut retrace = false;
|
||||
|
|
@ -156,6 +193,15 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
retrace = true;
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("View:"));
|
||||
if let Some(v) = view_dropdown(ui, st.view, 170.0) {
|
||||
st.view = v;
|
||||
if is_trace {
|
||||
set_view(app, v);
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Mode:"));
|
||||
let mode = st.params["mode"].as_str().unwrap_or("blackAndWhite").to_string();
|
||||
|
|
@ -181,9 +227,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
retrace |= release;
|
||||
|
||||
let open: bool = !pstate::<bool>(ui.ctx(), "it-advanced-closed");
|
||||
if ui.add(egui::Button::new(format!("{} {}", if open { "▾" } else { "▸" }, tl!("Advanced"))).frame(false)).clicked() {
|
||||
set_pstate(ui.ctx(), "it-advanced-closed", open);
|
||||
ui.add_space(4.0);
|
||||
let open = app.ui.image_trace_advanced;
|
||||
if widgets::section_toggle(ui, tl!("Advanced"), open) {
|
||||
app.ui.image_trace_advanced = !open;
|
||||
}
|
||||
if open {
|
||||
for (key, label, range, suffix) in
|
||||
|
|
@ -258,6 +305,73 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use egui::{Event, PointerButton, Pos2, Rect, vec2};
|
||||
|
||||
/// One headless frame of the panel (and its menu) with `events`: the texts drawn, with where.
|
||||
fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<Event>) -> Vec<(String, Rect)> {
|
||||
let screen = Rect::from_min_size(Pos2::ZERO, vec2(280.0, 700.0));
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
crate::tests_removeanchors::shapes_text(&out.shapes.iter().map(|c| c.shape.clone()).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
/// Click the panel's `label` (the first one drawn).
|
||||
fn click(app: &mut VectorcraftApp, ctx: &egui::Context, label: &str) {
|
||||
let texts = frame(app, ctx, vec![]);
|
||||
let at = texts.iter().find(|(t, _)| t == label).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no `{label}` in {texts:?}"));
|
||||
let press = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(app, ctx, vec![Event::PointerMoved(at), press(true)]);
|
||||
frame(app, ctx, vec![press(false)]);
|
||||
}
|
||||
|
||||
fn traced_app() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
let img = image::RgbaImage::from_fn(20, 20, |x, _| image::Rgba(if x < 10 { [0, 0, 0, 255] } else { [255, 255, 255, 255] }));
|
||||
let mut png = vec![];
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
let data = vectorcraft_format::base64_encode(&png);
|
||||
app.run("file.place", json!({"name": "half.png", "dataBase64": data, "link": false})).unwrap();
|
||||
app.run("imageTrace.make", json!({"preset": "Default"})).unwrap();
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_is_a_section_that_stays_as_it_was_left() {
|
||||
let mut app = traced_app();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let has = |texts: &[(String, Rect)], s: &str| texts.iter().any(|(t, _)| t == s);
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
assert!(app.ui.image_trace_advanced && has(&texts, "Advanced") && has(&texts, "Corners"), "open at first: {texts:?}");
|
||||
click(&mut app, &ctx, "Advanced");
|
||||
assert!(!app.ui.image_trace_advanced, "the header closes it");
|
||||
assert!(!has(&frame(&mut app, &ctx, vec![]), "Corners"));
|
||||
// Remembered with the UI preferences, across launches.
|
||||
let saved: crate::state::UiState = serde_json::from_value(serde_json::to_value(&app.ui).unwrap()).unwrap();
|
||||
assert!(!saved.image_trace_advanced);
|
||||
let old: crate::state::UiState = serde_json::from_value(json!({})).unwrap();
|
||||
assert!(old.image_trace_advanced, "open for preferences saved before the section existed");
|
||||
click(&mut app, &ctx, "Advanced");
|
||||
assert!(app.ui.image_trace_advanced && has(&frame(&mut app, &ctx, vec![]), "Corners"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_view_dropdown_shows_and_sets_the_objects_view() {
|
||||
let mut app = traced_app();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
assert!(texts.iter().any(|(t, _)| t == "View:") && texts.iter().any(|(t, _)| t == "Tracing Result"), "{texts:?}");
|
||||
let steps = app.session.active().unwrap().history.undo.len();
|
||||
click(&mut app, &ctx, "Tracing Result");
|
||||
click(&mut app, &ctx, "Source Image");
|
||||
assert_eq!(selected_trace(&app).map(|t| t.1), Some(TraceView::Source));
|
||||
assert_eq!(app.session.active().unwrap().history.undo.len(), steps + 1, "one undo step, no new trace");
|
||||
// Another object's view is shown when it is selected.
|
||||
app.run("imageTrace.setView", json!({"view": "outlines"})).unwrap();
|
||||
assert!(frame(&mut app, &ctx, vec![]).iter().any(|(t, _)| t == "Outlines"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_and_menu_draw_headless() {
|
||||
|
|
@ -266,6 +380,7 @@ mod tests {
|
|||
app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
for _ in 0..2 {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
show(&mut app, ui);
|
||||
menu(&mut app, ui);
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
image_section(app, ui);
|
||||
divider(ui);
|
||||
}
|
||||
if let Some(preset) = crate::panels::image_trace::selected_preset(app) {
|
||||
trace_section(app, ui, &preset);
|
||||
if let Some((preset, view)) = crate::panels::image_trace::selected_trace(app) {
|
||||
trace_section(app, ui, &preset, view);
|
||||
divider(ui);
|
||||
}
|
||||
if matches!(first.as_ref().map(|n| &n.kind), Some(NodeKind::Text(_))) {
|
||||
|
|
@ -162,13 +162,14 @@ fn image_section(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
|
||||
/// The one selected Image Trace object: its preset (choosing another traces it again with that
|
||||
/// one), Expand and Release.
|
||||
fn trace_section(app: &mut VectorcraftApp, ui: &mut Ui, preset: &str) {
|
||||
/// one), view, Expand and Release.
|
||||
fn trace_section(app: &mut VectorcraftApp, ui: &mut Ui, preset: &str, view: vectorcraft_doc::TraceView) {
|
||||
section_header(ui, tl!("Image Trace"));
|
||||
ui.horizontal(|ui| {
|
||||
dim_label(ui, tl!("Preset:"));
|
||||
crate::panels::image_trace::preset_dropdown(app, ui, preset, ui.available_width());
|
||||
});
|
||||
ui.horizontal(|ui| crate::panels::image_trace::view_row(app, ui, view, ui.available_width()));
|
||||
let w = (ui.available_width() - 6.0) / 2.0;
|
||||
ui.horizontal(|ui| {
|
||||
for (label, id) in [(tl!("Expand"), "imageTrace.expand"), (tl!("Release"), "imageTrace.release")] {
|
||||
|
|
|
|||
|
|
@ -365,6 +365,9 @@ pub struct UiState {
|
|||
/// Layers panel › Panel Options… (row size, thumbnails, Show Layers Only).
|
||||
#[serde(default)]
|
||||
pub layers_panel: crate::panels::layers::PanelOptions,
|
||||
/// The Image Trace panel's Advanced section is open (as it was last left).
|
||||
#[serde(default = "yes")]
|
||||
pub image_trace_advanced: bool,
|
||||
/// The desktop window's size, position and maximized state, saved when the app quits and
|
||||
/// restored at the next launch (the desktop host reads and writes it; none on the web).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -518,6 +521,7 @@ impl Default for UiState {
|
|||
dxf_import: Value::Null,
|
||||
home: None,
|
||||
layers_panel: Default::default(),
|
||||
image_trace_advanced: true,
|
||||
window: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -308,7 +308,8 @@ fn click_label(app: &mut VectorcraftApp, ctx: &egui::Context, label: &str) {
|
|||
|
||||
#[test]
|
||||
fn the_control_bar_and_properties_trace_a_selected_image() {
|
||||
use crate::panels::image_trace::{TRACE_BUTTON_W, selected_preset};
|
||||
use crate::panels::image_trace::{TRACE_BUTTON_W, selected_trace};
|
||||
let selected_preset = |app: &VectorcraftApp| selected_trace(app).map(|t| t.0);
|
||||
use crate::tests_removeanchors::{at, click_control, control_frame, has, properties_frame};
|
||||
let mut app = app();
|
||||
let data = vectorcraft_format::base64_encode(&png(30, 30, 72.0));
|
||||
|
|
@ -332,19 +333,27 @@ fn the_control_bar_and_properties_trace_a_selected_image() {
|
|||
app.run("edit.undo", json!({})).unwrap();
|
||||
click_label(&mut app, &ctx, "Image Trace");
|
||||
assert_eq!(selected_preset(&app).as_deref(), Some("Default"));
|
||||
// An Image Trace object: its preset (another traces it again), the panel and Expand.
|
||||
// An Image Trace object: its preset (another traces it again), view, the panel and Expand.
|
||||
let bar = control_frame(&mut app, &ctx, vec![]);
|
||||
for s in ["Image Tracing", "Preset:", "Default", "Expand"] {
|
||||
for s in ["Image Tracing", "Preset:", "Default", "View:", "Tracing Result", "Expand"] {
|
||||
assert!(has(&bar, s), "{s}");
|
||||
}
|
||||
assert!(has(&properties_frame(&mut app, 260.0), "Release"));
|
||||
let props = properties_frame(&mut app, 260.0);
|
||||
assert!(has(&props, "Release") && has(&props, "View:"));
|
||||
// Another view redraws it without tracing again.
|
||||
let traced = app.session.active().unwrap().selection.objects.clone();
|
||||
click_control(&mut app, &ctx, at(&bar, "Tracing Result"));
|
||||
click_label(&mut app, &ctx, "Outlines with Source Image");
|
||||
assert_eq!(selected_trace(&app).map(|t| t.1), Some(vectorcraft_doc::TraceView::OutlinesWithSource));
|
||||
assert_eq!(app.session.active().unwrap().selection.objects, traced, "the same object");
|
||||
let bar = control_frame(&mut app, &ctx, vec![]);
|
||||
click_control(&mut app, &ctx, at(&bar, "Default"));
|
||||
click_label(&mut app, &ctx, "3 Colors");
|
||||
assert_eq!(selected_preset(&app).as_deref(), Some("3 Colors"));
|
||||
click_label(&mut app, &ctx, "Expand");
|
||||
let st = app.session.active().unwrap();
|
||||
let g = st.doc.node(st.selection.objects[0]).unwrap();
|
||||
assert!(selected_preset(&app).is_none() && g.children().unwrap().iter().all(|c| !matches!(c.kind, NodeKind::Image(_))), "expanded");
|
||||
assert!(selected_trace(&app).is_none() && g.children().unwrap().iter().all(|c| !matches!(c.kind, NodeKind::Image(_))), "expanded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ fn labels(items: &[Item]) -> Vec<&'static str> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn shapes_text(shapes: &[Shape]) -> Vec<(String, Rect)> {
|
||||
pub(crate) fn shapes_text(shapes: &[Shape]) -> Vec<(String, Rect)> {
|
||||
fn walk(s: &Shape, v: &mut Vec<(String, Rect)>) {
|
||||
match s {
|
||||
Shape::Text(t) => v.push((t.galley.text().to_string(), Rect::from_min_size(t.pos, t.galley.size()))),
|
||||
|
|
|
|||
|
|
@ -121,6 +121,21 @@ pub fn section_header(ui: &mut Ui, text: &str) {
|
|||
ui.add_space(2.0);
|
||||
}
|
||||
|
||||
/// A collapsible section's header across the panel: a chevron (down while `open`) and `text`;
|
||||
/// a click anywhere on it toggles the section. Whether it was clicked.
|
||||
pub fn section_toggle(ui: &mut Ui, text: &str, open: bool) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 24.0), Sense::click());
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(rect, 3.0, t.hover);
|
||||
}
|
||||
let chevron = Rect::from_center_size(pos2(rect.left() + 8.0, rect.center().y), vec2(12.0, 12.0));
|
||||
icons::paint(ui, if open { "chevron-down" } else { "chevron-right" }, chevron, if resp.hovered() { t.text_strong } else { t.icon });
|
||||
ui.painter().text(pos2(rect.left() + 18.0, rect.center().y), egui::Align2::LEFT_CENTER, tl!(text), theme::semibold(12.5), t.text);
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::CollapsingHeader, true, open, tl!(text)));
|
||||
resp.on_hover_cursor(egui::CursorIcon::PointingHand).clicked()
|
||||
}
|
||||
|
||||
/// Full-width 1 px divider with vertical margins.
|
||||
pub fn divider(ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
|
|
|
|||
Loading…
Reference in New Issue