mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge main into feat/ai-save-nested-envelope-editable
This commit is contained in:
Generated
+1
@@ -6930,6 +6930,7 @@ name = "vectorcraft-ui-egui"
|
||||
version = "0.24.1"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"chrono",
|
||||
"craft-keymap",
|
||||
"craft-ui",
|
||||
"craft-update",
|
||||
|
||||
@@ -432,6 +432,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
),
|
||||
("view.nextArtboard", "Next Artboard", "", "{} the navigator's Next Artboard button: view.goToArtboard {index: \"next\"} → {index}"),
|
||||
("view.lastArtboard", "Last Artboard", "", "{} the navigator's Last Artboard button: view.goToArtboard {index: \"last\"} → {index}"),
|
||||
(
|
||||
"view.statusBarShow",
|
||||
"Status Bar Show",
|
||||
"",
|
||||
"{show: \"artboardName\" | \"currentTool\" | \"dateTime\" | \"undos\" | \"colorProfile\"} the status bar's Show menu (a click on its info field): the field shows the name of the artboard the navigator shows, the tool in use (the default), the date and time (local on the desktop, UTC on the web), the active document's undo and redo step counts, or the profile its colors use (the one assigned for its color mode, else, or when that one is not available, that mode's working space); kept with the UI state; any other value is an error → show",
|
||||
),
|
||||
("view.edges", "Hide Edges", "Cmd+H", "{}"),
|
||||
("view.artboards", "Hide Artboards", "Cmd+Shift+H", "{}"),
|
||||
(
|
||||
|
||||
@@ -84,7 +84,7 @@ fn fills(paths: &[TracedPath]) -> Vec<TracedPath> {
|
||||
continue;
|
||||
}
|
||||
if sp.closed && marks.iter().all(|&m| m) {
|
||||
runs.push(SubPath::new(sp.anchors.clone(), true));
|
||||
runs.push(sp.clone());
|
||||
continue;
|
||||
}
|
||||
// Start after an unmarked segment, so that a run across the subpath's first anchor
|
||||
|
||||
@@ -54,6 +54,9 @@ log = { workspace = true }
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
craft-ui = { workspace = true, features = ["native-windows"] }
|
||||
craft-update = { workspace = true }
|
||||
# The local date and time the status bar's Date and Time readout shows (`chrono::Local`). Already in
|
||||
# Cargo.lock with the time-zone crates `clock` uses.
|
||||
chrono = { workspace = true, features = ["clock"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# `now_ms` (src/lib.rs) reads the page's `performance.now()` with this crate in the web build. It is
|
||||
|
||||
@@ -28,9 +28,9 @@ pub(crate) use control_bar::{Fit, group_separator, left_out, link_reserve};
|
||||
#[cfg(test)]
|
||||
use hints::hint_segments;
|
||||
pub use hints::{hint_bar, hint_strings};
|
||||
pub use status_bar::status_bar;
|
||||
pub use status_bar::{StatusBarShow, deserialize_status_bar_show, set_status_bar_show, status_bar};
|
||||
#[cfg(test)]
|
||||
use status_bar::{StatusReadoutWidths, status_readouts};
|
||||
use status_bar::{StatusReadoutWidths, color_profile_name, date_time_label, status_readouts};
|
||||
#[cfg(test)]
|
||||
use tabs::{CLOSE_BEFORE_TITLE, first_tab_shown, tab_drop, tab_groups, tab_title};
|
||||
pub use tabs::{doc_tabs, isolation_bar, step_document};
|
||||
|
||||
@@ -73,19 +73,37 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
app.status(e);
|
||||
}
|
||||
ui.separator();
|
||||
let tool = vectorcraft_tools::tool_info(app.session.tool_id())
|
||||
.map(|t| {
|
||||
// The catalog keys carry the full label ("Selection Tool"); only English drops the suffix.
|
||||
let shown = tl!(t.label);
|
||||
if crate::i18n::current() == crate::i18n::Lang::EN { shown.trim_end_matches(" Tool") } else { shown }
|
||||
})
|
||||
.unwrap_or("");
|
||||
// The tool name leaves the status message its least width and ends in an ellipsis first.
|
||||
let (info, next) = info_text(app);
|
||||
if let Some(secs) = next {
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_secs(secs));
|
||||
}
|
||||
// The info field leaves the status message its least width and ends in an ellipsis first.
|
||||
// An empty readout shows a dash, so the field can still be clicked.
|
||||
let room = (ui.available_width() - MESSAGE_MIN - ui.spacing().item_spacing.x).max(0.0);
|
||||
ui.scope(|ui| {
|
||||
ui.set_max_width(room);
|
||||
clipped_label(ui, egui::RichText::new(tool).size(11.5).color(t.text_dim));
|
||||
let shown = if info.is_empty() { "–" } else { info.as_str() };
|
||||
let field = ui
|
||||
.scope(|ui| {
|
||||
ui.set_max_width(room);
|
||||
clipped_label(ui, egui::RichText::new(shown).size(11.5).color(t.text_dim))
|
||||
})
|
||||
.inner
|
||||
.interact(Sense::click());
|
||||
let mut pick = None;
|
||||
egui::Popup::menu(&field).show(|ui| {
|
||||
ui.menu_button(tl!("Show"), |ui| {
|
||||
for show in StatusBarShow::ALL {
|
||||
if widgets::menu_item_name(ui, show.label(), true, app.ui.status_bar_show == show) {
|
||||
pick = Some(show);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if let Some(show) = pick
|
||||
&& let Err(e) = app.run("view.statusBarShow", json!({ "show": show.param() }))
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
status_readouts(app, ui, &t);
|
||||
});
|
||||
});
|
||||
@@ -95,6 +113,152 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
/// The least width the status message keeps when the window is narrow.
|
||||
const MESSAGE_MIN: f32 = 180.0;
|
||||
|
||||
/// What the status bar's info field shows, chosen from the Show menu that a click on the field
|
||||
/// opens (`view.statusBarShow`). Kept with the UI state ([`crate::UiState::status_bar_show`]).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum StatusBarShow {
|
||||
/// The name of the artboard the navigator shows.
|
||||
ArtboardName,
|
||||
/// The name of the tool in use.
|
||||
#[default]
|
||||
CurrentTool,
|
||||
/// The date and time.
|
||||
DateTime,
|
||||
/// How many steps the active document can undo and redo.
|
||||
Undos,
|
||||
/// The profile the active document's colors use.
|
||||
ColorProfile,
|
||||
}
|
||||
|
||||
impl StatusBarShow {
|
||||
/// Every choice, in the Show menu's order.
|
||||
pub const ALL: [StatusBarShow; 5] =
|
||||
[StatusBarShow::ArtboardName, StatusBarShow::CurrentTool, StatusBarShow::DateTime, StatusBarShow::Undos, StatusBarShow::ColorProfile];
|
||||
|
||||
/// The `show` param of `view.statusBarShow`.
|
||||
pub fn param(self) -> &'static str {
|
||||
match self {
|
||||
StatusBarShow::ArtboardName => "artboardName",
|
||||
StatusBarShow::CurrentTool => "currentTool",
|
||||
StatusBarShow::DateTime => "dateTime",
|
||||
StatusBarShow::Undos => "undos",
|
||||
StatusBarShow::ColorProfile => "colorProfile",
|
||||
}
|
||||
}
|
||||
|
||||
/// The choice whose param is `param`.
|
||||
pub fn from_param(param: &str) -> Option<StatusBarShow> {
|
||||
StatusBarShow::ALL.into_iter().find(|s| s.param() == param)
|
||||
}
|
||||
|
||||
/// The Show menu's label, in the interface language.
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
StatusBarShow::ArtboardName => tl!("Artboard Name"),
|
||||
StatusBarShow::CurrentTool => tl!("Current Tool"),
|
||||
StatusBarShow::DateTime => tl!("Date and Time"),
|
||||
StatusBarShow::Undos => tl!("Number of Undos"),
|
||||
StatusBarShow::ColorProfile => tl!("Document Color Profile"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serde reader for the saved choice ([`crate::UiState::status_bar_show`]): a value this version
|
||||
/// does not know reads as Current Tool.
|
||||
pub fn deserialize_status_bar_show<'de, D: serde::Deserializer<'de>>(d: D) -> Result<StatusBarShow, D::Error> {
|
||||
let v = <serde_json::Value as serde::Deserialize>::deserialize(d)?;
|
||||
Ok(v.as_str().and_then(StatusBarShow::from_param).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// `view.statusBarShow {show}`: the info field shows that readout from now on → the choice.
|
||||
pub fn set_status_bar_show(app: &mut VectorcraftApp, p: &serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let key = "show";
|
||||
let Some(show) = p.get(key).and_then(serde_json::Value::as_str).and_then(StatusBarShow::from_param) else {
|
||||
let all: Vec<&str> = StatusBarShow::ALL.iter().map(|s| s.param()).collect();
|
||||
return Err(format!("`{key}` must be one of {}", all.join(", ")));
|
||||
};
|
||||
app.ui.status_bar_show = show;
|
||||
Ok(json!(show.param()))
|
||||
}
|
||||
|
||||
/// The info field's text for the choice in the Show menu, and for Date and Time the seconds until
|
||||
/// the minute it shows ends (when the bar draws again).
|
||||
pub(super) fn info_text(app: &VectorcraftApp) -> (String, Option<u64>) {
|
||||
let st = app.session.active();
|
||||
match app.ui.status_bar_show {
|
||||
StatusBarShow::CurrentTool => {
|
||||
let tool = vectorcraft_tools::tool_info(app.session.tool_id())
|
||||
.map(|t| {
|
||||
// The catalog keys carry the full label ("Selection Tool"); only English drops the suffix.
|
||||
let shown = tl!(t.label);
|
||||
if crate::i18n::current() == crate::i18n::Lang::EN { shown.trim_end_matches(" Tool") } else { shown }
|
||||
})
|
||||
.unwrap_or("");
|
||||
(tool.to_string(), None)
|
||||
}
|
||||
StatusBarShow::ArtboardName => {
|
||||
let name = st.and_then(|d| {
|
||||
let n = d.doc.artboards.len();
|
||||
d.doc.artboards.get(crate::panels::artboards::selected(app, n)).map(|a| a.name.clone())
|
||||
});
|
||||
(name.unwrap_or_default(), None)
|
||||
}
|
||||
StatusBarShow::DateTime => {
|
||||
let (text, next) = date_time_now();
|
||||
(text, Some(next))
|
||||
}
|
||||
StatusBarShow::Undos => {
|
||||
let text = st.map(|d| format!("{}: {} {}: {}", tl!("Undos"), d.history.undo.len(), tl!("Redos"), d.history.redo.len()));
|
||||
(text.unwrap_or_default(), None)
|
||||
}
|
||||
StatusBarShow::ColorProfile => (st.map(|d| color_profile_name(&d.doc)).unwrap_or_default(), None),
|
||||
}
|
||||
}
|
||||
|
||||
/// The profile `doc`'s colors use for its color mode, as the context it draws through has it
|
||||
/// ([`vectorcraft_engine::cmd::colormgmt::doc_cms`]): the one assigned (Edit › Assign Profile), else
|
||||
/// the working space of that mode (Edit › Color Settings), also when the assigned one is not
|
||||
/// available.
|
||||
pub(super) fn color_profile_name(doc: &vectorcraft_doc::Document) -> String {
|
||||
let cms = vectorcraft_engine::cmd::colormgmt::doc_cms(doc);
|
||||
let settings = cms.settings();
|
||||
let name = match doc.color_mode {
|
||||
vectorcraft_doc::ColorMode::Rgb => &settings.rgb,
|
||||
vectorcraft_doc::ColorMode::Cmyk => &settings.cmyk,
|
||||
};
|
||||
vectorcraft_color::cms::canonical_name(name).to_string()
|
||||
}
|
||||
|
||||
/// `[year, month, day, hour, minute, second]` as the info field shows it: `2026-11-15 14:03`.
|
||||
pub(super) fn date_time_label([y, mo, d, h, mi, _]: [i64; 6]) -> String {
|
||||
format!("{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}")
|
||||
}
|
||||
|
||||
/// The local date and time ([`date_time_label`]) and the seconds until the minute ends.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn date_time_now() -> (String, u64) {
|
||||
use chrono::{Datelike, Timelike};
|
||||
let now = chrono::Local::now();
|
||||
let parts = [
|
||||
i64::from(now.year()),
|
||||
i64::from(now.month()),
|
||||
i64::from(now.day()),
|
||||
i64::from(now.hour()),
|
||||
i64::from(now.minute()),
|
||||
i64::from(now.second()),
|
||||
];
|
||||
(date_time_label(parts), 60 - u64::from(now.second().min(59)))
|
||||
}
|
||||
|
||||
/// The web build reads no time zone, so it shows the date and time in UTC.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn date_time_now() -> (String, u64) {
|
||||
let secs = web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH).map_or(0, |d| d.as_secs());
|
||||
let parts = vectorcraft_doc::metadata::civil(i64::try_from(secs).unwrap_or(i64::MAX));
|
||||
(format!("{} UTC", date_time_label(parts)), 60 - u64::try_from(parts[5].clamp(0, 59)).unwrap_or(0))
|
||||
}
|
||||
|
||||
/// A label that ends in an ellipsis when it is wider than the room left in `ui`. Only a cut label
|
||||
/// shows its full text in a tooltip; screen readers always receive the full text.
|
||||
pub(super) fn clipped_label(ui: &mut Ui, text: impl Into<egui::WidgetText>) -> egui::Response {
|
||||
|
||||
@@ -576,6 +576,155 @@ fn a_clipped_tool_name_shows_in_full_on_hover() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The texts of one status bar frame after `events`, `dt` seconds after the last one.
|
||||
fn status_after(app: &mut crate::VectorcraftApp, ctx: &egui::Context, dt: f64, events: Vec<egui::Event>) -> Vec<StatusText> {
|
||||
let t = ctx.input(|i| i.time) + dt;
|
||||
bar_frame(app, ctx, 1400.0, Some(t), events, super::status_bar).0
|
||||
}
|
||||
|
||||
/// A press and release of the primary button at `pos`.
|
||||
fn click_at(pos: egui::Pos2) -> Vec<egui::Event> {
|
||||
let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
vec![egui::Event::PointerMoved(pos), button(true), button(false)]
|
||||
}
|
||||
|
||||
/// The center of the first text that is `text`.
|
||||
fn text_center(texts: &[StatusText], text: &str) -> egui::Pos2 {
|
||||
let at = texts.iter().find(|t| t.text == text).unwrap_or_else(|| panic!("no {text:?} in {texts:?}"));
|
||||
at.clip.intersect(at.bounds).center()
|
||||
}
|
||||
|
||||
/// The status bar's info field shows the readout `view.statusBarShow` chooses: the navigator's
|
||||
/// artboard name, the tool in use, the date and time, the undo and redo counts, or the document's
|
||||
/// color profile.
|
||||
#[test]
|
||||
fn the_status_bar_info_field_shows_the_chosen_readout() {
|
||||
let (mut app, ctx) = status_readout_app();
|
||||
let texts = status_after(&mut app, &ctx, 0.1, vec![]);
|
||||
assert!(texts.iter().any(|t| t.text == "Selection"), "Current Tool by default: {texts:?}");
|
||||
|
||||
app.run("artboard.new", json!({"x": 200, "y": 0, "width": 100, "height": 100, "name": "Back Cover"})).unwrap();
|
||||
app.run("view.statusBarShow", json!({"show": "artboardName"})).unwrap();
|
||||
let first = app.session.active().unwrap().doc.artboards[0].name.clone();
|
||||
assert!(status_after(&mut app, &ctx, 0.1, vec![]).iter().any(|t| t.text == first), "the first artboard's name");
|
||||
app.run("view.goToArtboard", json!({"index": 1})).unwrap();
|
||||
assert!(status_after(&mut app, &ctx, 0.1, vec![]).iter().any(|t| t.text == "Back Cover"), "the navigator's artboard");
|
||||
|
||||
app.run("view.statusBarShow", json!({"show": "undos"})).unwrap();
|
||||
let counts = |app: &crate::VectorcraftApp| {
|
||||
let h = &app.session.active().unwrap().history;
|
||||
format!("Undos: {} Redos: {}", h.undo.len(), h.redo.len())
|
||||
};
|
||||
let before = app.session.active().unwrap().history.undo.len();
|
||||
app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
assert_eq!(counts(&app), format!("Undos: {} Redos: 0", before + 1));
|
||||
assert!(status_after(&mut app, &ctx, 0.1, vec![]).iter().any(|t| t.text == counts(&app)));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(counts(&app), format!("Undos: {before} Redos: 1"));
|
||||
assert!(status_after(&mut app, &ctx, 0.1, vec![]).iter().any(|t| t.text == counts(&app)), "the redo step counts");
|
||||
|
||||
app.run("view.statusBarShow", json!({"show": "colorProfile"})).unwrap();
|
||||
let working = vectorcraft_color::cms::working_settings();
|
||||
let mode_working = |app: &crate::VectorcraftApp| match app.session.active().unwrap().doc.color_mode {
|
||||
vectorcraft_doc::ColorMode::Rgb => working.rgb.clone(),
|
||||
vectorcraft_doc::ColorMode::Cmyk => working.cmyk.clone(),
|
||||
};
|
||||
let untagged = vectorcraft_color::cms::canonical_name(&mode_working(&app)).to_string();
|
||||
assert_eq!(super::color_profile_name(&app.session.active().unwrap().doc), untagged, "untagged: the working space");
|
||||
assert!(status_after(&mut app, &ctx, 0.1, vec![]).iter().any(|t| t.text == untagged));
|
||||
// A profile assigned for the document's color mode shows; one that is not available shows the
|
||||
// working space the document draws through.
|
||||
let (key, assigned) = match app.session.active().unwrap().doc.color_mode {
|
||||
vectorcraft_doc::ColorMode::Rgb => ("rgb", vectorcraft_color::cms::PROPHOTO_RGB),
|
||||
vectorcraft_doc::ColorMode::Cmyk => ("cmyk", vectorcraft_color::cms::DEVICE_CMYK),
|
||||
};
|
||||
app.run("edit.assignProfile", json!({ key: assigned })).unwrap();
|
||||
assert!(status_after(&mut app, &ctx, 0.1, vec![]).iter().any(|t| t.text == assigned), "the assigned profile");
|
||||
let st = app.session.active_mut().unwrap();
|
||||
std::sync::Arc::make_mut(&mut st.doc).color_profiles =
|
||||
vectorcraft_doc::ColorProfiles { rgb: Some("Not Installed".into()), cmyk: Some("Not Installed".into()) };
|
||||
assert!(status_after(&mut app, &ctx, 0.1, vec![]).iter().any(|t| t.text == untagged), "an unavailable profile: the working space");
|
||||
|
||||
app.run("view.statusBarShow", json!({"show": "dateTime"})).unwrap();
|
||||
let texts = status_after(&mut app, &ctx, 0.1, vec![]);
|
||||
let is_date = |s: &str| {
|
||||
let b = s.as_bytes();
|
||||
b.len() == 16
|
||||
&& b.iter().enumerate().all(|(i, c)| match i {
|
||||
4 | 7 => *c == b'-',
|
||||
10 => *c == b' ',
|
||||
13 => *c == b':',
|
||||
_ => c.is_ascii_digit(),
|
||||
})
|
||||
};
|
||||
assert!(texts.iter().any(|t| is_date(&t.text)), "a date and time: {texts:?}");
|
||||
assert_eq!(super::date_time_label([2026, 3, 7, 9, 5, 59]), "2026-03-07 09:05");
|
||||
|
||||
// Without a document the field shows a dash, which can still be clicked.
|
||||
app.run("view.statusBarShow", json!({"show": "undos"})).unwrap();
|
||||
while app.session.active().is_some() {
|
||||
app.session.execute("file.close", &json!({})).unwrap();
|
||||
}
|
||||
assert!(status_after(&mut app, &ctx, 0.1, vec![]).iter().any(|t| t.text == "–"));
|
||||
}
|
||||
|
||||
/// A click on the info field opens its Show menu, which checks the readout shown and changes it.
|
||||
#[test]
|
||||
fn a_click_on_the_info_field_opens_the_show_menu() {
|
||||
let (mut app, ctx) = status_readout_app();
|
||||
status_after(&mut app, &ctx, 1.0, vec![egui::Event::PointerGone]);
|
||||
let texts = status_after(&mut app, &ctx, 0.1, vec![]);
|
||||
let field = text_center(&texts, "Selection");
|
||||
status_after(&mut app, &ctx, 0.1, click_at(field));
|
||||
let texts = status_after(&mut app, &ctx, 0.1, vec![]);
|
||||
let show = text_center(&texts, "Show");
|
||||
status_after(&mut app, &ctx, 0.1, vec![egui::Event::PointerMoved(show)]);
|
||||
status_after(&mut app, &ctx, 0.5, vec![]);
|
||||
let texts = status_after(&mut app, &ctx, 0.5, vec![]);
|
||||
for label in [" Artboard Name", "✓ Current Tool", " Date and Time", " Number of Undos", " Document Color Profile"] {
|
||||
assert!(texts.iter().any(|t| t.text == label), "no {label:?} in {texts:?}");
|
||||
}
|
||||
let undos = text_center(&texts, " Number of Undos");
|
||||
status_after(&mut app, &ctx, 0.1, vec![egui::Event::PointerMoved(egui::pos2(undos.x, show.y)), egui::Event::PointerMoved(undos)]);
|
||||
status_after(&mut app, &ctx, 0.1, click_at(undos));
|
||||
assert_eq!(app.ui.status_bar_show, super::StatusBarShow::Undos);
|
||||
assert_eq!(crate::menus::checked(&app, "view.statusBarShow", &json!({"show": "undos"})), Some(true));
|
||||
assert_eq!(crate::menus::checked(&app, "view.statusBarShow", &json!({"show": "currentTool"})), Some(false));
|
||||
}
|
||||
|
||||
/// `view.statusBarShow` takes only the five readouts, leaving the choice as it was otherwise, and the
|
||||
/// choice is saved with the UI state, which reads a value it does not know as Current Tool.
|
||||
#[test]
|
||||
fn the_status_bar_show_choice_is_checked_and_saved() {
|
||||
let (mut app, _ctx) = status_readout_app();
|
||||
assert_eq!(app.run("view.statusBarShow", json!({"show": "dateTime"})).unwrap(), json!("dateTime"));
|
||||
for bad in [json!({}), json!({"show": "zoom"}), json!({"show": 2}), json!({"show": null}), json!({"show": "DateTime"})] {
|
||||
let err = app.run("view.statusBarShow", bad.clone()).unwrap_err();
|
||||
assert!(err.contains("`show` must be one of artboardName, currentTool, dateTime, undos, colorProfile"), "{bad}: {err}");
|
||||
assert_eq!(app.ui.status_bar_show, super::StatusBarShow::DateTime, "{bad} leaves the choice");
|
||||
}
|
||||
let saved = serde_json::to_value(&app.ui).unwrap();
|
||||
assert_eq!(saved["status_bar_show"], json!("dateTime"));
|
||||
let read: crate::state::UiState = serde_json::from_value(saved.clone()).unwrap();
|
||||
assert_eq!(read.status_bar_show, super::StatusBarShow::DateTime);
|
||||
for (stored, shown) in [
|
||||
(json!("colorProfile"), super::StatusBarShow::ColorProfile),
|
||||
(json!("zoom"), super::StatusBarShow::CurrentTool),
|
||||
(json!(7), super::StatusBarShow::CurrentTool),
|
||||
] {
|
||||
let mut v = saved.clone();
|
||||
v["status_bar_show"] = stored;
|
||||
assert_eq!(serde_json::from_value::<crate::state::UiState>(v).unwrap().status_bar_show, shown);
|
||||
}
|
||||
let mut v = saved;
|
||||
v.as_object_mut().unwrap().remove("status_bar_show");
|
||||
assert_eq!(
|
||||
serde_json::from_value::<crate::state::UiState>(v).unwrap().status_bar_show,
|
||||
super::StatusBarShow::CurrentTool,
|
||||
"state saved before it"
|
||||
);
|
||||
}
|
||||
|
||||
/// Busy-job transitions preserve message placement, labels and accessibility.
|
||||
#[test]
|
||||
fn background_activity_keeps_the_status_message_origin() {
|
||||
|
||||
@@ -1348,6 +1348,14 @@ font-technology Variable Променливи
|
||||
Previous Artboard Предишна работна област
|
||||
Next Artboard Следваща работна област
|
||||
Last Artboard Последна работна област
|
||||
Status Bar Show Показване в лентата на състоянието
|
||||
Artboard Name Име на работната област
|
||||
Current Tool Текущ инструмент
|
||||
Date and Time Дата и час
|
||||
Number of Undos Брой отмени
|
||||
Document Color Profile Цветови профил на документа
|
||||
Undos Отмени
|
||||
Redos Повторения
|
||||
Go To Link Към връзката
|
||||
GPL palettes keep solid colours only, as RGB. Палитрите GPL запазват само плътни цветове като RGB.
|
||||
GPU Performance Производителност на графичния процесор
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1085,6 +1085,14 @@ lighting Light Svjetlo
|
||||
Previous Artboard Prethodna radna ploča
|
||||
Next Artboard Sljedeća radna ploča
|
||||
Last Artboard Posljednja radna ploča
|
||||
Status Bar Show Prikaz u traci stanja
|
||||
Artboard Name Naziv radne ploče
|
||||
Current Tool Trenutni alat
|
||||
Date and Time Datum i vrijeme
|
||||
Number of Undos Broj poništavanja
|
||||
Document Color Profile Profil boja dokumenta
|
||||
Undos Poništavanja
|
||||
Redos Ponavljanja
|
||||
Graphic Style Options… Opcije grafičkog stila…
|
||||
Symbol Options Opcije simbola
|
||||
Symbol Options… Opcije simbola…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2325,6 +2325,14 @@ digits Malayalam Malaiàlam
|
||||
Enclosing Lasso Llaç envoltant
|
||||
Last Últim
|
||||
Last Artboard Últim tauler
|
||||
Status Bar Show Mostra a la barra d'estat
|
||||
Artboard Name Nom del tauler
|
||||
Current Tool Eina actual
|
||||
Date and Time Data i hora
|
||||
Number of Undos Nombre de desfets
|
||||
Document Color Profile Perfil de color del document
|
||||
Undos Desfets
|
||||
Redos Refets
|
||||
Last commit Últim commit
|
||||
Last Effect: {name} Últim efecte: {name}
|
||||
Last Effect: {} Últim efecte: {}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1109,6 +1109,14 @@ lighting Light Světlo
|
||||
Previous Artboard Předchozí pracovní plocha
|
||||
Next Artboard Další pracovní plocha
|
||||
Last Artboard Poslední pracovní plocha
|
||||
Status Bar Show Zobrazit ve stavovém řádku
|
||||
Artboard Name Název pracovní plochy
|
||||
Current Tool Aktuální nástroj
|
||||
Date and Time Datum a čas
|
||||
Number of Undos Počet kroků zpět
|
||||
Document Color Profile Barevný profil dokumentu
|
||||
Undos Zpět
|
||||
Redos Znovu
|
||||
Graphic Style Options… Možnosti grafického stylu…
|
||||
Symbol Options Možnosti symbolu
|
||||
Symbol Options… Možnosti symbolu…
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6527,6 +6527,14 @@ swatch Bright Violet Kraftig violet
|
||||
Color wheel Farvehjul
|
||||
First Artboard Første kunstplade
|
||||
Last Artboard Sidste kunstplade
|
||||
Status Bar Show Vis i statuslinjen
|
||||
Artboard Name Kunstpladens navn
|
||||
Current Tool Aktuelt værktøj
|
||||
Date and Time Dato og klokkeslæt
|
||||
Number of Undos Antal fortrydelser
|
||||
Document Color Profile Dokumentets farveprofil
|
||||
Undos Fortrydelser
|
||||
Redos Gentagelser
|
||||
Next Artboard Næste kunstplade
|
||||
Previous Artboard Forrige kunstplade
|
||||
Step up or down Trinvis op eller ned
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1393,6 +1393,14 @@ file-filter Templates Vorlagen
|
||||
Previous Artboard Vorherige Zeichenfläche
|
||||
Next Artboard Nächste Zeichenfläche
|
||||
Last Artboard Letzte Zeichenfläche
|
||||
Status Bar Show In Statusleiste anzeigen
|
||||
Artboard Name Name der Zeichenfläche
|
||||
Current Tool Aktuelles Werkzeug
|
||||
Date and Time Datum und Uhrzeit
|
||||
Number of Undos Anzahl der Rückgängig-Schritte
|
||||
Document Color Profile Farbprofil des Dokuments
|
||||
Undos Rückgängig
|
||||
Redos Wiederholen
|
||||
Go To Link Gehe zu Verknüpfung
|
||||
GPL palettes keep solid colours only, as RGB. GPL-Paletten enthalten nur einfarbige Farben, als RGB.
|
||||
GPU Performance GPU-Leistung
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1323,6 +1323,14 @@ font-technology Variable Μεταβλητές
|
||||
Previous Artboard Προηγούμενος πίνακας σχεδίασης
|
||||
Next Artboard Επόμενος πίνακας σχεδίασης
|
||||
Last Artboard Τελευταίος πίνακας σχεδίασης
|
||||
Status Bar Show Εμφάνιση στη γραμμή κατάστασης
|
||||
Artboard Name Όνομα πίνακα σχεδίασης
|
||||
Current Tool Τρέχον εργαλείο
|
||||
Date and Time Ημερομηνία και ώρα
|
||||
Number of Undos Αριθμός αναιρέσεων
|
||||
Document Color Profile Χρωματικό προφίλ εγγράφου
|
||||
Undos Αναιρέσεις
|
||||
Redos Επαναλήψεις
|
||||
Go To Link Μετάβαση στη σύνδεση
|
||||
GPL palettes keep solid colours only, as RGB. Οι παλέτες GPL διατηρούν μόνο συμπαγή χρώματα σε RGB.
|
||||
GPU Performance Απόδοση GPU
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1402,6 +1402,14 @@ font-technology Variable Variables
|
||||
Previous Artboard Mesa de trabajo anterior
|
||||
Next Artboard Mesa de trabajo siguiente
|
||||
Last Artboard Última mesa de trabajo
|
||||
Status Bar Show Mostrar en la barra de estado
|
||||
Artboard Name Nombre de la mesa de trabajo
|
||||
Current Tool Herramienta actual
|
||||
Date and Time Fecha y hora
|
||||
Number of Undos Número de deshacer
|
||||
Document Color Profile Perfil de color del documento
|
||||
Undos Deshacer
|
||||
Redos Rehacer
|
||||
Go To Link Ir al vínculo
|
||||
GPL palettes keep solid colours only, as RGB. Las paletas GPL solo conservan colores sólidos, en RGB.
|
||||
GPU Performance Rendimiento de GPU
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1088,6 +1088,14 @@ lighting Light Valo
|
||||
Previous Artboard Edellinen piirtoalusta
|
||||
Next Artboard Seuraava piirtoalusta
|
||||
Last Artboard Viimeinen piirtoalusta
|
||||
Status Bar Show Näytä tilarivillä
|
||||
Artboard Name Piirtoalustan nimi
|
||||
Current Tool Nykyinen työkalu
|
||||
Date and Time Päivämäärä ja aika
|
||||
Number of Undos Kumousten määrä
|
||||
Document Color Profile Asiakirjan väriprofiili
|
||||
Undos Kumoukset
|
||||
Redos Uudelleentekot
|
||||
Graphic Style Options… Grafiikkatyylin asetukset…
|
||||
Symbol Options Symbolin asetukset
|
||||
Symbol Options… Symbolin asetukset…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1396,6 +1396,14 @@ font-technology Variable Variables
|
||||
Previous Artboard Plan de travail précédent
|
||||
Next Artboard Plan de travail suivant
|
||||
Last Artboard Dernier plan de travail
|
||||
Status Bar Show Afficher dans la barre d'état
|
||||
Artboard Name Nom du plan de travail
|
||||
Current Tool Outil actif
|
||||
Date and Time Date et heure
|
||||
Number of Undos Nombre d'annulations
|
||||
Document Color Profile Profil colorimétrique du document
|
||||
Undos Annulations
|
||||
Redos Rétablissements
|
||||
Go To Link Atteindre le lien
|
||||
GPL palettes keep solid colours only, as RGB. Les palettes GPL ne conservent que les couleurs unies, en RVB.
|
||||
GPU Performance Performances GPU
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1908,6 +1908,14 @@
|
||||
Lasso Tool લાસો ટૂલ
|
||||
Last છેલ્લું
|
||||
Last Artboard છેલ્લું આર્ટબોર્ડ
|
||||
Status Bar Show સ્ટેટસ બારમાં બતાવો
|
||||
Artboard Name આર્ટબોર્ડનું નામ
|
||||
Current Tool વર્તમાન ટૂલ
|
||||
Date and Time તારીખ અને સમય
|
||||
Number of Undos પૂર્વવત્ કરવાની સંખ્યા
|
||||
Document Color Profile દસ્તાવેજની રંગ પ્રોફાઇલ
|
||||
Undos પૂર્વવત્
|
||||
Redos ફરીથી કરો
|
||||
Last Effect: {name} છેલ્લી ઇફેક્ટ: {name}
|
||||
Last Effect… છેલ્લી ઇફેક્ટ…
|
||||
Last Page છેલ્લું પેજ
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 72 and column 5.
|
@@ -2570,6 +2570,14 @@ antiAlias Sharp शार्प
|
||||
Lasso Tool लैस्सो टूल
|
||||
Last अंतिम
|
||||
Last Artboard अंतिम आर्टबोर्ड
|
||||
Status Bar Show स्टेटस बार में दिखाएँ
|
||||
Artboard Name आर्टबोर्ड का नाम
|
||||
Current Tool मौजूदा टूल
|
||||
Date and Time तारीख और समय
|
||||
Number of Undos पूर्ववत करने की संख्या
|
||||
Document Color Profile दस्तावेज़ की रंग प्रोफ़ाइल
|
||||
Undos पूर्ववत
|
||||
Redos फिर से करें
|
||||
Last commit अंतिम कमिट
|
||||
Last Effect: {} अंतिम इफ़ेक्ट: {}
|
||||
Last Effect: {name} अंतिम इफ़ेक्ट: {name}
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1744,6 +1744,14 @@ file-filter Templates Sablonok
|
||||
Previous Artboard Előző rajztábla
|
||||
Next Artboard Következő rajztábla
|
||||
Last Artboard Utolsó rajztábla
|
||||
Status Bar Show Megjelenítés az állapotsorban
|
||||
Artboard Name Rajztábla neve
|
||||
Current Tool Aktuális eszköz
|
||||
Date and Time Dátum és idő
|
||||
Number of Undos Visszavonások száma
|
||||
Document Color Profile Dokumentum színprofilja
|
||||
Undos Visszavonások
|
||||
Redos Újra
|
||||
Go to Conflict Ugrás az ütközésre
|
||||
Go To Link Ugrás a csatoláshoz
|
||||
Good Jó
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5858,6 +5858,14 @@ swatch Bright Violet Violet cerah
|
||||
Color wheel Roda warna
|
||||
First Artboard Artboard pertama
|
||||
Last Artboard Artboard terakhir
|
||||
Status Bar Show Tampilkan di bilah status
|
||||
Artboard Name Nama artboard
|
||||
Current Tool Alat saat ini
|
||||
Date and Time Tanggal dan waktu
|
||||
Number of Undos Jumlah urungkan
|
||||
Document Color Profile Profil warna dokumen
|
||||
Undos Urungkan
|
||||
Redos Ulangi
|
||||
Next Artboard Artboard berikutnya
|
||||
Previous Artboard Artboard sebelumnya
|
||||
Step up or down Naikkan atau turunkan bertahap
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2467 and column 5.
|
@@ -1423,6 +1423,14 @@ file-filter Templates Modelli
|
||||
Previous Artboard Tavola da disegno precedente
|
||||
Next Artboard Tavola da disegno successiva
|
||||
Last Artboard Ultima tavola da disegno
|
||||
Status Bar Show Mostra nella barra di stato
|
||||
Artboard Name Nome tavola da disegno
|
||||
Current Tool Strumento attivo
|
||||
Date and Time Data e ora
|
||||
Number of Undos Numero di annullamenti
|
||||
Document Color Profile Profilo colore del documento
|
||||
Undos Annullamenti
|
||||
Redos Ripristini
|
||||
Go To Link Vai al collegamento
|
||||
GPL palettes keep solid colours only, as RGB. Le palette GPL conservano solo i colori pieni, in RGB.
|
||||
GPU Performance Prestazioni GPU
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1885,6 +1885,14 @@ antiAlias Sharp シャープ
|
||||
Previous Artboard 前のアートボード
|
||||
Next Artboard 次のアートボード
|
||||
Last Artboard 最後のアートボード
|
||||
Status Bar Show ステータスバーの表示
|
||||
Artboard Name アートボード名
|
||||
Current Tool 現在のツール
|
||||
Date and Time 日付と時刻
|
||||
Number of Undos 取り消し回数
|
||||
Document Color Profile ドキュメントのカラープロファイル
|
||||
Undos 取り消し
|
||||
Redos やり直し
|
||||
Gradient Fill: click to apply, drag onto art グラデーションの塗り: クリックで適用、アートにドラッグ
|
||||
Gradient Mesh グラデーションメッシュ
|
||||
Gradient Tool グラデーションツール
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6146,6 +6146,14 @@ imageTrace Outline კონტური
|
||||
JavaScript: JavaScript:
|
||||
Large Gaps დიდი ღრეჩოები
|
||||
Last Artboard ბოლო სამუშაო დაფა
|
||||
Status Bar Show ჩვენება მდგომარეობის ზოლში
|
||||
Artboard Name სამუშაო დაფის სახელი
|
||||
Current Tool მიმდინარე ხელსაწყო
|
||||
Date and Time თარიღი და დრო
|
||||
Number of Undos დაბრუნებების რაოდენობა
|
||||
Document Color Profile დოკუმენტის ფერის პროფილი
|
||||
Undos დაბრუნებები
|
||||
Redos ხელახლა შესრულებები
|
||||
Levels დონეები
|
||||
Library Panel Options ბიბლიოთეკის პანელის პარამეტრები
|
||||
Locate Library… ბიბლიოთეკის მოძებნა…
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1875,6 +1875,14 @@ antiAlias Sharp 날카롭게
|
||||
Previous Artboard 이전 아트보드
|
||||
Next Artboard 다음 아트보드
|
||||
Last Artboard 마지막 아트보드
|
||||
Status Bar Show 상태 표시줄 표시
|
||||
Artboard Name 아트보드 이름
|
||||
Current Tool 현재 도구
|
||||
Date and Time 날짜 및 시간
|
||||
Number of Undos 실행 취소 횟수
|
||||
Document Color Profile 문서 색상 프로파일
|
||||
Undos 실행 취소
|
||||
Redos 다시 실행
|
||||
Gradient Fill: click to apply, drag onto art 그레이디언트 칠: 클릭하여 적용, 아트웍에 끌어 놓기
|
||||
Gradient Mesh 그레이디언트 메시
|
||||
Gradient Tool 그레이디언트 도구
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -8307,6 +8307,14 @@ swatch Bright Violet Felviolet
|
||||
Color wheel Kleurenwiel
|
||||
First Artboard Eerste tekenbord
|
||||
Last Artboard Laatste tekenbord
|
||||
Status Bar Show Weergeven in statusbalk
|
||||
Artboard Name Naam van tekenbord
|
||||
Current Tool Huidig gereedschap
|
||||
Date and Time Datum en tijd
|
||||
Number of Undos Aantal ongedaan gemaakte stappen
|
||||
Document Color Profile Kleurprofiel van document
|
||||
Undos Ongedaan maken
|
||||
Redos Opnieuw uitvoeren
|
||||
Next Artboard Volgende tekenbord
|
||||
Previous Artboard Vorige tekenbord
|
||||
Step up or down Stapsgewijs verhogen of verlagen
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1045,6 +1045,14 @@ lighting Light Światło
|
||||
Previous Artboard Poprzedni obszar kompozycji
|
||||
Next Artboard Następny obszar kompozycji
|
||||
Last Artboard Ostatni obszar kompozycji
|
||||
Status Bar Show Pokaż na pasku stanu
|
||||
Artboard Name Nazwa obszaru kompozycji
|
||||
Current Tool Bieżące narzędzie
|
||||
Date and Time Data i godzina
|
||||
Number of Undos Liczba cofnięć
|
||||
Document Color Profile Profil kolorów dokumentu
|
||||
Undos Cofnięcia
|
||||
Redos Ponowienia
|
||||
Graphic Style Options… Opcje stylu graficznego…
|
||||
Symbol Options Opcje symbolu
|
||||
Symbol Options… Opcje symbolu…
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1258,6 +1258,14 @@ antiAlias Sharp Nítido
|
||||
Previous Artboard Mesa de trabalho anterior
|
||||
Next Artboard Próxima mesa de trabalho
|
||||
Last Artboard Última mesa de trabalho
|
||||
Status Bar Show Mostrar na barra de status
|
||||
Artboard Name Nome da prancheta
|
||||
Current Tool Ferramenta atual
|
||||
Date and Time Data e hora
|
||||
Number of Undos Número de ações desfeitas
|
||||
Document Color Profile Perfil de cor do documento
|
||||
Undos Desfazer
|
||||
Redos Refazer
|
||||
Gradient Gradiente
|
||||
Gradient Fill: click to apply, drag onto art Preenchimento de Gradiente: clique para aplicar, arraste sobre a arte
|
||||
Gradient Finishes Finalizações de Gradiente
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1412,6 +1412,14 @@ file-filter Templates Шаблоны
|
||||
Previous Artboard Предыдущая монтажная область
|
||||
Next Artboard Следующая монтажная область
|
||||
Last Artboard Последняя монтажная область
|
||||
Status Bar Show Показ в строке состояния
|
||||
Artboard Name Имя монтажной области
|
||||
Current Tool Текущий инструмент
|
||||
Date and Time Дата и время
|
||||
Number of Undos Число отмен
|
||||
Document Color Profile Цветовой профиль документа
|
||||
Undos Отмены
|
||||
Redos Повторы
|
||||
Go To Link Перейти по ссылке
|
||||
GPL palettes keep solid colours only, as RGB. Палитры GPL сохраняют только сплошные цвета в RGB.
|
||||
GPU Performance Производительность GPU
|
||||
@@ -7840,9 +7848,6 @@ shape Open Открытый
|
||||
@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` должно содержать индексы опорных точек (0, 1, 2…), а не {k}
|
||||
@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` должно содержать индексы опорных точек (0, 1, 2…), а не {v}
|
||||
@msg `answer` for this {kind} dialog must be one of: {_1} `answer` для этого диалогового окна {kind} должно быть одним из: {_1}
|
||||
@msg `answers` has more than {MAX_ANSWERS} answers в `answers` больше {MAX_ANSWERS} ответов
|
||||
@msg `answers` is larger than 1 MiB `answers` больше 1 MiB
|
||||
@msg `answers` must be a list of answers `answers` должно быть списком ответов
|
||||
@msg `artboard` must be the index of an artboard of the document `artboard` должно быть индексом монтажной области документа
|
||||
@msg `artboardsAt` must be [x, y] `artboardsAt` должно быть [x, y]
|
||||
@msg `artboards` must be a list of artboard indices `artboards` должно быть списком индексов монтажных областей
|
||||
@@ -8586,10 +8591,6 @@ shape Open Открытый
|
||||
@msg invalid t недопустимое t
|
||||
@msg invalid time value недопустимое значение времени
|
||||
@msg invalid tolerance недопустимый допуск
|
||||
@msg is a confirm's answer, whose `value` must be true or false является ответом для confirm, `value` которого должно быть true или false
|
||||
@msg is a prompt's answer, whose `value` must be a string or null является ответом для prompt, `value` которого должно быть строкой или null
|
||||
@msg is an alert's answer, whose `value` must be "ok" является ответом для alert, `value` которого должно быть "ok"
|
||||
@msg is longer than 16 KiB длиннее 16 KiB
|
||||
@msg it draws its image through a clipping path он рисует своё изображение через контур обрезки
|
||||
@msg it draws no image он не рисует изображения
|
||||
@msg it is larger than {MAX_DICTIONARY_FILE_BYTES} bytes он больше {MAX_DICTIONARY_FILE_BYTES} байт
|
||||
@@ -8717,9 +8718,6 @@ shape Open Открытый
|
||||
@msg more than {MAX_FILES} images больше {MAX_FILES} изображений
|
||||
@msg more than {MAX_IMPORT} presets больше {MAX_IMPORT} наборов
|
||||
@msg moved must be [[x,y]…] moved должно быть [[x,y]…]
|
||||
@msg must be an object with `kind` and `value` должен быть объектом с `kind` и `value`
|
||||
@msg must have `kind` alert, confirm or prompt должен иметь `kind` alert, confirm или prompt
|
||||
@msg must have exactly `kind` and `value` должен содержать только `kind` и `value`
|
||||
# Messages that begin with a lowercase word, n to s.
|
||||
@msg name (a toolbar's name) is required требуется name (имя панели инструментов)
|
||||
@msg name (a toolbar's name) must be a string name (имя панели инструментов) должно быть строкой
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1361,6 +1361,14 @@ font-technology Variable Variabla
|
||||
Previous Artboard Föregående rityta
|
||||
Next Artboard Nästa rityta
|
||||
Last Artboard Sista ritytan
|
||||
Status Bar Show Visa i statusfältet
|
||||
Artboard Name Ritytans namn
|
||||
Current Tool Aktuellt verktyg
|
||||
Date and Time Datum och tid
|
||||
Number of Undos Antal ångringar
|
||||
Document Color Profile Dokumentets färgprofil
|
||||
Undos Ångringar
|
||||
Redos Gör om
|
||||
Go To Link Gå till länk
|
||||
GPL palettes keep solid colours only, as RGB. GPL-paletter sparar bara enfärgade färger, som RGB.
|
||||
GPU Performance GPU-prestanda
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3213,6 +3213,72 @@ fn f(text: &str, n: usize) -> Result<u32, String> {
|
||||
assert!(message_literals_in(&["effects"]).contains("the SVG filter isn't valid XML: {e}"));
|
||||
}
|
||||
|
||||
/// The scan reads the message a `.map_err` closure gives with `return`, also from inside an `if`
|
||||
/// branch or a `let` statement but not from a closure inside it, the last expression after a block
|
||||
/// statement with no `;`, and the values a `let` statement gives a local that the closure returns,
|
||||
/// and not those of a local the closure only passes elsewhere or writes into a format string, by
|
||||
/// name or as a `format!` argument.
|
||||
#[test]
|
||||
fn message_scan_reads_returns_and_tails_and_skips_locals() {
|
||||
let code = r#"
|
||||
fn f(text: &str) -> Result<u32, String> {
|
||||
let a = read(text).map_err(|_| {
|
||||
return "the file cannot be read".to_string();
|
||||
})?;
|
||||
let b = read(text).map_err(|e| {
|
||||
if e.is_io() {
|
||||
return format!("the disk failed: {e}");
|
||||
}
|
||||
e.to_string()
|
||||
})?;
|
||||
let c = read(text).map_err(|e| {
|
||||
if e.is_io() { cleanup(); }
|
||||
format!("the file is cut short: {e}")
|
||||
})?;
|
||||
let d = read(text).map_err(|e| {
|
||||
if e.is_io() { cleanup(); }
|
||||
let level = if e.is_io() { "warning" } else { "error" };
|
||||
log_failure(level, &e);
|
||||
format!("the file is not a library: {e}")
|
||||
})?;
|
||||
let g = read(text).map_err(|e| {
|
||||
let kind = match e.kind() {
|
||||
Kind::Gone => return GONE.into(),
|
||||
_ => { "some other kind" }
|
||||
};
|
||||
format!("the {kind} file is wrong")
|
||||
})?;
|
||||
let h = read(text).map_err(|e| {
|
||||
let name = e.name().unwrap_or_else(|| return "unnamed source");
|
||||
let note = e.note().map(move |n| { return "noted source" });
|
||||
format!("the source {name} is gone")
|
||||
})?;
|
||||
let k = read(text).map_err(|e| {
|
||||
let why = if e.is_deep() { format!("its objects are nested too deep ({e})") } else { e.to_string() };
|
||||
FormatError::NotVectorcraft(why)
|
||||
})?;
|
||||
let m = read(text).map_err(|e| {
|
||||
let level = if e.is_io() { "warning" } else { "error" };
|
||||
format!("the file failed with a {}", level)
|
||||
})?;
|
||||
Ok(a)
|
||||
}
|
||||
"#;
|
||||
let consts = [("GONE", "the file is gone")].map(|(k, v)| (k.to_string(), vec![v.to_string()])).into();
|
||||
let want = [
|
||||
"the file cannot be read",
|
||||
"the disk failed: {e}",
|
||||
"the file is cut short: {e}",
|
||||
"the file is not a library: {e}",
|
||||
"the file is gone",
|
||||
"the {kind} file is wrong",
|
||||
"the source {name} is gone",
|
||||
"its objects are nested too deep ({e})",
|
||||
"the file failed with a {_1}",
|
||||
];
|
||||
assert_eq!(source_messages(code, &[], &consts), want);
|
||||
}
|
||||
|
||||
/// The scan reads the messages of an error handler that a `.map_err(name)` names: a closure bound
|
||||
/// to that name, up to its `;`, and a function of that name, each of its statements.
|
||||
#[test]
|
||||
@@ -5019,27 +5085,72 @@ fn statement(code: &str) -> &str {
|
||||
|
||||
/// Where each value a closure can return starts, `body` being the text after its parameters: the
|
||||
/// body itself, each block or `if`/`else` branch in it (`{ ACB_CUT_SHORT.into() }`, not the
|
||||
/// `{` of a `json!({…})` value), each `match` arm (`=>`) and each statement after a `;` (the
|
||||
/// last expression of a block or a function body), up to the bracket that closes the call the
|
||||
/// closure is given to or the function.
|
||||
/// `{` of a `json!({…})` value), each `match` arm (`=>`), each statement after a `;` or after a
|
||||
/// block statement's `}` (the last expression of a block or a function body) and the value of
|
||||
/// each `return`, up to the bracket that closes the call the closure is given to or the function.
|
||||
/// The blocks, arms and statements inside a `let` statement give a local its value, so they count
|
||||
/// only when the local's name is used, outside a string literal and a `format!`, in a value the closure returns
|
||||
/// (`let why = if … { format!("…") } else { … }; Error(why)`), and not when the local only goes
|
||||
/// elsewhere (`let level = if … { "warning" } else { "error" }; log(level);`). A `return` counts
|
||||
/// wherever it is, except inside a closure the body holds (`unwrap_or_else(|| return …)`).
|
||||
fn closure_returns(body: &str) -> Vec<&str> {
|
||||
let mut out = vec![body];
|
||||
let (mut depth, mut chars) = (0usize, body.char_indices());
|
||||
let ident = |c: char| c.is_alphanumeric() || c == '_';
|
||||
// The name a `let` statement starting at `at` binds (empty for a pattern), if it is one.
|
||||
let let_at = |at: usize| {
|
||||
let rest = body[at..].trim_start().strip_prefix("let").filter(|r| r.starts_with(char::is_whitespace))?.trim_start();
|
||||
let rest = rest.strip_prefix("mut ").map_or(rest, str::trim_start);
|
||||
Some(&rest[..rest.find(|c: char| !ident(c)).unwrap_or(rest.len())])
|
||||
};
|
||||
// Each place a value starts, with the `let` whose local it gives a value to, if any, and
|
||||
// whether a `return` gives it.
|
||||
let mut found: Vec<(&str, Option<&str>, bool)> = vec![(body, None, false)];
|
||||
// For each open bracket, the `quiet` and `in_let` of the text around it: `quiet` names the
|
||||
// `let` that a value inside the bracket belongs to, `in_let` the `let` the statement is.
|
||||
let mut outer = vec![];
|
||||
let (mut quiet, mut in_let, mut chars) = (None, let_at(0), body.char_indices());
|
||||
// Where the innermost closure in the body that a `return` would leave ends.
|
||||
let mut inner_closure_end = 0;
|
||||
while let Some((at, c)) = chars.next() {
|
||||
match c {
|
||||
'(' | '[' => depth += 1,
|
||||
'{' => {
|
||||
depth += 1;
|
||||
if !body[..at].trim_end().ends_with('(') {
|
||||
out.push(&body[at + 1..]);
|
||||
'|' if at >= inner_closure_end => {
|
||||
let before = body[..at].trim_end();
|
||||
let after_operator = before.ends_with(['(', ',', '=']) && !["==", "!=", "<=", ">="].iter().any(|op| before.ends_with(op));
|
||||
let opens = after_operator || (before.ends_with("move") && !before[..before.len() - 4].ends_with(ident));
|
||||
let params_end = if body[at + 1..].starts_with('|') { Some(at + 1) } else { body[at + 1..].find('|').map(|end| at + 1 + end) };
|
||||
if let Some(params_end) = params_end.filter(|_| opens) {
|
||||
inner_closure_end = params_end + 1 + statement(&body[params_end + 1..]).len();
|
||||
}
|
||||
}
|
||||
')' | ']' | '}' => match depth.checked_sub(1) {
|
||||
Some(d) => depth = d,
|
||||
None => break,
|
||||
},
|
||||
';' => out.push(&body[at + 1..]),
|
||||
'=' if body[at + 1..].starts_with('>') => out.push(&body[at + 2..]),
|
||||
'(' | '[' | '{' => {
|
||||
outer.push((quiet, in_let));
|
||||
if c == '{' && !body[..at].trim_end().ends_with('(') {
|
||||
found.push((&body[at + 1..], quiet.or(in_let), false));
|
||||
}
|
||||
quiet = quiet.or(in_let);
|
||||
in_let = if c == '{' { let_at(at + 1) } else { None };
|
||||
}
|
||||
')' | ']' | '}' => {
|
||||
let Some(around) = outer.pop() else { break };
|
||||
(quiet, in_let) = around;
|
||||
if c == '}' {
|
||||
found.push((&body[at + 1..], quiet.or(in_let), false));
|
||||
if in_let.is_none() {
|
||||
in_let = let_at(at + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
';' => {
|
||||
found.push((&body[at + 1..], quiet, false));
|
||||
in_let = let_at(at + 1);
|
||||
}
|
||||
'=' if body[at + 1..].starts_with('>') => found.push((&body[at + 2..], quiet.or(in_let), false)),
|
||||
'r' if at >= inner_closure_end
|
||||
&& body[at..].starts_with("return")
|
||||
&& !body[..at].ends_with(ident)
|
||||
&& !body[at + "return".len()..].starts_with(ident) =>
|
||||
{
|
||||
found.push((&body[at + "return".len()..], None, true));
|
||||
}
|
||||
'"' => loop {
|
||||
match chars.next() {
|
||||
Some((_, '\\')) => {
|
||||
@@ -5052,6 +5163,69 @@ fn closure_returns(body: &str) -> Vec<&str> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// The values the closure returns: each `return`, and each expression that ends its block
|
||||
// rather than a `;` (a block's start, which spans the whole block, aside).
|
||||
let returned: Vec<String> = found
|
||||
.iter()
|
||||
.filter(|&&(start, held, by_return)| {
|
||||
let tail = !start.trim_start().starts_with('{') && !start[statement(start).len()..].starts_with(';');
|
||||
held.is_none() && (by_return || tail)
|
||||
})
|
||||
.map(|(start, ..)| without_format_args(&without_strings(statement(start))))
|
||||
.collect();
|
||||
let used = |name: &str| !name.is_empty() && returned.iter().any(|value| holds_ascii_word(value, name));
|
||||
found.into_iter().filter(|&(_, held, _)| held.is_none_or(used)).map(|(start, ..)| start).collect()
|
||||
}
|
||||
|
||||
/// `code`, its string literals already emptied, with the arguments of each `format!(…)` removed:
|
||||
/// a value written into a format string is part of a message, not one of its own.
|
||||
fn without_format_args(code: &str) -> String {
|
||||
let (mut out, mut rest) = (String::new(), code);
|
||||
while let Some(at) = rest.find("format!(") {
|
||||
out.push_str(&rest[..at + "format!(".len()]);
|
||||
rest = &rest[at + "format!(".len()..];
|
||||
let mut depth = 0usize;
|
||||
let end = rest.char_indices().find_map(|(i, c)| match c {
|
||||
'(' | '[' | '{' => {
|
||||
depth += 1;
|
||||
None
|
||||
}
|
||||
')' | ']' | '}' => match depth.checked_sub(1) {
|
||||
Some(d) => {
|
||||
depth = d;
|
||||
None
|
||||
}
|
||||
None => Some(i),
|
||||
},
|
||||
_ => None,
|
||||
});
|
||||
rest = &rest[end.unwrap_or(rest.len())..];
|
||||
}
|
||||
out.push_str(rest);
|
||||
out
|
||||
}
|
||||
|
||||
/// `code` with the text of its string literals removed, the quotes kept.
|
||||
fn without_strings(code: &str) -> String {
|
||||
let (mut out, mut chars) = (String::new(), code.chars());
|
||||
while let Some(c) = chars.next() {
|
||||
out.push(c);
|
||||
if c == '"' {
|
||||
loop {
|
||||
match chars.next() {
|
||||
Some('\\') => {
|
||||
chars.next();
|
||||
}
|
||||
Some('"') => {
|
||||
out.push('"');
|
||||
break;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
@@ -6135,6 +6135,14 @@ theme Appearance Görünüm
|
||||
JavaScript: JavaScript:
|
||||
Large Gaps Büyük Boşluklar
|
||||
Last Artboard Son Çalışma Yüzeyi
|
||||
Status Bar Show Durum Çubuğunda Göster
|
||||
Artboard Name Çalışma Yüzeyi Adı
|
||||
Current Tool Geçerli Araç
|
||||
Date and Time Tarih ve Saat
|
||||
Number of Undos Geri Alma Sayısı
|
||||
Document Color Profile Belge Renk Profili
|
||||
Undos Geri Al
|
||||
Redos Yinele
|
||||
Levels Düzeyler
|
||||
Library Panel Options Kütüphane Paneli Seçenekleri
|
||||
Link JavaScript Files… JavaScript Dosyalarını Bağla…
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1417,6 +1417,14 @@ file-filter Templates Шаблони
|
||||
Previous Artboard Попередня монтажна область
|
||||
Next Artboard Наступна монтажна область
|
||||
Last Artboard Остання монтажна область
|
||||
Status Bar Show Показ у рядку стану
|
||||
Artboard Name Назва монтажної області
|
||||
Current Tool Поточний інструмент
|
||||
Date and Time Дата й час
|
||||
Number of Undos Кількість скасувань
|
||||
Document Color Profile Колірний профіль документа
|
||||
Undos Скасування
|
||||
Redos Повтори
|
||||
Go To Link Перейти за посиланням
|
||||
GPL palettes keep solid colours only, as RGB. Палітри GPL зберігають лише суцільні кольори RGB.
|
||||
GPU Performance Продуктивність GPU
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1653,6 +1653,14 @@ file-filter Templates Mẫu
|
||||
Previous Artboard Bảng vẽ trước
|
||||
Next Artboard Bảng vẽ tiếp theo
|
||||
Last Artboard Bảng vẽ cuối cùng
|
||||
Status Bar Show Hiển thị trên thanh trạng thái
|
||||
Artboard Name Tên bảng vẽ
|
||||
Current Tool Công cụ hiện tại
|
||||
Date and Time Ngày và giờ
|
||||
Number of Undos Số lần hoàn tác
|
||||
Document Color Profile Hồ sơ màu của tài liệu
|
||||
Undos Hoàn tác
|
||||
Redos Làm lại
|
||||
Go To Link Đến liên kết
|
||||
Good Tốt
|
||||
GPL palettes keep solid colours only, as RGB. Bảng màu GPL chỉ lưu màu đặc theo RGB.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 205 and column 5.
|
@@ -1352,6 +1352,14 @@ file-filter Templates 模板
|
||||
Previous Artboard 上一个画板
|
||||
Next Artboard 下一个画板
|
||||
Last Artboard 最后一个画板
|
||||
Status Bar Show 状态栏显示
|
||||
Artboard Name 画板名称
|
||||
Current Tool 当前工具
|
||||
Date and Time 日期和时间
|
||||
Number of Undos 撤销次数
|
||||
Document Color Profile 文档颜色配置文件
|
||||
Undos 撤销
|
||||
Redos 重做
|
||||
Go To Link 转到链接
|
||||
GPL palettes keep solid colours only, as RGB. GPL 调板仅保留为 RGB 的纯色。
|
||||
GPU Performance GPU 性能
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1387,6 +1387,14 @@ file-filter Templates 範本
|
||||
Previous Artboard 上一個工作區域
|
||||
Next Artboard 下一個工作區域
|
||||
Last Artboard 最後一個工作區域
|
||||
Status Bar Show 狀態列顯示
|
||||
Artboard Name 工作區域名稱
|
||||
Current Tool 目前工具
|
||||
Date and Time 日期和時間
|
||||
Number of Undos 復原次數
|
||||
Document Color Profile 文件色彩描述檔
|
||||
Undos 復原
|
||||
Redos 重做
|
||||
Go To Link 前往連結
|
||||
GPL palettes keep solid colours only, as RGB. GPL 調色盤僅保留純色,並以 RGB 儲存。
|
||||
GPU Performance GPU 效能
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -455,6 +455,7 @@ fn run_ui_part5(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"view.goToArtboard" => go_to_artboard(app, p),
|
||||
"view.statusBarShow" => crate::chrome::set_status_bar_show(app, p),
|
||||
// The navigator's four buttons as commands of their own, so Edit › Keyboard Shortcuts lists
|
||||
// them (view.goToArtboard needs an index, so a key alone can't run it).
|
||||
"view.firstArtboard" => go_to_artboard(app, &json!({ "index": "first" })),
|
||||
|
||||
@@ -63,6 +63,7 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
|
||||
"view.rotate" => app.view().zip(p.get("angle").and_then(Value::as_f64)).is_some_and(|(view, a)| (view.rotation - a).abs() < 1e-9),
|
||||
"window.control" => app.ui.control_bar,
|
||||
"window.helpBar" => app.ui.help_bar,
|
||||
"view.statusBarShow" => p.get("show").and_then(Value::as_str) == Some(app.ui.status_bar_show.param()),
|
||||
"window.control.dock" => p.get("edge").and_then(Value::as_str) == Some(crate::chrome::control_bar_placement(&app.ui)),
|
||||
"window.control.options" => p
|
||||
.get("group")
|
||||
|
||||
@@ -433,6 +433,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
),
|
||||
("view.nextArtboard", "Next Artboard", "", "{} the navigator's Next Artboard button: view.goToArtboard {index: \"next\"} → {index}"),
|
||||
("view.lastArtboard", "Last Artboard", "", "{} the navigator's Last Artboard button: view.goToArtboard {index: \"last\"} → {index}"),
|
||||
(
|
||||
"view.statusBarShow",
|
||||
"Status Bar Show",
|
||||
"",
|
||||
"{show: \"artboardName\" | \"currentTool\" | \"dateTime\" | \"undos\" | \"colorProfile\"} the status bar's Show menu (a click on its info field): the field shows the name of the artboard the navigator shows, the tool in use (the default), the date and time (local on the desktop, UTC on the web), the active document's undo and redo step counts, or the profile its colors use (the one assigned for its color mode, else, or when that one is not available, that mode's working space); kept with the UI state; any other value is an error → show",
|
||||
),
|
||||
("view.edges", "Hide Edges", "Cmd+H", "{}"),
|
||||
("view.artboards", "Hide Artboards", "Cmd+Shift+H", "{}"),
|
||||
(
|
||||
|
||||
@@ -550,6 +550,10 @@ pub struct UiState {
|
||||
#[serde(default)]
|
||||
pub toolbar_right: bool,
|
||||
pub status_bar: bool,
|
||||
/// What the status bar's info field shows (its Show menu, `view.statusBarShow`): Current Tool by
|
||||
/// default and in state saved without it ([`crate::chrome::deserialize_status_bar_show`]).
|
||||
#[serde(default, deserialize_with = "crate::chrome::deserialize_status_bar_show")]
|
||||
pub status_bar_show: crate::chrome::StatusBarShow,
|
||||
pub help_bar: bool,
|
||||
pub dock: bool,
|
||||
/// The bars that Tab (`window.dock`) hid, which the next Tab shows again; none when Tab has not
|
||||
@@ -1177,6 +1181,7 @@ impl Default for UiState {
|
||||
tools_beside_canvas: false,
|
||||
toolbar_right: false,
|
||||
status_bar: true,
|
||||
status_bar_show: crate::chrome::StatusBarShow::CurrentTool,
|
||||
help_bar: true,
|
||||
dock: true,
|
||||
tab_hid: None,
|
||||
|
||||
+16
@@ -6623,6 +6623,22 @@ Reset Rotate View is the command `view.rotateReset`:
|
||||
the angle the selected objects share (`selectionRotation` in `document.inspect`, 0 when their angles differ), so their
|
||||
bounding box is square to the window. It returns `{angle}`, and with nothing selected it returns "nothing selected".
|
||||
|
||||
`view.statusBarShow` (desktop app, the Show menu that a click on the status bar's info field opens) chooses what that
|
||||
field shows, with `show` set to one of these values:
|
||||
|
||||
- `artboardName`: the name of the artboard the status bar's navigator shows.
|
||||
- `currentTool`: the name of the tool in use. This is the default.
|
||||
- `dateTime`: the date and time, as `2026-11-15 14:03`. The desktop app shows local time, and the web build shows UTC
|
||||
with ` UTC` after it.
|
||||
- `undos`: the active document's undo and redo step counts, as `Undos: 3 Redos: 1`.
|
||||
- `colorProfile`: the profile the active document's colors use. That is the profile assigned for its color mode
|
||||
(Edit › Assign Profile), or that mode's working space (Edit › Color Settings) when none is assigned or the
|
||||
assigned one is not available.
|
||||
|
||||
It returns the value set. The choice is kept with the UI state. A missing or other `show` fails with
|
||||
``` `show` must be one of artboardName, currentTool, dateTime, undos, colorProfile ``` and leaves the choice as it was.
|
||||
Without a document, the readouts that need one show a dash.
|
||||
|
||||
The Smart Guides preferences (Preferences › Smart Guides) apply to `pointer_gesture` with Smart Guides on (the
|
||||
default view) and to the mouse; they change what the tools show and how far a target pulls, and only Construction
|
||||
Guides change where a snapped point lands:
|
||||
|
||||
@@ -456,6 +456,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
|
||||
| Workspaces (save, reset, switch); each remembers the layout changes made in it across switches and restarts until Reset <name> | done | `UiState::workspace_changes` |
|
||||
| Opening a file: after 0.4 s a progress window with the file name, a moving bar (reading counts no steps), how many more files wait, Cancel (Esc: the file doesn't open) and Continue in Background; never over another dialog or progress window | done | #5930; `opening::show`, `file.cancelOpen`, `file.openInBackground`. Save, Place and Close still show only the status bar's spinner (Place and Save run off the UI thread; Close is immediate) |
|
||||
| Window › Help Bar shows or hides the tool-help strip apart from the status bar; kept with the preferences and in saved workspaces; Presentation Mode hides both bars | done | `window.helpBar`; `help_bar_toggle_is_independent_and_saved_in_workspaces`, `help_bar_and_status_bar_render_independently`, `legacy_help_bar_visibility_follows_each_layouts_status_bar` |
|
||||
| Status bar: a click on the info field opens a Show menu that chooses its readout: Artboard Name, Current Tool (the default), Date and Time, Number of Undos (undo and redo counts) or Document Color Profile; kept with the UI state | done | `view.statusBarShow`; [public behavior reference](https://web.archive.org/web/2014/https://helpx.adobe.com/illustrator/using/workspace-basics.html); `the_status_bar_info_field_shows_the_chosen_readout`, `a_click_on_the_info_field_opens_the_show_menu`, `the_status_bar_show_choice_is_checked_and_saved`. The menu labels are VectorCraft's own |
|
||||
| Background Export: a progress dialog with the file name, the artboard count and a bar (busy for one artboard and for formats made in one go), Cancel (Esc) and Continue in Background; it opens after 0.4 s and never over another dialog. A cancelled export writes no file | done | #2564; on the shared `vectorcraft_doc::cancel` token (#2546), so a cancelled render keeps no half-drawn pattern tile or effect raster. The bar moves between artboards, not within one. Save As, Export for Screens and Export Selection don't show it yet |
|
||||
| Swatch, graphic style and symbol library panels: each library opens in a panel of its own, several at once, that floats, groups, docks and closes like any panel; opening an open library brings its panel forward; Previous / Next Library change the library in place; the panel menu's Persistent reopens the panel at startup with its view and find field (a library loaded from a file is read again; when its file is missing the panel reopens showing the file's path with Locate Library…, and the status bar names the file) | done | #2603; panel ids `library:<kind>:<id>`, `ui.libraryPanel {panel, library?, view?, showFind?, persistent?}`, `ui.libraryPanel.locate {panel, path?}`; the library panels have no sort option, as before |
|
||||
| DXF export/import preserves intended curve closure | done | #1299; R13–2018 closed SPLINE bit set/read; an open curve ending at its start stays open; knots and control points unchanged; circular cubic paths export as native CIRCLE/ARC where stroke semantics permit; DXF Options' Preset menu and `document.exportDxf`'s `dxfPreset: "cnc"` set the CNC / CAM options (#5500) |
|
||||
|
||||
Reference in New Issue
Block a user