Merge pull request #2555 from dotCooCoo/feat/simplify-slider

Simplify: Object › Path › Simplify opens a slider with Auto-Simplify and More Options
This commit is contained in:
BFlat
2026-10-20 02:20:39 +07:00
committed by GitHub
42 changed files with 598 additions and 21 deletions
+19 -3
View File
@@ -130,7 +130,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Simplify…",
["Object", "Path"],
None,
"{tolerance?: pt (1), cornerAngle?: deg (30), straightLines?: bool} → {ids, before, after} anchor counts",
"{tolerance?: pt (1), level?: 0..100 (the Simplify slider, in place of tolerance: 0 fewest anchor points, 50 is 1 pt, every 10 levels halves or doubles it), auto?: bool (Auto-Simplify: the lowest level whose tolerance is at most 1/400 of the selection's longer side, or 100 when that side is under 12.5 pt), cornerAngle?: deg (30), straightLines?: bool} → {ids, before, after} anchor counts, and the level used with level or auto",
has_selection,
simplify
),
@@ -800,13 +800,29 @@ fn edit_paths(s: &mut Session, label: &str, f: impl Fn(&PathData) -> PathData) -
}
fn simplify(s: &mut Session, p: &Value) -> Result<Value> {
// With auto, the slider's level is Auto-Simplify's for the selection's size; otherwise it is the level given.
let level = if bool_or(p, "auto", false) {
let roots = selected_roots(s)?;
let size = s.doc()?.doc.bounds_of(&roots, false).map_or(0.0, |b| b.width().max(b.height()));
Some(po::auto_simplify_level(size))
} else {
num_param(p, "level").map(|l| if l.is_finite() { l.round().clamp(0.0, 100.0) } else { po::SIMPLIFY_LEVEL })
};
let tolerance = match level {
Some(l) => po::simplify_level_tolerance(l),
None => len_param(p, "tolerance").unwrap_or(1.0),
};
let opts = po::SimplifyOptions {
tolerance: len_param(p, "tolerance").unwrap_or(1.0).clamp(1e-4, 1e4),
tolerance: tolerance.clamp(1e-4, 1e4),
corner_angle_deg: num_param(p, "cornerAngle").unwrap_or(30.0).clamp(0.0, 180.0),
straight_lines: bool_or(p, "straightLines", false),
};
let (ids, before, after) = edit_paths(s, "Simplify", |path| po::simplify_with(path, &opts))?;
Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "before": before, "after": after }))
let mut out = json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "before": before, "after": after });
if let Some(l) = level {
out["level"] = json!(l);
}
Ok(out)
}
fn add_anchor_points(s: &mut Session, _: &Value) -> Result<Value> {
+1
View File
@@ -36,6 +36,7 @@ pub use journal::Journal;
pub use tooling::{UiRequest, ViewInfo};
pub use vectorcraft_doc as doc;
pub use vectorcraft_doc::cancel;
pub use vectorcraft_pathops::{SIMPLIFY_LEVEL, simplify_level_tolerance};
pub use vectorcraft_tools as tools;
#[derive(Debug, thiserror::Error)]
+52
View File
@@ -448,6 +448,58 @@ fn simplify_dense_polyline() {
assert!(close(bounds(&s, id), Rect::new(0.0, 60.0, 300.0, 140.0), 2.0));
}
/// A sine wave with an anchor every point over `width` pt, `amplitude` pt high and a wavelength of
/// `2π × period` pt, its anchors `jag` pt above and below it in turn, selected: (the session, the
/// path's id).
fn dense_wave(width: u32, amplitude: f64, period: f64, jag: f64) -> (Session, NodeId) {
let mut s = session();
let y = |i: u32| 100.0 + amplitude * (f64::from(i) / period).sin() + if i.is_multiple_of(2) { jag } else { -jag };
let anchors: Vec<Value> = (0..=width).map(|i| json!({"x": f64::from(i), "y": y(i)})).collect();
let r = s.execute("path.create", &json!({"anchors": anchors})).unwrap();
(s, NodeId(r["id"].as_u64().unwrap()))
}
/// The Simplify slider: moving its level from 0 to 100 never lowers the anchor count, level 0
/// leaves fewer anchors than level 100, and level 50 gives the 1 pt result.
#[test]
fn simplify_level_moves_the_anchor_count_monotonically() {
let (mut s, id) = dense_wave(300, 40.0, 25.0, 0.0);
let mut counts = vec![];
for level in (0..=100).step_by(10) {
let out = s.execute("object.path.simplify", &json!({ "level": level })).unwrap();
assert_eq!(out["level"], json!(f64::from(level)));
counts.push(path_of(&s, id).0.anchor_count());
s.execute("edit.undo", &json!({})).unwrap();
}
assert!(counts.windows(2).all(|w| w[0] <= w[1]), "{counts:?}");
assert!(counts[0] < counts[10], "{counts:?}");
s.execute("object.path.simplify", &json!({"tolerance": 1})).unwrap();
assert_eq!(path_of(&s, id).0.anchor_count(), counts[5], "level 50 is 1 pt");
}
/// Auto-Simplify picks the lowest level whose tolerance is at most 1/400 of the selection's longer
/// side, and 11 sampled points on each segment of the original path stay that close to the result.
#[test]
fn auto_simplify_keeps_the_shape_within_a_400th_of_its_size() {
let (mut s, id) = dense_wave(100, 15.0, 8.0, 0.2);
let original = path_of(&s, id).0.clone();
let out = s.execute("object.path.simplify", &json!({"auto": true})).unwrap();
// 100 pt across: 0.25 pt at most, level 70 (0.25 pt).
assert_eq!(out["level"], json!(70.0));
let result = path_of(&s, id).0.clone();
assert!(result.anchor_count() < original.anchor_count() / 2, "{}", result.anchor_count());
// How far each point along the original lies from the nearest point of the result.
use vectorcraft_geom::ParamCurve;
use vectorcraft_geom::kurbo::ParamCurveNearest;
let fitted = &result.subpaths[0];
let off = |p: vectorcraft_geom::Point| {
(0..fitted.segment_count()).map(|i| fitted.segment(i).nearest(p, 1e-9).distance_sq.sqrt()).fold(f64::INFINITY, f64::min)
};
let sp = &original.subpaths[0];
let worst = (0..sp.segment_count()).flat_map(|i| (0..=10).map(move |k| sp.segment(i).eval(f64::from(k) / 10.0))).map(off).fold(0.0, f64::max);
assert!(worst <= 100.0 / 400.0, "{worst}");
}
/// How many steps there are to undo and to redo, and whether the document has unsaved changes.
fn history(s: &Session) -> (usize, usize, bool) {
let st = s.doc().unwrap();
+3
View File
@@ -355,6 +355,9 @@ fn structured_junk() {
("object.path.offsetPath", json!({"offset": 1e308})),
("object.path.offsetPath", json!({"offset": -1e308})),
("object.path.simplify", json!({"tolerance": -1, "cornerAngle": 1e308})),
("object.path.simplify", json!({"level": 1e308, "tolerance": "x"})),
("object.path.simplify", json!({"level": -1e308, "auto": "yes"})),
("object.path.simplify", json!({"auto": true, "level": [1]})),
(
"shape.rectangularGrid",
json!({"x": 0, "y": 0, "width": 1e308, "height": -1e308, "rows": 999, "columns": u64::MAX, "rowSkew": 1e308, "columnSkew": -1e308, "frame": false, "fill": true}),
+24
View File
@@ -29,6 +29,30 @@ pub fn simplify(path: &PathData, tolerance: f64) -> PathData {
simplify_with(path, &SimplifyOptions { tolerance, ..Default::default() })
}
/// The Simplify slider's level for a tolerance of 1 pt, where the slider opens.
pub const SIMPLIFY_LEVEL: f64 = 50.0;
/// The tolerance (points) of Simplify slider level `level`, from 0 (the fewest anchor points) to
/// 100 (the closest to the original): 1 pt at [`SIMPLIFY_LEVEL`], doubled for every 10 levels below
/// it and halved for every 10 above, so level 0 is 32 pt and level 100 is 1/32 pt. A level outside
/// 0..=100 counts as the nearer end, and one that is not a number counts as [`SIMPLIFY_LEVEL`].
pub fn simplify_level_tolerance(level: f64) -> f64 {
let level = if level.is_finite() { level.clamp(0.0, 100.0) } else { SIMPLIFY_LEVEL };
((SIMPLIFY_LEVEL - level) / 10.0).exp2()
}
/// Auto-Simplify's level for paths whose bounds are `size` points on their longer side: the lowest
/// whole level whose tolerance ([`simplify_level_tolerance`]) is at most 1/400 of `size`. It returns
/// level 100 for a size of 0 or one that is not a number, and for a size under 12.5 pt, where no
/// level is that close.
pub fn auto_simplify_level(size: f64) -> f64 {
let target = size / 400.0;
if !(target.is_finite() && target > 0.0) {
return 100.0;
}
(SIMPLIFY_LEVEL - 10.0 * target.log2()).ceil().clamp(0.0, 100.0)
}
/// Simplify never adds anchors: keep whichever subpath is smaller.
fn no_worse(original: &SubPath, fitted: SubPath) -> SubPath {
if fitted.anchors.len() > original.anchors.len() { original.clone() } else { fitted }
+2 -2
View File
@@ -15,8 +15,8 @@ mod planar;
pub use boolean::{BoolOp, DEFAULT_PRECISION, area, boolean, boolean_n, normalize, try_boolean, try_normalize, unite_all};
pub use edit::{
AverageAxis, SimplifyOptions, add_anchor_points, average, join, remove_anchor, remove_redundant_points, simplify, simplify_with, smooth,
split_into_grid,
AverageAxis, SIMPLIFY_LEVEL, SimplifyOptions, add_anchor_points, auto_simplify_level, average, join, remove_anchor, remove_redundant_points,
simplify, simplify_level_tolerance, simplify_with, smooth, split_into_grid,
};
pub use offset::{Cap, Join, offset_path, outline_stroke, stroke_region};
pub use pathfinder::{FaceMerger, PathfinderOp, Region, Shape, merge_regions, pathfinder, region_at, regions};
+14
View File
@@ -343,6 +343,20 @@ fn simplify_straight_lines_option() {
assert!(max_deviation(&c, &s) <= 1.0 + 1e-6);
}
/// The Simplify slider's levels: 1 pt at 50, doubled for every 10 levels down and halved for every
/// 10 up; Auto-Simplify takes the lowest level whose tolerance is at most 1/400 of the size, and
/// level 100 for a size under 12.5 pt.
#[test]
fn simplify_levels_and_auto_simplify() {
assert_eq!(simplify_level_tolerance(SIMPLIFY_LEVEL), 1.0);
assert_eq!((simplify_level_tolerance(0.0), simplify_level_tolerance(100.0)), (32.0, 1.0 / 32.0));
assert_eq!((simplify_level_tolerance(-5.0), simplify_level_tolerance(500.0), simplify_level_tolerance(f64::NAN)), (32.0, 1.0 / 32.0, 1.0));
assert_eq!((auto_simplify_level(400.0), auto_simplify_level(100.0), auto_simplify_level(300.0)), (50.0, 70.0, 55.0));
assert!(simplify_level_tolerance(auto_simplify_level(300.0)) <= 300.0 / 400.0);
assert_eq!((auto_simplify_level(12.5), auto_simplify_level(10.0)), (100.0, 100.0));
assert_eq!((auto_simplify_level(0.0), auto_simplify_level(f64::INFINITY), auto_simplify_level(1e9)), (100.0, 100.0, 0.0));
}
#[test]
fn smooth_rounds_corners() {
let s = smooth(&rect(0.0, 0.0, 10.0, 10.0), 1.0);
+2 -1
View File
@@ -552,7 +552,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
rulers(ui, full, &xf, app.hover_doc, app.session.general_unit(), origin_preview.unwrap_or_else(|| app.ruler_origin()), &t);
}
if app.ui.task_bar && !revolve_edit && !app.session.tool_busy() && app.ui.screen_mode < 3 {
// The task bar is not drawn while the Simplify slider shows.
if app.ui.task_bar && !revolve_edit && !app.session.tool_busy() && app.ui.screen_mode < 3 && !crate::dialogs::simplify_slider::showing(app) {
task_bar(app, ui, &xf);
}
if app.ui.screen_mode < 3 {
+9 -2
View File
@@ -611,14 +611,21 @@ pub(super) fn preview_result(
}
return None;
}
preview_now(app, d, label, cmd, params)
}
/// The preview of [`preview_result`] without its checkbox: `params` run again on the interaction's
/// snapshot when they differ from the last preview. Returns the command's result only when a new
/// preview succeeds.
pub(super) fn preview_now(app: &mut crate::VectorcraftApp, d: &mut Dialog, label: &str, cmd: &str, params: Value) -> Option<Value> {
let mut result = None;
if d.fields.get(LAST) != Some(&params) || !app.session.in_interaction() {
if d.fields.get(PREVIEWED) != Some(&params) || !app.session.in_interaction() {
let _ = app.session.begin_interaction(label);
match app.session.preview(cmd, &params) {
Ok(r) => result = Some(r),
Err(e) => app.status(e.to_string()),
}
d.fields.insert(LAST.into(), params);
d.fields.insert(PREVIEWED.into(), params);
}
result
}
+2
View File
@@ -89,6 +89,7 @@ pub mod save_swatch_library;
pub mod script_check;
mod shapes;
pub mod shortcut_import;
pub(crate) mod simplify_slider;
pub mod slices;
pub mod spot_colors;
pub(crate) mod svg_options;
@@ -325,6 +326,7 @@ registry! {
Plugin: [plugin::KIND] => plugin::SPEC,
ScriptCheck: [script_check::KIND] => script_check::SPEC,
VectorHalftone: [halftone::KIND] => halftone::SPEC,
SimplifySlider: [simplify_slider::KIND] => simplify_slider::SPEC,
PerspectiveGrid: [perspective_grid::KIND] => perspective_grid::SPEC,
Envelope: [envelope::WARP, envelope::MESH, envelope::OPTIONS] => envelope::SPEC,
LiquifyOptions: [liquify::KIND] => liquify::SPEC,
+18 -2
View File
@@ -101,9 +101,18 @@ fn split_grid(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
form::check(ui, d, "addGuides", tl!("Add Guides"));
}
/// The fields the Simplify dialog opens with at `tolerance` (points): the slider's tolerance when
/// its More Options opens the dialog.
pub(crate) fn simplify_dialog(tolerance: f64) -> Value {
json!({"tolerance": tolerance, "angleThreshold": ANGLE_THRESHOLD, "straightLines": false, "showOriginal": false, "retain": false, "preview": true})
}
/// The fields the Simplify dialog keeps when OK confirms it with `retain` ticked.
const SIMPLIFY_KEPT: [&str; 6] = ["tolerance", "angleThreshold", "straightLines", "showOriginal", "retain", "preview"];
/// Simplify's fields: Tolerance, Corner Point Angle Threshold (its label above a slider from 0° on
/// the left, the smoothest, to 180°, the sharpest), Convert to Straight Lines and Show Original
/// Path.
/// the left, the smoothest, to 180°, the sharpest), Convert to Straight Lines, Show Original Path
/// and Open this dialog with these settings next time.
fn simplify_fields(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
let rail = Tokens::get(ui.ctx()).input_border;
egui::Grid::new("dlg").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| {
@@ -115,6 +124,7 @@ fn simplify_fields(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
ui.add_space(8.0);
form::check(ui, d, "straightLines", tl!("Convert to Straight Lines"));
form::check(ui, d, "showOriginal", tl!("Show Original Path"));
form::check(ui, d, "retain", tl!("Open this dialog with these settings next time"));
}
/// Simplify's Show Original Path: while the dialog previews with it on, every path the preview ran
@@ -175,6 +185,12 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
return run_and_close(app, id, params);
}
let r = form::commit_preview(app, id, params)?;
// Simplify: with retain ticked, the menu item opens this dialog on these fields next time; with it
// unticked, the menu item opens the slider.
if d.kind == "simplify" {
let kept = SIMPLIFY_KEPT.into_iter().filter_map(|k| Some((k.to_string(), d.fields.get(k)?.clone())));
app.ui.simplify = if d.bool("retain") { Value::Object(kept.collect()) } else { Value::Null };
}
// The fields as the dialog held them, so they show as typed when it opens again.
if d.kind == "offsetPath" {
let kept = ["offset", "joins", "miterLimit"].into_iter().filter_map(|k| Some((k.to_string(), d.fields.get(k)?.clone())));
@@ -0,0 +1,165 @@
//! Object › Path › Simplify's slider: a strip under the selection with Auto-Simplify, a slider from
//! the fewest anchor points (left) to the closest fit to the original (right), and More Options,
//! which opens the Simplify dialog at the slider's tolerance. While the strip is open, the canvas
//! previews `object.path.simplify {level}`; Enter or a click outside the strip keeps the result as
//! one undo step, and Escape puts the paths back. The slider opens at level 50, a tolerance of
//! 1 pt (`vectorcraft_pathops::simplify_level_tolerance` maps levels to tolerances).
//!
//! Fields: `level` (0..=100, the command's parameter).
use egui::{Color32, CornerRadius, Order, Pos2, Sense, Stroke, vec2};
use serde_json::{Value, json};
use vectorcraft_engine::{SIMPLIFY_LEVEL, simplify_level_tolerance};
use vectorcraft_geom::Rect;
use super::{DialogResult, DialogSpec, form, modal};
use crate::state::Dialog;
use crate::theme::Tokens;
use crate::{VectorcraftApp, widgets};
/// The dialog kind of the Simplify slider.
pub(crate) const KIND: &str = "simplifySlider";
pub(super) const SPEC: DialogSpec = DialogSpec { window: Some(show), confirm, preview: true, ..DialogSpec::FORM };
/// The command the slider previews, and its undo label.
const CMD: &str = "object.path.simplify";
const LABEL: &str = "Simplify";
/// The fields the slider opens with.
pub(crate) fn fields() -> Value {
json!({ "level": SIMPLIFY_LEVEL })
}
/// Whether the Simplify slider is open.
pub(crate) fn showing(app: &VectorcraftApp) -> bool {
app.ui.dialog.as_ref().is_some_and(|d| d.kind == KIND)
}
/// The slider's level, a whole number from 0 to 100.
fn level(d: &Dialog) -> f64 {
let l = d.f64("level", SIMPLIFY_LEVEL);
if l.is_finite() { l.round().clamp(0.0, 100.0) } else { SIMPLIFY_LEVEL }
}
fn params(d: &Dialog) -> Value {
json!({ "level": level(d) })
}
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> DialogResult {
form::commit_preview(app, CMD, params(d))
}
/// The open slider, or an error when it isn't open.
fn open_slider(app: &VectorcraftApp) -> Result<Dialog, String> {
app.ui.dialog.clone().filter(|d| d.kind == KIND).ok_or_else(|| "the Simplify slider is not open".to_string())
}
/// Auto-Simplify (`ui.simplifyAuto`): the open slider moves to the level `object.path.simplify
/// {auto: true}` picks for the selection, and the canvas previews it → {level}.
pub(crate) fn auto(app: &mut VectorcraftApp) -> Result<Value, String> {
let mut d = open_slider(app)?;
let _ = app.session.begin_interaction(LABEL);
let r = app.session.preview(CMD, &json!({ "auto": true })).map_err(|e| e.to_string())?;
let level = r["level"].as_f64().unwrap_or(SIMPLIFY_LEVEL);
d.fields.insert("level".into(), json!(level));
d.fields.insert(form::PREVIEWED.into(), params(&d));
app.ui.dialog = Some(d);
Ok(json!({ "level": level }))
}
/// More Options (`ui.simplifyMoreOptions`): the Simplify dialog takes the slider's place, at the
/// slider's tolerance → {tolerance}.
pub(crate) fn more_options(app: &mut VectorcraftApp) -> Result<Value, String> {
let d = open_slider(app)?;
let tolerance = simplify_level_tolerance(level(&d));
app.ui.dialog = Some(Dialog::new("simplify", super::path_ops::simplify_dialog(tolerance)));
Ok(json!({ "tolerance": tolerance }))
}
/// The private field that holds the selection's bounds before the preview, `[x0, y0, x1, y1]`.
const BOUNDS: &str = "__bounds";
/// The selection's bounds before the preview: the interaction's snapshot when a preview runs, read
/// once and kept in `d`, so the strip stays where it opened while the preview changes the paths.
fn bounds(app: &mut VectorcraftApp, d: &mut Dialog) -> Option<Rect> {
if let Some([x0, y0, x1, y1]) = d.fields.get(BOUNDS).and_then(|v| serde_json::from_value::<[f64; 4]>(v.clone()).ok()) {
return Some(Rect::new(x0, y0, x1, y1));
}
let b = match app.session.active().and_then(|st| st.interaction.as_ref()) {
Some(it) => it.doc.bounds_of(&it.selection.objects, true),
None => app.selection_bounds(),
}?;
d.fields.insert(BOUNDS.into(), json!([b.x0, b.y0, b.x1, b.y1]));
Some(b)
}
/// Where the strip goes: under the lowest corner on screen of `bounds` (the selection's, before the
/// preview), centered on them, kept on the canvas.
fn place(app: &mut VectorcraftApp, ctx: &egui::Context, id: egui::Id, bounds: Option<Rect>) -> Pos2 {
let size = egui::AreaState::load(ctx, id).and_then(|s| s.size).unwrap_or(vec2(270.0, 42.0));
let screen = ctx.content_rect();
let Some((rect, view)) = app.canvas_rect.zip(app.view().copied()) else { return screen.center() - size / 2.0 };
let xf = crate::canvas::Xf::new(rect, &view);
let under = match bounds {
Some(b) => {
let lowest = xf.quad(b).iter().map(|c| c.y).fold(f32::NEG_INFINITY, f32::max);
Pos2::new(xf.to_screen(b.center()).x - size.x / 2.0, lowest + 28.0)
}
None => rect.center() - size / 2.0,
};
let bounds = rect.shrink(8.0);
under.clamp(bounds.min, (bounds.max - size).max(bounds.min))
}
/// What a button of the strip asked for.
enum Action {
Auto,
MoreOptions,
}
fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
let Some(mut d) = app.ui.dialog.clone() else { return };
let t = Tokens::get(ctx);
let id = egui::Id::new(("dialog", KIND));
let b = bounds(app, &mut d);
let pos = place(app, ctx, id, b);
// A click anywhere else keeps the result, and the rest of the app doesn't take it.
let dim = modal::backdrop();
let outside =
egui::Area::new(dim.id).order(dim.order).fixed_pos(Pos2::ZERO).show(ctx, |ui| ui.allocate_rect(ctx.content_rect(), Sense::click())).inner;
let mut action = None;
egui::Area::new(id).order(Order::Foreground).fixed_pos(pos).show(ctx, |ui| {
egui::Frame::NONE
.fill(t.panel)
.stroke(Stroke::new(1.0, t.tool_active))
.corner_radius(CornerRadius::same(5))
.inner_margin(egui::Margin::symmetric(8, 6))
.shadow(egui::epaint::Shadow { offset: [0, 3], blur: 10, spread: 0, color: Color32::from_black_alpha(70) })
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 6.0;
if widgets::icon_button(ui, "wand-sparkles", tl!("Auto-Simplify"), false, 28.0).clicked() {
action = Some(Action::Auto);
}
let rail = t.input_border;
form::slider_rail_named(ui, &mut d, "level", tl!("Simplify"), 0.0..=100.0, &|_| rail);
if widgets::icon_button(ui, "ellipsis", tl!("More Options"), false, 28.0).clicked() {
action = Some(Action::MoreOptions);
}
});
});
});
let p = params(&d);
let _ = form::preview_now(app, &mut d, LABEL, CMD, p);
app.ui.dialog = Some(d);
let r = match action {
Some(Action::Auto) => auto(app),
Some(Action::MoreOptions) => more_options(app),
None if outside.clicked() || ctx.input(|i| i.key_pressed(egui::Key::Enter)) => super::confirm(app),
None => Ok(Value::Null),
};
if let Err(e) = r {
app.status(e);
}
}
+4
View File
@@ -474,6 +474,10 @@ fn menu_parameter_dialogs_are_compact() {
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
app.run("select.all", json!({})).unwrap();
crate::menus::invoke(&mut app, id, json!({}));
// Simplify opens its slider; More Options opens the dialog.
if id == "object.path.simplify" {
app.run("ui.simplifyMoreOptions", json!({})).unwrap();
}
assert!(app.ui.dialog.is_some(), "{id} opened no dialog");
let wide = dialog_rect(&mut app, 1600.0);
eprintln!("{id}: {:.0} × {:.0} in a 1600-point window", wide.width(), wide.height());
+173 -5
View File
@@ -292,13 +292,181 @@ fn app_with_dense_path() -> VectorcraftApp {
app
}
/// Object › Path › Simplify, then the slider's More Options: the Simplify dialog.
fn open_simplify_dialog(app: &mut VectorcraftApp) {
crate::menus::invoke(app, "object.path.simplify", json!({}));
app.run("ui.simplifyMoreOptions", json!({})).unwrap();
}
/// The anchors of the paths in `app`'s document.
fn anchor_count(app: &VectorcraftApp) -> usize {
let mut n = 0;
app.session.doc().unwrap().doc.walk(|node| {
if let vectorcraft_doc::NodeKind::Path { path, .. } = &node.kind {
n += path.anchor_count();
}
});
n
}
/// The parameters of the Simplify preview on the canvas.
fn simplify_preview(app: &VectorcraftApp) -> serde_json::Value {
app.session.doc().unwrap().interaction.as_ref().and_then(|it| it.preview.clone()).expect("a preview").1
}
/// The Simplify slider's strip, and the centers of its Auto-Simplify button, its rail and its More
/// Options button.
fn slider_strip(ctx: &egui::Context) -> (Rect, Pos2, Pos2, Pos2) {
let r = rect_of(ctx, egui::Id::new(("dialog", simplify_slider::KIND)));
let y = r.center().y;
(r, Pos2::new(r.left() + 8.0 + 14.0, y), Pos2::new(r.left() + 8.0 + 28.0 + 6.0 + 90.0, y), Pos2::new(r.right() - 8.0 - 14.0, y))
}
/// Object › Path › Simplify opens a slider under the selection that previews the 1 pt result.
/// Dragging it to the right previews more anchor points and to the left fewer, and Enter keeps the
/// result as one undo step.
#[test]
fn simplify_opens_a_slider_whose_drag_previews_fewer_or_more_points() {
let (ctx, mut app) = (ctx(), app_with_dense_path());
let undo_len = app.session.doc().unwrap().history.undo.len();
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(simplify_slider::KIND));
settle(&ctx, &mut app);
assert_eq!(simplify_preview(&app), json!({"level": 50.0}));
assert_eq!(anchor_count(&app), 2, "the 1 pt result");
let (_, _, rail, _) = slider_strip(&ctx);
drag(&ctx, &mut app, rail, vec2(100.0, 0.0));
assert_eq!(simplify_preview(&app), json!({"level": 100.0}));
let most = anchor_count(&app);
assert!(most > 2, "{most}");
let (_, _, rail, _) = slider_strip(&ctx);
drag(&ctx, &mut app, rail + vec2(90.0, 0.0), vec2(-190.0, 0.0));
assert_eq!(simplify_preview(&app), json!({"level": 0.0}));
assert_eq!(anchor_count(&app), 2);
frame(
&ctx,
&mut app,
vec![Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }],
);
assert!(app.ui.dialog.is_none() && !app.session.in_interaction());
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo_len + 1);
assert_eq!(anchor_count(&app), 2);
}
/// Escape, taken by the app's key handling before the dialog layer draws, closes the Simplify
/// slider and puts the paths back as they were, with no undo step.
#[test]
fn simplify_slider_escape_puts_the_paths_back() {
let (ctx, mut app) = (ctx(), app_with_dense_path());
let before = app.session.doc().unwrap().doc.clone();
let undo_len = app.session.doc().unwrap().history.undo.len();
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
settle(&ctx, &mut app);
let (_, _, rail, _) = slider_strip(&ctx);
drag(&ctx, &mut app, rail, vec2(100.0, 0.0));
assert_eq!(simplify_preview(&app), json!({"level": 100.0}));
assert_ne!(app.session.doc().unwrap().doc, before, "the preview changes the path");
let escape = Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
let input = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, SCREEN)), events: vec![escape], ..Default::default() };
let mut out = ctx.run_ui(input, |ui| {
crate::shortcuts::handle(&mut app, ui.ctx());
show(&mut app, ui.ctx());
});
out.textures_delta.clear();
assert!(app.ui.dialog.is_none() && !app.session.in_interaction());
assert_eq!(app.session.doc().unwrap().doc, before);
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo_len);
assert_eq!(anchor_count(&app), 20);
}
/// The Simplify slider stays where it opened, under the paths as they were, while the preview
/// changes their bounds, so a drag doesn't move the rail under the pointer.
#[test]
fn simplify_slider_stays_where_it_opened_while_the_preview_changes_the_bounds() {
let ctx = ctx();
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.canvas_rect = Some(Rect::from_min_size(Pos2::ZERO, SCREEN));
// A hump 20 pt high, which level 0 (32 pt) fits with a lower curve, seen at 400%.
let anchors: Vec<_> = (0..20).map(|i| json!({"x": 30 + i * 15, "y": 100.0 + 20.0 * (std::f64::consts::PI * i as f64 / 19.0).sin()})).collect();
app.run("path.create", json!({"anchors": anchors})).unwrap();
app.run("view.setZoom", json!({"zoom": 400})).unwrap();
app.view_mut().unwrap().center = vectorcraft_geom::Point::new(172.5, 60.0);
let before = app.selection_bounds().unwrap();
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
settle(&ctx, &mut app);
let opened = rect_of(&ctx, egui::Id::new(("dialog", simplify_slider::KIND)));
app.ui.dialog.as_mut().unwrap().fields.insert("level".into(), json!(0));
settle(&ctx, &mut app);
assert_eq!(simplify_preview(&app), json!({"level": 0.0}));
let after = app.selection_bounds().unwrap();
assert!(after.height() < before.height() - 0.5, "the preview changes the bounds: {before:?} → {after:?}");
assert_eq!(rect_of(&ctx, egui::Id::new(("dialog", simplify_slider::KIND))), opened);
}
/// Auto-Simplify moves the slider to the level that keeps the path within 1/400 of its size, and a
/// click outside the strip keeps the result as one undo step.
#[test]
fn simplify_slider_auto_simplify_and_a_click_outside_keep_the_result() {
let (ctx, mut app) = (ctx(), app_with_dense_path());
let before = app.session.doc().unwrap().doc.clone();
let undo_len = app.session.doc().unwrap().history.undo.len();
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
settle(&ctx, &mut app);
let (_, auto, ..) = slider_strip(&ctx);
click(&ctx, &mut app, auto);
// The path is 285 pt wide: 0.7125 pt at most, level 55.
assert_eq!(app.ui.dialog.as_ref().unwrap().f64("level", 0.0), 55.0);
assert_eq!(app.session.doc().unwrap().interaction.as_ref().and_then(|it| it.preview.clone()).unwrap().1, json!({"auto": true}));
frame(&ctx, &mut app, vec![]);
assert_eq!(anchor_count(&app), 2);
assert!(app.session.in_interaction(), "the result waits for a click outside");
click(&ctx, &mut app, Pos2::new(20.0, 20.0));
assert!(app.ui.dialog.is_none() && !app.session.in_interaction());
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo_len + 1);
assert_ne!(app.session.doc().unwrap().doc, before);
}
/// More Options opens the Simplify dialog at the slider's tolerance. With "Open this dialog with
/// these settings next time" ticked, OK makes the menu item open the dialog on its settings; with
/// it unticked, the slider again.
#[test]
fn simplify_slider_more_options_and_the_dialog_that_opens_directly() {
let (ctx, mut app) = (ctx(), app_with_dense_path());
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
app.ui.dialog.as_mut().unwrap().fields.insert("level".into(), json!(60));
settle(&ctx, &mut app);
assert_eq!(simplify_preview(&app), json!({"level": 60.0}));
let (_, _, _, more) = slider_strip(&ctx);
click(&ctx, &mut app, more);
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!((d.kind.as_str(), d.f64("tolerance", 0.0), d.bool("retain")), ("simplify", 0.5, false));
settle(&ctx, &mut app);
assert_eq!(simplify_preview(&app)["tolerance"], json!(0.5));
let texts = frame(&ctx, &mut app, vec![]);
let retain = texts.iter().find(|(t, _)| t == "Open this dialog with these settings next time").map(|(_, r)| *r).expect("the checkbox");
click(&ctx, &mut app, retain.center());
app.ui.dialog.as_mut().unwrap().fields.insert("straightLines".into(), json!(true));
confirm(&mut app).unwrap();
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
let d = app.ui.dialog.clone().unwrap();
assert_eq!((d.kind.as_str(), d.f64("tolerance", 0.0), d.bool("straightLines"), d.bool("retain")), ("simplify", 0.5, true, true));
app.ui.dialog.as_mut().unwrap().fields.insert("retain".into(), json!(false));
confirm(&mut app).unwrap();
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(simplify_slider::KIND));
assert!(app.run("ui.simplifyAuto", json!({})).is_ok());
cancel(&mut app);
assert!(app.run("ui.simplifyMoreOptions", json!({})).is_err(), "the slider is not open");
}
#[test]
fn simplify_dialog_preview_counts_and_cancel_restore_the_original() {
let (ctx, mut app) = (ctx(), app_with_dense_path());
let before = app.session.doc().unwrap().doc.clone();
let selection = app.session.doc().unwrap().selection.clone();
let undo_len = app.session.doc().unwrap().history.undo.len();
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
open_simplify_dialog(&mut app);
settle(&ctx, &mut app);
let texts = frame(&ctx, &mut app, vec![]);
let preview = texts.iter().find(|(t, _)| t == "Preview").map(|(_, r)| *r).expect("a Preview checkbox");
@@ -337,7 +505,7 @@ fn simplify_dialog_ok_keeps_exactly_one_undo_with_or_without_preview() {
let before = app.session.doc().unwrap().doc.clone();
let selection = app.session.doc().unwrap().selection.clone();
let undo_len = app.session.doc().unwrap().history.undo.len();
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
open_simplify_dialog(&mut app);
app.ui.dialog.as_mut().unwrap().fields.insert("preview".into(), json!(on));
settle(&ctx, &mut app);
if on {
@@ -381,7 +549,7 @@ fn only_straight_segments(app: &VectorcraftApp) -> bool {
fn simplify_dialog_sends_corner_angle_and_straight_lines() {
let (ctx, mut app) = (ctx(), app_with_selection());
app.run("shape.ellipse", json!({"x": 100, "y": 200, "width": 160, "height": 90})).unwrap();
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
open_simplify_dialog(&mut app);
settle(&ctx, &mut app);
let texts = frame(&ctx, &mut app, vec![]);
assert!(texts.iter().any(|(t, _)| t == "Corner Point Angle Threshold:"), "{texts:?}");
@@ -407,7 +575,7 @@ fn simplify_dialog_corner_point_angle_threshold_keeps_or_smooths_corners() {
let (ctx, mut app) = (ctx(), app_with_selection());
app.run("edit.clear", json!({})).unwrap();
app.run("shape.polygon", json!({"cx": 200, "cy": 200, "radius": 100, "sides": 8})).unwrap();
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
open_simplify_dialog(&mut app);
app.ui.dialog.as_mut().unwrap().fields.insert("tolerance".into(), json!("10 pt"));
settle(&ctx, &mut app);
assert!(only_straight_segments(&app), "the corners stay and the sides stay straight");
@@ -426,7 +594,7 @@ fn simplify_dialog_show_original_path_outlines_the_paths_before_the_preview() {
Some((id, vectorcraft_doc::NodeKind::Path { path, .. })) => (id, path.clone()),
_ => panic!("a path is selected"),
};
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
open_simplify_dialog(&mut app);
settle(&ctx, &mut app);
let texts = frame(&ctx, &mut app, vec![]);
assert!(super::path_ops::original_paths(&app).is_empty(), "Show Original Path is off as the dialog opens");
+2
View File
@@ -2868,6 +2868,8 @@
Show Note Показване на бележка
Show Options Показване на опциите
Show Original Path Показване на оригиналния път
Open this dialog with these settings next time Отваряне на този прозорец с тези настройки следващия път
Auto-Simplify Автоматично опростяване
Show Package Показване на пакет
Show Panel Показване на панел
Show Pattern Swatches Показване на образци за мостра
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1712,6 +1712,8 @@ lighting Light Světlo
Corner Point Angle Threshold: Prahový úhel rohových bodů:
Convert to Straight Lines Převést na přímé čáry
Show Original Path Zobrazit původní cestu
Open this dialog with these settings next time Příště otevřít toto dialogové okno s tímto nastavením
Auto-Simplify Automaticky zjednodušit
Transparency Options Možnosti průhlednosti
Color Mixer (Shift-click the chip) Míchání barev (Shift+kliknutí na políčko)
Fill: click for swatches, Shift-click for the color mixer Výplň: kliknutím otevřete vzorník, Shift+kliknutím míchání barev
@@ -4173,6 +4175,7 @@ imageTrace Outline Obrys
@msg background {other}: transparent, white, black or a colour such as "#ff8800" pozadí {other}: transparent, white, black nebo barva jako "#ff8800"
@msg bad color {v} chybná barva {v}
@msg the Color Picker is not open Výběr barvy není otevřený
@msg the Simplify slider is not open Posuvník Zjednodušit není otevřený
@msg the screen color sampler is already open nástroj pro nabírání barvy z obrazovky už je otevřený
@msg this system has no screen color sampler tento systém nemá nástroj pro nabírání barvy z obrazovky
@msg bad colour `{hex}` (#rrggbb) chybná barva `{hex}` (#rrggbb)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5052,6 +5052,8 @@ swatch Bright Violet Kraftig violet
Outline Object Objekt til kontur
Update Selection Opdater markering
Show Original Path Vis oprindelig kurve
Open this dialog with these settings next time Åbn denne dialogboks med disse indstillinger næste gang
Auto-Simplify Forenkl automatisk
@msg check a layer to export markér et lag, der skal eksporteres
@msg select the art to export first markér først grafikken, der skal eksporteres
@msg no saved selection recalled ingen gemt markering hentet
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2907,6 +2907,8 @@ mark-style Japanese Japanisch
Show Note Notiz einblenden
Show Options Optionen einblenden
Show Original Path Originalpfad einblenden
Open this dialog with these settings next time Dieses Dialogfeld nächstes Mal mit diesen Einstellungen öffnen
Auto-Simplify Automatisch vereinfachen
Color Panel Options Optionen für das Farbbedienfeld
Show Package Paket anzeigen
Show Panel Bedienfeld einblenden
@@ -3808,6 +3810,7 @@ theme Appearance Erscheinungsbild
@msg background {other}: transparent, white, black or a colour such as "#ff8800" background {other}: transparent, white, black oder eine Farbe wie "#ff8800"
@msg bad color {v} ungültige Farbe {v}
@msg the Color Picker is not open der Farbwähler ist nicht geöffnet
@msg the Simplify slider is not open der Regler „Vereinfachen“ ist nicht geöffnet
@msg the screen color sampler is already open die Farbaufnahme vom Bildschirm ist bereits geöffnet
@msg this system has no screen color sampler dieses System hat keine Farbaufnahme vom Bildschirm
@msg bad colour `{hex}` (#rrggbb) ungültige Farbe `{hex}` (#rrggbb)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3325,6 +3325,8 @@ theme Appearance Εμφάνιση
Show Note Εμφάνιση σημείωσης
Show Options Εμφάνιση επιλογών
Show Original Path Εμφάνιση αρχικής διαδρομής
Open this dialog with these settings next time Άνοιγμα αυτού του παραθύρου με αυτές τις ρυθμίσεις την επόμενη φορά
Auto-Simplify Αυτόματη απλοποίηση
Show Package Εμφάνιση συσκευασίας
Show Panel Εμφάνιση πίνακα
Show Pattern Swatches Εμφάνιση δειγμάτων μοτίβου
@@ -5113,6 +5115,7 @@ effect Points Σημεία
@msg span: "selection" places a linear or radial gradient, not a freeform one span: το "selection" τοποθετεί γραμμικό ή ακτινικό ντεγκραντέ, όχι ελεύθερης μορφής
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: το "selection" τοποθετεί το ίδιο το ντεγκραντέ: παραλείψτε τα `start` και `end` (το τοποθετούν ήδη σε συντεταγμένες εγγράφου)
@msg the Color Picker is not open ο επιλογέας χρώματος δεν είναι ανοιχτός
@msg the Simplify slider is not open ο ρυθμιστής Απλοποίησης δεν είναι ανοιχτός
@msg the action has no steps η ενέργεια δεν έχει βήματα
@msg the file's layers weren't read ({_1}): it comes in as its page, in one layer τα επίπεδα του αρχείου δεν διαβάστηκαν ({_1}): εισάγεται ως η σελίδα του, σε ένα επίπεδο
@msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part τα επίπεδα του αρχείου δεν διαβάστηκαν από τα δεδομένα επεξεργασίας του ({_1}): προέρχονται από το τμήμα PDF του
Can't render this file because it contains an unexpected character in line 2016 and column 19.
+3
View File
@@ -2925,6 +2925,8 @@
Show Note Mostrar nota
Show Options Mostrar opciones
Show Original Path Mostrar trazado original
Open this dialog with these settings next time Abrir este cuadro de diálogo con estos ajustes la próxima vez
Auto-Simplify Simplificar automáticamente
Color Panel Options Opciones del panel Color
Show Package Mostrar paquete
Show Panel Mostrar panel
@@ -3871,6 +3873,7 @@ axis Both Ambos
@msg background {other}: transparent, white, black or a colour such as "#ff8800" fondo {other}: transparent, white, black o un color como "#ff8800"
@msg bad color {v} color no válido {v}
@msg the Color Picker is not open el Selector de color no está abierto
@msg the Simplify slider is not open el regulador de Simplificar no está abierto
@msg the screen color sampler is already open el muestreador de color de la pantalla ya está abierto
@msg this system has no screen color sampler este sistema no tiene muestreador de color de la pantalla
@msg bad colour `{hex}` (#rrggbb) color no válido `{hex}` (#rrggbb)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1675,6 +1675,8 @@ lighting Light Valo
Corner Point Angle Threshold: Kulmapisteiden kulmakynnys:
Convert to Straight Lines Muunna suoriksi viivoiksi
Show Original Path Näytä alkuperäinen polku
Open this dialog with these settings next time Avaa tämä valintaikkuna näillä asetuksilla ensi kerralla
Auto-Simplify Yksinkertaista automaattisesti
Transparency Options Läpinäkyvyysasetukset
Color Mixer (Shift-click the chip) Värisekoitin (Vaihto-napsauta värilaattaa)
Fill: click for swatches, Shift-click for the color mixer Täyttö: napsauta avataksesi värimallit, Vaihto-napsauta avataksesi värisekoittimen
@@ -5125,6 +5127,7 @@ mark-style Japanese Japanilainen
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" sijoittaa lineaarisen tai säteittäisen liukuvärin, ei vapaamuotoista
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" sijoittaa itse liukuvärin: jätä pois `start` ja `end` (ne sijoittavat sen jo asiakirjan koordinaatteihin)
@msg the Color Picker is not open värinvalitsin ei ole auki
@msg the Simplify slider is not open Yksinkertaista-liukusäädin ei ole auki
@msg the action has no steps toiminnossa ei ole vaiheita
@msg the file's layers weren't read ({_1}): it comes in as its page, in one layer tiedoston tasoja ei luettu ({_1}): se tuodaan sivunaan yhdelle tasolle
@msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part tiedoston tasoja ei luettu sen muokkaustiedoista ({_1}): ne tulevat sen PDF-osasta
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2922,6 +2922,8 @@
Show Note Afficher la note
Show Options Afficher les options
Show Original Path Afficher le tracé d’origine
Open this dialog with these settings next time Ouvrir cette boîte de dialogue avec ces réglages la prochaine fois
Auto-Simplify Simplifier automatiquement
Color Panel Options Options du panneau Couleur
Show Package Afficher le dossier d’assemblage
Show Panel Afficher le panneau
@@ -3868,6 +3870,7 @@ axis Both Les deux
@msg background {other}: transparent, white, black or a colour such as "#ff8800" arrière-plan {other} : transparent, white, black ou une couleur comme "#ff8800"
@msg bad color {v} couleur non valide {v}
@msg the Color Picker is not open le Sélecteur de couleurs n’est pas ouvert
@msg the Simplify slider is not open le curseur Simplifier n’est pas ouvert
@msg the screen color sampler is already open l’outil de prélèvement de couleur à l’écran est déjà ouvert
@msg this system has no screen color sampler ce système n’a pas d’outil de prélèvement de couleur à l’écran
@msg bad colour `{hex}` (#rrggbb) couleur non valide `{hex}` (#rrggbb)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3543,6 +3543,8 @@ mark-style Japanese Japán
Show Note Megjegyzés megjelenítése
Show Options Beállítások megjelenítése
Show Original Path Eredeti útvonal megjelenítése
Open this dialog with these settings next time Legközelebb ez a párbeszédablak nyíljon meg ezekkel a beállításokkal
Auto-Simplify Automatikus egyszerűsítés
Color Panel Options Szín panel beállításai
Show Package Csomag megjelenítése
Show Panel Panel megjelenítése
@@ -4414,6 +4416,7 @@ imageTrace Outline Körvonal
@msg background {other}: transparent, white, black or a colour such as "#ff8800" background {other}: transparent, white, black vagy egy szín, például "#ff8800"
@msg bad color {v} hibás szín: {v}
@msg the Color Picker is not open a Színválasztó nincs megnyitva
@msg the Simplify slider is not open az Egyszerűsítés csúszka nincs megnyitva
@msg the screen color sampler is already open a képernyős színmintavevő már nyitva van
@msg this system has no screen color sampler ezen a rendszeren nincs képernyős színmintavevő
@msg bad colour `{hex}` (#rrggbb) hibás szín: `{hex}` (#rrggbb)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5052,6 +5052,8 @@ swatch Bright Violet Violet cerah
Outline Object Kerangka objek
Update Selection Perbarui seleksi
Show Original Path Tampilkan jalur asli
Open this dialog with these settings next time Buka dialog ini dengan pengaturan ini lain kali
Auto-Simplify Sederhanakan otomatis
@msg check a layer to export centang lapisan yang akan diekspor
@msg select the art to export first pilih dulu karya yang akan diekspor
@msg no saved selection recalled tidak ada seleksi tersimpan yang dipanggil
Can't render this file because it contains an unexpected character in line 3771 and column 19.
+3
View File
@@ -2958,6 +2958,8 @@ mark-style Japanese Giapponese
Show Note Mostra nota
Show Options Mostra opzioni
Show Original Path Mostra tracciato originale
Open this dialog with these settings next time Apri questa finestra di dialogo con queste impostazioni la prossima volta
Auto-Simplify Semplifica automaticamente
Color Panel Options Opzioni pannello Colore
Show Package Mostra pacchetto
Show Panel Mostra pannello
@@ -3910,6 +3912,7 @@ axis Both Entrambi
@msg background {other}: transparent, white, black or a colour such as "#ff8800" sfondo {other}: transparent, white, black o un colore come "#ff8800"
@msg bad color {v} colore non valido {v}
@msg the Color Picker is not open il Selettore colore non è aperto
@msg the Simplify slider is not open il cursore Semplifica non è aperto
@msg the screen color sampler is already open il campionatore di colore dello schermo è già aperto
@msg this system has no screen color sampler questo sistema non ha un campionatore di colore dello schermo
@msg bad colour `{hex}` (#rrggbb) colore non valido `{hex}` (#rrggbb)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2766,6 +2766,8 @@ mark-style Japanese 日本式
Show Note メモを表示
Show Options オプションを表示
Show Original Path 元のパスを表示
Open this dialog with these settings next time 次回はこの設定でこのダイアログを開く
Auto-Simplify 自動単純化
Color Panel Options カラーパネルオプション
Show Package パッケージを表示
Show Panel パネルを表示
@@ -4225,6 +4227,7 @@ swatch Bright Violet ビビッドバイオレット
@msg no blend options ブレンドオプションがありません
@msg bad color {v} カラー {v} が正しくありません
@msg the Color Picker is not open カラーピッカーが開いていません
@msg the Simplify slider is not open 単純化スライダーが開いていません
@msg the screen color sampler is already open 画面のカラーサンプラーはすでに開いています
@msg this system has no screen color sampler このシステムには画面のカラーサンプラーがありません
@msg Corners edits the corners of a path 「コーナー」で編集できるのはパスの角だけです
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -4756,6 +4756,7 @@ imageTrace Outline კონტური
@msg the file's layers weren't read ({_1}): it comes in as its page, in one layer ფაილის შრეები ვერ წაიკითხა ({_1}): ის გაიხსნება როგორც მისი გვერდი, ერთ შრეში
@msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part ფაილის შრეები რედაქტირების მონაცემებიდან ვერ წაიკითხა ({_1}): ისინი მისი PDF ნაწილიდან აიღება
@msg the Color Picker is not open ფერის ამრჩევი გახსნილი არ არის
@msg the Simplify slider is not open გამარტივების სლაიდერი გახსნილი არ არის
@msg the screen color sampler is already open ეკრანიდან ფერის აღება უკვე გახსნილია
@msg this system has no screen color sampler ამ სისტემას ეკრანიდან ფერის აღების საშუალება არ აქვს
# Dimension tool and Object › Dimensions. A drafting dimension is ზომა (ზომის დასმა), like a dimension on a technical drawing.
@@ -4899,6 +4900,8 @@ imageTrace Outline კონტური
Rotate View to Selection ხედის შემობრუნება მონიშნულის მიხედვით
Update Selection მონიშნულის განახლება
Show Original Path საწყისი ტრაექტორიის ჩვენება
Open this dialog with these settings next time შემდეგ ჯერზე ამ დიალოგის ამ პარამეტრებით გახსნა
Auto-Simplify ავტომატური გამარტივება
Texture Image ტექსტურის გამოსახულება
@msg check a layer to export მონიშნეთ საექსპორტო შრე
@msg select the art to export first ჯერ მონიშნეთ საექსპორტო ნახატი
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2760,6 +2760,8 @@ mark-style Japanese 일본식
Show Note 메모 표시
Show Options 옵션 표시
Show Original Path 원래 패스 표시
Open this dialog with these settings next time 다음에 이 설정으로 이 대화상자 열기
Auto-Simplify 자동 단순화
Color Panel Options 색상 패널 옵션
Show Package 패키지 표시
Show Panel 패널 표시
@@ -4157,6 +4159,7 @@ theme Appearance 외관
@msg no blend options 혼합 옵션이 없습니다
@msg bad color {v} 잘못된 색상 {v}
@msg the Color Picker is not open 색상 피커가 열려 있지 않습니다
@msg the Simplify slider is not open 단순화 슬라이더가 열려 있지 않습니다
@msg the screen color sampler is already open 화면 색상 추출기가 이미 열려 있습니다
@msg this system has no screen color sampler 이 시스템에는 화면 색상 추출기가 없습니다
@msg Corners edits the corners of a path 모퉁이는 패스의 모퉁이를 편집합니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5064,6 +5064,8 @@ swatch Bright Violet Felviolet
Outline Object Object omtrekken
Update Selection Selectie bijwerken
Show Original Path Oorspronkelijk pad tonen
Open this dialog with these settings next time Dit dialoogvenster de volgende keer met deze instellingen openen
Auto-Simplify Automatisch vereenvoudigen
@msg check a layer to export vink een laag aan om te exporteren
@msg select the art to export first selecteer eerst de illustratie die je wilt exporteren
@msg no saved selection recalled geen opgeslagen selectie opgeroepen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1632,6 +1632,8 @@ lighting Light Światło
Corner Point Angle Threshold: Próg kąta punktów narożnych:
Convert to Straight Lines Konwertuj na linie proste
Show Original Path Pokaż oryginalną ścieżkę
Open this dialog with these settings next time Następnym razem otwórz to okno dialogowe z tymi ustawieniami
Auto-Simplify Uprość automatycznie
Transparency Options Opcje przezroczystości
Color Mixer (Shift-click the chip) Mikser kolorów (kliknij próbkę z Shift)
Fill: click for swatches, Shift-click for the color mixer Wypełnienie: kliknij, aby wybrać próbkę, z Shift — mikser kolorów
@@ -3995,6 +3997,7 @@ imageTrace Outline Kontur
@msg background {other}: transparent, white, black or a colour such as "#ff8800" tło {other}: transparent, white, black lub kolor, np. "#ff8800"
@msg bad color {v} nieprawidłowy kolor {v}
@msg the Color Picker is not open Próbnik kolorów nie jest otwarty
@msg the Simplify slider is not open Suwak Uprość nie jest otwarty
@msg the screen color sampler is already open narzędzie do pobierania koloru z ekranu jest już otwarte
@msg this system has no screen color sampler ten system nie ma narzędzia do pobierania koloru z ekranu
@msg bad colour `{hex}` (#rrggbb) nieprawidłowy kolor `{hex}` (#rrggbb)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2430,6 +2430,8 @@
Show Note Mostrar Nota
Show Options Mostrar Opções
Show Original Path Mostrar Caminho Original
Open this dialog with these settings next time Abrir este diálogo com estas configurações na próxima vez
Auto-Simplify Simplificar Automaticamente
Show Package Mostrar Pacote
Show Panel Mostrar Painel
Show Perspective Grid Mostrar Grade de Perspectiva
@@ -5068,6 +5070,7 @@ mark-style Japanese Japonesas
@msg Unknown dimension mode Modo de cota desconhecido
@msg Unknown dimension option {k} Opção de cota desconhecida {k}
@msg the Color Picker is not open o Seletor de Cores não está aberto
@msg the Simplify slider is not open o controle deslizante Simplificar não está aberto
@msg the screen color sampler is already open o coletor de cores da tela já está aberto
@msg this system has no screen color sampler este sistema não tem coletor de cores da tela
Darkness Escuridão
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -2954,6 +2954,8 @@ mark-style Japanese Японский
Show Note Показать примечание
Show Options Показать параметры
Show Original Path Показать исходный контур
Open this dialog with these settings next time Открывать это диалоговое окно с этими параметрами в следующий раз
Auto-Simplify Автоматическое упрощение
Color Panel Options Параметры панели «Цвет»
Show Package Показать пакет
Show Panel Показать панель
@@ -3906,6 +3908,7 @@ axis Both Оба
@msg background {other}: transparent, white, black or a colour such as "#ff8800" фон {other}: transparent, white, black или цвет, например "#ff8800"
@msg bad color {v} недопустимый цвет {v}
@msg the Color Picker is not open палитра цветов не открыта
@msg the Simplify slider is not open ползунок «Упростить» не открыт
@msg the screen color sampler is already open средство выбора цвета с экрана уже открыто
@msg this system has no screen color sampler в этой системе нет средства выбора цвета с экрана
@msg bad colour `{hex}` (#rrggbb) недопустимый цвет `{hex}` (#rrggbb)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2868,6 +2868,8 @@
Show Note Visa anteckning
Show Options Visa alternativ
Show Original Path Visa ursprunglig bana
Open this dialog with these settings next time Öppna den här dialogrutan med de här inställningarna nästa gång
Auto-Simplify Förenkla automatiskt
Show Package Visa paket
Show Panel Visa panel
Show Pattern Swatches Visa mönsterprover
@@ -5117,6 +5119,7 @@ theme Appearance Utseende
@msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" placerar en linjär eller radiell övertoning, inte en fri
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" placerar själva övertoningen: utelämna `start` och `end` (de placerar den redan i dokumentkoordinater)
@msg the Color Picker is not open färgväljaren är inte öppen
@msg the Simplify slider is not open Förenkla-reglaget är inte öppet
@msg the action has no steps åtgärden har inga steg
@msg the file's layers weren't read ({_1}): it comes in as its page, in one layer filens lager lästes inte ({_1}): den hämtas in som en sida, i ett enda lager
@msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part filens lager lästes inte från dess redigeringsdata ({_1}): de kommer från dess PDF-del
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -2950,6 +2950,8 @@ mark-style Japanese Японський
Show Note Показати примітку
Show Options Показати параметри
Show Original Path Показати початковий контур
Open this dialog with these settings next time Наступного разу відкривати це діалогове вікно з цими параметрами
Auto-Simplify Автоматичне спрощення
Color Panel Options Параметри панелі «Колір»
Show Package Показати пакет
Show Panel Показати панель
@@ -3894,6 +3896,7 @@ axis Both Обидві
@msg background {other}: transparent, white, black or a colour such as "#ff8800" фон {other}: transparent, white, black або колір, наприклад "#ff8800"
@msg bad color {v} неприпустимий колір {v}
@msg the Color Picker is not open палітра кольорів не відкрита
@msg the Simplify slider is not open повзунок «Спростити» не відкрито
@msg the screen color sampler is already open засіб вибору кольору з екрана вже відкритий
@msg this system has no screen color sampler у цій системі немає засобу вибору кольору з екрана
@msg bad colour `{hex}` (#rrggbb) неприпустимий колір `{hex}` (#rrggbb)
Can't render this file because it contains an unexpected character in line 725 and column 40.
+3
View File
@@ -3283,6 +3283,8 @@ mark-style Japanese Kiểu Nhật
Show Note Hiện ghi chú
Show Options Hiện tùy chọn
Show Original Path Hiện đường dẫn gốc
Open this dialog with these settings next time Lần sau mở hộp thoại này với các cài đặt này
Auto-Simplify Tự động đơn giản hóa
Color Panel Options Tùy chọn bảng màu
Show Package Hiện gói
Show Panel Hiện bảng
@@ -4062,6 +4064,7 @@ theme Appearance Giao diện
@msg background {other}: transparent, white, black or a colour such as "#ff8800" background {other}: transparent, white, black hoặc màu như "#ff8800"
@msg bad color {v} màu không hợp lệ {v}
@msg the Color Picker is not open Bộ chọn màu chưa được mở
@msg the Simplify slider is not open Thanh trượt Đơn giản hóa chưa được mở
@msg the screen color sampler is already open công cụ lấy mẫu màu trên màn hình đã được mở
@msg this system has no screen color sampler hệ thống này không có công cụ lấy mẫu màu trên màn hình
@msg bad colour `{hex}` (#rrggbb) màu không hợp lệ `{hex}` (#rrggbb)
Can't render this file because it contains an unexpected character in line 3961 and column 10.
+3
View File
@@ -2809,6 +2809,8 @@ font-script Japanese 汉字
Show Note 显示注解
Show Options 显示选项
Show Original Path 显示原始路径
Open this dialog with these settings next time 下次以这些设置打开此对话框
Auto-Simplify 自动简化
Color Panel Options 颜色面板选项
Show Package 显示打包
Show Panel 显示面板
@@ -3971,6 +3973,7 @@ imageTrace Outline 轮廓
@msg no blend options 没有混合选项
@msg bad color {v} 颜色 {v} 无效
@msg the Color Picker is not open 拾色器未打开
@msg the Simplify slider is not open 简化滑块未打开
@msg the screen color sampler is already open 屏幕取色器已打开
@msg this system has no screen color sampler 此系统没有屏幕取色器
@msg Corners edits the corners of a path 转角用于编辑路径的转角
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1979,6 +1979,7 @@ file-filter Templates 範本
@msg no blend options 沒有漸變選項
@msg bad color {v} 顏色 {v} 無效
@msg the Color Picker is not open 「顏色選擇器」未開啟
@msg the Simplify slider is not open 「簡化」滑桿未開啟
@msg the screen color sampler is already open 螢幕取色器已開啟
@msg this system has no screen color sampler 此系統沒有螢幕取色器
@msg Corners edits the corners of a path 「轉角」用於編輯路徑的轉角
@@ -3676,6 +3677,8 @@ font-script Japanese 漢字
Show Note 顯示註解
Show Options 顯示選項
Show Original Path 顯示原始路徑
Open this dialog with these settings next time 下次以這些設定開啟此對話方塊
Auto-Simplify 自動簡化
Color Panel Options 顏色面板選項
Show Package 顯示封裝
Show Panel 顯示面板
Can't render this file because it contains an unexpected character in line 6 and column 47.
+19 -4
View File
@@ -666,6 +666,18 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{id?, corners?: [i…]} open Corners for path `id` (default: the selected one) and its corners (anchor indices of the path with its corners uncut, as object.setLiveShape takes them: a rectangle's 0 top-left, 1 top-right, 2 bottom-right, 3 bottom-left; default: the Direct-Selected corners, else every corner) (dialog `corners`: kind (round|invertedRound|chamfer), radius (pt); double-clicking a corner widget opens it too): OK runs object.setLiveShape",
),
(
"ui.simplifyAuto",
"Auto-Simplify",
"",
"{} the open Simplify slider (Object › Path › Simplify opens it: dialog `simplifySlider` with field `level` 0..100, object.path.simplify's level, 50 as it opens, previewed on the canvas until ui.dialog.confirm keeps it as one undo step; after OK in the Simplify dialog with `retain` ticked, the menu item opens the dialog `simplify` with the fields of that OK instead) moves to the level object.path.simplify {auto: true} picks for the selection and previews it → {level}; an error when the slider isn't open",
),
(
"ui.simplifyMoreOptions",
"More Options",
"",
"{} the Simplify dialog (dialog `simplify`: tolerance (pt), angleThreshold, straightLines, showOriginal, retain: ticked, OK makes Object › Path › Simplify open this dialog with these fields next time) takes the open Simplify slider's place, at the slider's tolerance → {tolerance}; an error when the slider isn't open",
),
(
"ui.justification",
"Justification…",
@@ -1583,6 +1595,8 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
None => Err("no such user library".into()),
},
"ui.expandDialog" => crate::dialogs::expand::open(app),
"ui.simplifyAuto" => crate::dialogs::simplify_slider::auto(app),
"ui.simplifyMoreOptions" => crate::dialogs::simplify_slider::more_options(app),
"attributes.openUrl" => crate::panels::attributes::open_url(app, p),
"ui.javascriptFilesDialog" => crate::panels::svg_interactivity::files_dialog(app, p),
"ui.spotColors" => crate::dialogs::spot_colors::open(app),
@@ -2292,6 +2306,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
"ui.expandDialog" => app.session.active().is_some_and(|d| !d.selection.is_empty()),
"ui.spotColors" => app.session.active().is_some(),
"ui.menuDialog" => app.session.active().is_some_and(|d| !d.selection.is_empty()),
"ui.simplifyAuto" | "ui.simplifyMoreOptions" => crate::dialogs::simplify_slider::showing(app),
"ui.dxfOptionsDialog" => app.session.active().is_some(),
"links.editOriginal" | "links.reveal" => selected_image(app, |im| im.link.is_some()) || selected_placed(app),
"ui.placementOptionsDialog" => selected_image(app, |_| true) || selected_placed(app),
@@ -3369,10 +3384,10 @@ fn menu_dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static str, Value)>
// Offset Path opens on the values it was last confirmed with.
"object.path.offsetPath" if app.ui.offset_path.is_object() => ("offsetPath", app.ui.offset_path.clone()),
"object.path.offsetPath" => ("offsetPath", json!({"offset": "10 pt", "joins": "miter", "miterLimit": 4})),
"object.path.simplify" => (
"simplify",
json!({"tolerance": "1 pt", "angleThreshold": crate::dialogs::path_ops::ANGLE_THRESHOLD, "straightLines": false, "showOriginal": false, "preview": true}),
),
// Simplify opens its slider, or the dialog on the fields it was last confirmed with when
// "Open this dialog with these settings next time" was ticked.
"object.path.simplify" if app.ui.simplify.is_object() => ("simplify", app.ui.simplify.clone()),
"object.path.simplify" => (crate::dialogs::simplify_slider::KIND, crate::dialogs::simplify_slider::fields()),
"object.path.splitIntoGrid" => {
// The grid starts at the size of the selection.
let size =
+6
View File
@@ -488,6 +488,11 @@ pub struct UiState {
/// dialog opens on them (#1152).
#[serde(default)]
pub offset_path: Value,
/// The Simplify dialog's fields confirmed last with "Open this dialog with these settings next
/// time" ticked (null: not ticked): Object › Path › Simplify opens the dialog on them instead
/// of the Simplify slider.
#[serde(default)]
pub simplify: Value,
/// The Move dialog's checkboxes as it was last confirmed with (copy, objects, preview; null:
/// never used): it opens with them.
#[serde(default)]
@@ -823,6 +828,7 @@ impl Default for UiState {
dxf_options: Value::Null,
eps_options: Value::Null,
offset_path: Value::Null,
simplify: Value::Null,
move_options: Value::Null,
dxf_import: Value::Null,
home: None,
+17
View File
@@ -2024,6 +2024,23 @@ the path it had begun while that stays selected. Its reply's `tool` is the tool
{"name":"run_command","arguments":{"command":"path.moveAnchors","params":{"dx":0,"dy":40}}}
```
## Simplify
`object.path.simplify` takes the amount as `tolerance` (points, 1 by default) or as the Simplify slider's `level`, a whole
number from 0 (the fewest anchor points) to 100 (the closest fit). Level 50 is a tolerance of 1 pt, and every 10 levels
halve or double it, so level 0 is 32 pt and level 100 is 1/32 pt. `auto: true` (Auto-Simplify) uses the lowest level
whose tolerance is at most 1/400 of the selection's longer side, or level 100 when that side is under 12.5 pt. With
`level` or `auto`, the reply also has the `level` used:
```json
{"name":"run_command","arguments":{"command":"object.path.simplify","params":{"auto":true}}}
```
In the desktop app, Object › Path › Simplify opens the slider (`ui.menuDialog {command: "object.path.simplify"}`,
dialog `simplifySlider` with field `level`) and previews the result until `ui.dialog.confirm`. `ui.simplifyAuto` moves
the open slider to the Auto-Simplify level, and `ui.simplifyMoreOptions` opens the Simplify dialog at the slider's
tolerance. When OK confirms the dialog with `retain` ticked, the menu item opens the dialog on those settings next time.
## Editing a path with the Curvature tool
The Curvature tool edits any selected path, whatever drew it, keeping its shape except where edited (#798). Each edit
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -65,7 +65,7 @@ tools, never checked side by side) isn't captured row by row.
| Join tool scrubbing, Average, Join | done | Join merges the direct-selected anchors lying on one another along a path, as after Average, into one (#1533) |
| Average and Join in one step, with a corner or smooth choice for the joined anchor | partial | `path.join` accepts `to: corner|smooth` on the merged anchor; Average and Join are still separate commands; [gap 1](gaps.md) |
| Smooth tool smooths only the stretch of path under the drag | done | #1512 |
| Simplify (slider, auto, corner angle threshold) | partial | Tolerance, default-on Preview and original/new point counts (#1272); Corner Point Angle Threshold, Convert to Straight Lines and Show Original Path; slider, auto-simplify and Retain my latest settings remain missing |
| Simplify (slider, auto, corner angle threshold) | partial | Tolerance, default-on Preview and original/new point counts (#1272); Corner Point Angle Threshold, Convert to Straight Lines and Show Original Path; the slider under the selection with Auto-Simplify and More Options, and the dialog's option to open it directly with its last settings; the slider opens at 1 pt rather than at the Auto-Simplify level, and simplifying only a Direct Selection part of a path remains missing |
## Direct Selection, anchors and handles