Merge #653: Image Trace: Advanced as a section that stays open, and View modes
# Conflicts: # ROADMAP.md
This commit is contained in:
commit
13c37e0bf4
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