diff --git a/crates/render/src/frame.rs b/crates/render/src/frame.rs index 000995762..81b719893 100644 --- a/crates/render/src/frame.rs +++ b/crates/render/src/frame.rs @@ -1,6 +1,6 @@ use super::options::{AntiAlias, RenderOptions}; use super::threads::single_threaded_context; -use super::{Affine, Arc, BezPath, Document, FillRule, HashMap, Ink, Node, Rect, RenderContext, SymbolBoxes, TextObject, live}; +use super::{Affine, Arc, BezPath, Document, FillRule, HashMap, Ink, Node, Rect, RenderContext, StrokeLayer, SymbolBoxes, TextObject, live}; /// Hasher for pointer-keyed caches: a multiply-xorshift (SipHash showed up in frame profiles). #[derive(Default, Clone, Copy)] @@ -145,6 +145,16 @@ impl Frame<'_> { (self.opts.anti_alias == AntiAlias::Type).then(|| self.view * t.xf) } + /// Stroke `st` as this frame draws it: a copy without its dashes when the frame draws dashed + /// lines as solid lines ([`RenderOptions::solid_dashes`]), else `st` itself. + pub(crate) fn stroke<'s>(&self, st: &'s StrokeLayer) -> std::borrow::Cow<'s, StrokeLayer> { + if self.opts.solid_dashes && st.dash.is_some() { + std::borrow::Cow::Owned(StrokeLayer { dash: None, ..st.clone() }) + } else { + std::borrow::Cow::Borrowed(st) + } + } + /// A render context for drawing this frame's art offscreen (on the calling thread), with its /// edge smoothing: hard edges must not depend on where the art is drawn. pub(crate) fn offscreen_context(&self, w: u16, h: u16) -> RenderContext { diff --git a/crates/render/src/fx/pixels.rs b/crates/render/src/fx/pixels.rs index e89f2bdda..a49817cd0 100644 --- a/crates/render/src/fx/pixels.rs +++ b/crates/render/src/fx/pixels.rs @@ -382,7 +382,7 @@ impl Renderer { } /// The inputs a raster of `n`'s content, which reads `reads`, is drawn with in frame `f`: the - /// color settings, the edge smoothing, the document's raster effects resolution (which a + /// color settings, the edge smoothing, whether dashed strokes draw as solid lines, the document's raster effects resolution (which a /// Rasterize or De-Interlace anywhere in the content follows), the knockout `n` inherits and, /// while `n` holds the isolated group, isolation mode; the pattern definitions, symbols and /// brush library it reads; for images and placed documents their pixels and files, the @@ -398,6 +398,8 @@ impl Renderer { (doc.cms, f.opts.anti_alias.threshold(), self.knockout, f.doc.raster_effects_ppi.to_bits()).hash(&mut h); // Cached effects on containers can include paths made into guides. f.opts.hide_guides.hash(&mut h); + // Cached effects can include dashed strokes drawn as solid lines. + f.opts.solid_dashes.hash(&mut h); if let Some((isolated, path)) = self.isolation_path(f) && n.id != isolated && path.contains(&n.id) @@ -640,6 +642,7 @@ impl Renderer { let outline = match item { AppearanceItem::Fill(_) => ig.clone(), AppearanceItem::Stroke(st) => { + let st = &*f.stroke(st); let w = effects::stroke::aligned_width(st, effects::stroke::is_closed(ig)); effects::stroke::line_outline(&effects::stroke::stroke_pieces(ig, st).line, st, w, f.px * 0.25) } diff --git a/crates/render/src/options.rs b/crates/render/src/options.rs index 595f9797b..93d0f07b9 100644 --- a/crates/render/src/options.rs +++ b/crates/render/src/options.rs @@ -134,6 +134,9 @@ pub struct RenderOptions { /// Appearance of Black's Rich Black: pure CMYK black (black ink alone at 100%) shows as RGB /// black. Off, it shows through the CMYK profile, a dark grey, as other colours do. pub rich_black: bool, + /// Dashed strokes draw as solid lines, without their dashes (the Navigator's Draw Dashed Lines + /// as Solid Lines). Off, they draw their dashes. + pub solid_dashes: bool, } /// The opacity of the art around an isolated group or layer. @@ -219,6 +222,7 @@ impl Default for RenderOptions { interacting: false, dragging: false, rich_black: false, + solid_dashes: false, } } } diff --git a/crates/render/src/renderer/paint.rs b/crates/render/src/renderer/paint.rs index e39f106e8..00d9153c1 100644 --- a/crates/render/src/renderer/paint.rs +++ b/crates/render/src/renderer/paint.rs @@ -97,6 +97,13 @@ impl Renderer { chars: Option<(&Arc, usize)>, bounds: Rect, ) { + // A stroke drawn without its dashes is a copy, not the document's stroke: its outline is + // made here and not kept with the document's outlines. + let drawn = f.stroke(st); + let (st, chars) = match &drawn { + std::borrow::Cow::Owned(solid) => (solid, None), + std::borrow::Cow::Borrowed(st) => (*st, chars), + }; let pieces = effects::stroke::stroke_pieces(bp, st); // The line and its heads overlap: they take the opacity once, in one layer, instead of // each folding it into its paint. diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index a723c4da8..e171eecf6 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1292,6 +1292,43 @@ fn invalid_dash_pattern_strokes_solid() { } } +/// With `solid_dashes` a dashed stroke draws as the same stroke without dashes, on its own and as +/// the silhouette of a drop shadow; without it the dashes stay. +#[test] +fn solid_dashes_draw_dashed_strokes_as_solid_lines() { + let line = |dash: bool, shadow: bool| { + let mut d = Document::new(100.0, 100.0); + let mut bp = BezPath::new(); + bp.move_to((10.0, 50.0)); + bp.line_to((90.0, 50.0)); + let id = d.alloc_id(); + let mut n = Node::path(id, vectorcraft_geom::PathData::from_bezpath(&bp), Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 6.0)); + let st = n.appearance.stroke_mut().unwrap(); + st.dash = dash.then(|| vectorcraft_doc::Dash { pattern: vec![8.0, 8.0], ..Default::default() }); + if shadow { + let params = serde_json::json!({"x": 4, "y": 4, "blur": 1, "opacity": 100}); + st.effects.push(vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params, visible: true }); + } + let l = d.layers[0].id; + d.insert(Some(l), 0, n).unwrap(); + d + }; + let opts = |solid_dashes: bool| RenderOptions { background: Some([255, 255, 255, 255]), solid_dashes, ..Default::default() }; + let draw = |d: &Document, solid: bool| Renderer::new().render(d, 100, 100, Affine::IDENTITY, &opts(solid)); + for shadow in [false, true] { + let solid = draw(&line(false, shadow), false); + assert_ne!(draw(&line(true, shadow), false).pixels, solid.pixels, "the dashes show without the option (shadow: {shadow})"); + assert_eq!(draw(&line(true, shadow), true).pixels, solid.pixels, "the dashes go with the option (shadow: {shadow})"); + assert_eq!(draw(&line(false, shadow), true).pixels, solid.pixels, "a solid stroke is unchanged (shadow: {shadow})"); + } + // One renderer that draws the shadow both ways makes its raster again for each setting. + let (dashed, mut r) = (line(true, true), Renderer::new()); + let fresh = |solid: bool| draw(&dashed, solid).pixels; + for solid in [false, true, false] { + assert_eq!(r.render(&dashed, 100, 100, Affine::IDENTITY, &opts(solid)).pixels, fresh(solid), "reused renderer, solid: {solid}"); + } +} + /// An image that can't be decoded is counted on the thread that tried to decode it: an export /// compares [`decode_failures`] before and after it draws, so another thread's failure meanwhile is /// not its own. diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 67219de40..250f18647 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -63,6 +63,7 @@ pub mod live_paint_selection; pub mod missing_fonts; pub mod missing_links; pub(crate) mod modal; +pub mod navigator_panel_options; pub mod new_brush; pub mod new_color_group; mod new_document; @@ -405,6 +406,7 @@ registry! { PerspectivePlane: [perspective_plane::KIND] => perspective_plane::SPEC, LayerOptions: [layer_options::KIND] => layer_options::SPEC, LayersPanelOptions: [layers_panel_options::KIND] => layers_panel_options::SPEC, + NavigatorPanelOptions: [navigator_panel_options::KIND] => navigator_panel_options::SPEC, PathfinderOptions: [pathfinder_options::KIND] => pathfinder_options::SPEC, RearrangeArtboards: [rearrange_artboards::KIND] => rearrange_artboards::SPEC, PathToolOptions: [crate::addons::pathtools::KIND] => crate::addons::pathtools::OPTIONS_SPEC, diff --git a/crates/ui-egui/src/dialogs/navigator_panel_options.rs b/crates/ui-egui/src/dialogs/navigator_panel_options.rs new file mode 100644 index 000000000..c3582ae59 --- /dev/null +++ b/crates/ui-egui/src/dialogs/navigator_panel_options.rs @@ -0,0 +1,101 @@ +//! Navigator panel › Panel Options…: the color the view box is drawn in, a preset or a custom +//! color, and Draw Dashed Lines as Solid Lines for the thumbnail. +//! +//! Fields: `color` (`#rrggbb`, or a preset's name such as "Light Red") and `solidDashes`. OK runs +//! `ui.navigatorPanelOptions` with them (kept with the UI state). + +use serde_json::{Value, json}; +use vectorcraft_color::Color; + +use super::swatch_options::{grid, label}; +use super::{DialogSpec, run_and_close}; +use crate::panels::navigator::{VIEW_BOX_RED, resolve, view_box_colors}; +use crate::state::Dialog; +use crate::{VectorcraftApp, widgets}; + +/// The dialog kind of the Navigator Panel Options. +pub const KIND: &str = "navigatorPanelOptions"; + +pub(super) const SPEC: DialogSpec = + DialogSpec { heading: |_| tl!("Navigator Panel Options").into(), body, confirm, min_width: 300.0, ..DialogSpec::FORM }; + +/// `ui.navigatorPanelOptions` without options: open the dialog on the panel's options. +pub fn open(app: &mut VectorcraftApp) -> Result { + let o = &app.ui.navigator_panel; + app.ui.dialog = Some(Dialog::new(KIND, json!({ "color": o.color, "solidDashes": o.solid_dashes }))); + Ok(Value::Null) +} + +fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { + let [r, g, b] = resolve(&d.str("color")).unwrap_or(VIEW_BOX_RED); + let presets: Vec<(&str, [u8; 3])> = view_box_colors().collect(); + let preset = presets.iter().position(|(_, c)| *c == [r, g, b]); + let names: Vec<&str> = presets.iter().map(|(n, _)| *n).chain(["Custom"]).collect(); + grid(ui, |ui| { + label(ui, tl!("Color:")); + ui.horizontal(|ui| { + if let Some((_, [r, g, b])) = + widgets::dropdown(ui, "navigator-view-box", names[preset.unwrap_or(presets.len())], &names, 140.0).and_then(|i| presets.get(i)) + { + d.fields.insert("color".into(), json!(Color::rgb8(*r, *g, *b).to_hex())); + } + // Any other color (the preset list shows Custom then). + let mut rgb = [r, g, b]; + if ui.color_edit_button_srgb(&mut rgb).changed() { + d.fields.insert("color".into(), json!(Color::rgb8(rgb[0], rgb[1], rgb[2]).to_hex())); + } + }); + ui.end_row(); + }); + ui.add_space(8.0); + let solid = d.bool("solidDashes"); + if widgets::check(ui, tl!("Draw Dashed Lines as Solid Lines"), solid, true) { + d.fields.insert("solidDashes".into(), json!(!solid)); + } + false +} + +fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + let params = json!({ "color": d.fields.get("color").cloned().unwrap_or(Value::Null), "solidDashes": d.bool("solidDashes") }); + run_and_close(app, "ui.navigatorPanelOptions", params) +} + +#[cfg(test)] +mod tests { + use super::*; + use vectorcraft_engine::Session; + + /// The dialog opens on the panel's options (Light Red and solid dashes off by default), and OK + /// sets a preset's or a custom color and Draw Dashed Lines as Solid Lines, kept with the UI + /// state; a value that is not a color is refused and leaves the options as they were. + #[test] + fn panel_options_set_the_view_box_color_and_solid_dashes() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("ui.navigatorPanelOptions", json!({})).unwrap(); + let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!((d.kind.as_str(), d.str("color"), d.bool("solidDashes")), (KIND, "#ff3030".to_string(), false)); + // Drawn headlessly in the shared frame: the preset's name shows. + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut out = ctx.run_ui(Default::default(), |ui| super::super::show(&mut app, ui.ctx())); + out.textures_delta.clear(); + let set = |app: &mut VectorcraftApp, color: &str, solid: bool| { + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("color".into(), json!(color)); + d.fields.insert("solidDashes".into(), json!(solid)); + super::super::confirm(app) + }; + assert_eq!(set(&mut app, "Green", true).unwrap(), json!({"color": "#4fff4f", "solidDashes": true})); + assert!(app.ui.dialog.is_none()); + app.run("ui.navigatorPanelOptions", json!({})).unwrap(); + set(&mut app, "#0000FF", true).unwrap(); + assert_eq!(app.ui.navigator_panel.color, "#0000ff"); + app.run("ui.navigatorPanelOptions", json!({})).unwrap(); + assert!(set(&mut app, "nope", false).is_err()); + assert!(app.ui.navigator_panel.solid_dashes && app.ui.navigator_panel.color == "#0000ff", "a refused value changes nothing"); + let back: crate::UiState = serde_json::from_value(serde_json::to_value(&app.ui).unwrap()).unwrap(); + assert_eq!(back.navigator_panel, app.ui.navigator_panel, "kept with the UI state"); + let old: crate::UiState = serde_json::from_value(json!({})).unwrap(); + assert_eq!(old.navigator_panel, crate::panels::navigator::PanelOptions::default(), "UI state saved without them"); + } +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index cce9ebd4e..4f7feb3ee 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1702,6 +1702,7 @@ font-technology Variable Променливи Ligatures are off while the text is tracked. Лигатурите са изключени, докато текстът е с проследяване. Light Светло Light Blue Светлосиньо + Light Red Светлочервено Lighten Изсветляване Lightness Светлина Limit Color Guide to Library Ограничаване на цветния справочник до библиотека @@ -1920,6 +1921,8 @@ font-technology Variable Променливи Name: Име: Navigate Навигация Navigator Навигатор + Navigator Panel Options Опции за панела „Навигатор“ + Draw Dashed Lines as Solid Lines Изчертаване на прекъснатите линии като плътни Needs the desktop app Изисква десктоп приложение Neutral Gradients Неутрални градиенти New Нов diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 098c02cd4..fa7f049f6 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -683,6 +683,8 @@ Update Changed Links Ažuriraj promijenjene veze Magic Wand Čarobni štapić Navigator Navigator + Navigator Panel Options Opcije panela navigatora + Draw Dashed Lines as Solid Lines Crtaj isprekidane linije kao pune Pattern Options Opcije uzorka Properties Svojstva Separations Preview Pregled separacija @@ -2396,6 +2398,7 @@ digits Malayalam Malajalam “{file}” is protected by a password. „{file}” je zaštićen lozinkom. Dim Images to: Zatamni slike na: Light Blue Svijetloplava + Light Red Svijetlocrvena Layers Panel Options Opcije panela slojeva Row Size: Veličina reda: Show Layers Only Prikaži samo slojeve diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 93fe3cb2b..05d058dbc 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -2166,6 +2166,7 @@ digits Malayalam Malaiàlam Light Clar Light behind the object Llum darrere de l’objecte Light Blue Blau clar + Light Red Vermell clar Light Direction Direcció de la llum Light Gray Gris clar Light Intensity Intensitat de la llum @@ -2495,6 +2496,8 @@ digits Malayalam Malaiàlam Names the script builds as it runs: {count} Noms que l’script crea mentre s’executa: {count} Navigate Navega Navigator Navegador + Navigator Panel Options Opcions del panell Navegador + Draw Dashed Lines as Solid Lines Dibuixa les línies discontínues com a contínues Needs a choice Necessita una tria Needs a key Necessita una clau Needs Attention Only Només necessita atenció diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 3d287457e..03c6020b8 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -691,6 +691,8 @@ Update Changed Links Aktualizovat změněná propojení Magic Wand Kouzelná hůlka Navigator Navigátor + Navigator Panel Options Možnosti panelu Navigátor + Draw Dashed Lines as Solid Lines Kreslit čárkované čáry jako plné Pattern Options Možnosti vzorku Properties Vlastnosti Separations Preview Náhled separací @@ -2426,6 +2428,7 @@ digits Malayalam Malajálamské “{file}” is protected by a password. Soubor „{file}“ je chráněn heslem. Dim Images to: Ztlumit obrázky na: Light Blue Světle modrá + Light Red Světle červená Layers Panel Options Možnosti panelu Vrstvy Row Size: Velikost řádků: Show Layers Only Zobrazit pouze vrstvy diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index e45c0a364..37cfc9647 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -640,6 +640,8 @@ Update Changed Links Opdatér ændrede links Magic Wand Magisk tryllestav Navigator Navigator + Navigator Panel Options Indstillinger for navigatorpanelet + Draw Dashed Lines as Solid Lines Tegn stiplede linjer som hele linjer Pattern Options Mønsterindstillinger Properties Egenskaber Separations Preview Separationsforhåndsvisning @@ -2214,6 +2216,7 @@ digits Malayalam Malayalam “{file}” is protected by a password. »{file}« er beskyttet med en adgangskode. Dim Images to: Nedton billeder til: Light Blue Lyseblå + Light Red Lys rød Layers Panel Options Indstillinger for lagpanelet Row Size: Rækkestørrelse: Show Layers Only Vis kun lag diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 8db779ee4..9658d5729 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1764,6 +1764,7 @@ file-filter Templates Vorlagen Ligatures are off while the text is tracked. Ligaturen sind aus, solange für den Text eine Laufweite eingestellt ist. Light Hell Light Blue Hellblau + Light Red Hellrot Lighten Aufhellen Lightness Helligkeit Limit Color Guide to Library Farbhilfe auf Bibliothek beschränken @@ -1982,6 +1983,8 @@ file-filter Templates Vorlagen Name: Name: Navigate Navigieren Navigator Navigator + Navigator Panel Options Optionen für das Navigatorbedienfeld + Draw Dashed Lines as Solid Lines Gestrichelte Linien als durchgezogene Linien zeichnen Needs the desktop app Erfordert die Desktop-App Neutral Gradients Neutrale Verläufe New Neu diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 071628cc8..eca62bdb9 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1652,6 +1652,7 @@ font-technology Variable Μεταβλητές Libraries Βιβλιοθήκες Light Ανοιχτό Light Blue Ανοιχτό μπλε + Light Red Ανοιχτό κόκκινο Lighten Φώτισμα Lightness Φωτεινότητα Line Γραμμή @@ -1696,6 +1697,8 @@ font-technology Variable Μεταβλητές Name Όνομα Name: Όνομα: Navigator Πλοήγηση + Navigator Panel Options Επιλογές πίνακα πλοήγησης + Draw Dashed Lines as Solid Lines Σχεδίαση διακεκομμένων γραμμών ως συνεχών New Νέο Scatter: Διασπορά: Rotation: Περιστροφή: diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index c63c6a07b..e739c742c 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1767,6 +1767,7 @@ font-technology Variable Variables Ligatures are off while the text is tracked. Las ligaduras se desactivan mientras el texto tiene espaciado. Light Claro Light Blue Azul claro + Light Red Rojo claro Lighten Aclarar Lightness Luminosidad Limit Color Guide to Library Limitar guía de color a la biblioteca @@ -1985,6 +1986,8 @@ font-technology Variable Variables Name: Nombre: Navigate Navegar Navigator Navegador + Navigator Panel Options Opciones del panel Navegador + Draw Dashed Lines as Solid Lines Dibujar líneas discontinuas como continuas Needs the desktop app Requiere la aplicación de escritorio Neutral Gradients Degradados neutros New Nuevo diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 03f290305..e27750497 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -682,6 +682,8 @@ Update Changed Links Päivitä muuttuneet linkit Magic Wand Taikasauva Navigator Navigaattori + Navigator Panel Options Navigaattori-paneelin asetukset + Draw Dashed Lines as Solid Lines Piirrä katkoviivat yhtenäisinä viivoina Pattern Options Kuvion asetukset Properties Ominaisuudet Separations Preview Erottelujen esikatselu @@ -2370,6 +2372,7 @@ digits Malayalam Malajalam “{file}” is protected by a password. ”{file}” on suojattu salasanalla. Dim Images to: Himmennä kuvat: Light Blue Vaaleansininen + Light Red Vaalea punainen Layers Panel Options Tasot-paneelin asetukset Row Size: Rivin koko: Show Layers Only Näytä vain tasot diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index a0f15e3cc..bac0097a6 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1761,6 +1761,7 @@ font-technology Variable Variables Ligatures are off while the text is tracked. Les ligatures sont désactivées tant qu’une approche est appliquée au texte. Light Clair Light Blue Bleu clair + Light Red Rouge clair Lighten Éclaircir Lightness Luminosité Limit Color Guide to Library Limiter le guide des couleurs à la bibliothèque @@ -1979,6 +1980,8 @@ font-technology Variable Variables Name: Nom : Navigate Naviguer Navigator Navigation + Navigator Panel Options Options du panneau Navigation + Draw Dashed Lines as Solid Lines Dessiner les pointillés en traits pleins Needs the desktop app Nécessite l’application de bureau Neutral Gradients Dégradés neutres New Nouveau diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index e727d6aba..225fa709f 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2223,6 +2223,7 @@ file-filter Templates Sablonok Ligatures are off while the text is tracked. Ha a szövegnek betűköze van, a ligatúrák ki vannak kapcsolva. Light Fény Light Blue Világoskék + Light Red Világospiros Light Direction Fény iránya Light Gray Világosszürke Light Intensity Fény erőssége @@ -2505,6 +2506,8 @@ file-filter Templates Sablonok Name: Név: Navigate Navigálás Navigator Navigátor + Navigator Panel Options Navigátor panel beállításai + Draw Dashed Lines as Solid Lines Szaggatott vonalak rajzolása folytonosként Needs the desktop app Az asztali alkalmazás kell hozzá Negative Negatív Neutral Gradients Semleges színátmenetek diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index a15eddbb0..d0d09e839 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -640,6 +640,8 @@ Update Changed Links Perbarui tautan yang berubah Magic Wand Tongkat ajaib Navigator Navigator + Navigator Panel Options Opsi panel navigator + Draw Dashed Lines as Solid Lines Gambar garis putus-putus sebagai garis utuh Pattern Options Opsi pola Properties Properti Separations Preview Pratinjau separasi @@ -2214,6 +2216,7 @@ digits Malayalam Malayalam “{file}” is protected by a password. “{file}” dilindungi kata sandi. Dim Images to: Redupkan gambar menjadi: Light Blue Biru muda + Light Red Merah terang Layers Panel Options Opsi panel lapisan Row Size: Ukuran baris: Show Layers Only Tampilkan lapisan saja diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 4f93e7896..a4a7d547a 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1796,6 +1796,7 @@ file-filter Templates Modelli Ligatures are off while the text is tracked. Le legature sono disattivate quando al testo è applicata una spaziatura caratteri. Light Chiaro Light Blue Azzurro + Light Red Rosso chiaro Lighten Schiarisci Lightness Luminosità Limit Color Guide to Library Limita guida colori alla libreria @@ -2014,6 +2015,8 @@ file-filter Templates Modelli Name: Nome: Navigate Naviga Navigator Navigatore + Navigator Panel Options Opzioni pannello Navigatore + Draw Dashed Lines as Solid Lines Disegna le linee tratteggiate come linee continue Needs the desktop app Richiede l'app desktop Neutral Gradients Sfumature neutre New Nuovo diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 806f1bcc3..3d04b70ce 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -692,6 +692,8 @@ digits Malayalam マラヤーラム数字 Links リンク Magic Wand 自動選択 Navigator ナビゲーター + Navigator Panel Options ナビゲーターパネルオプション + Draw Dashed Lines as Solid Lines 破線を実線で表示 Pattern Options パターンオプション Properties プロパティ Separations Preview 分版プレビュー @@ -3617,6 +3619,7 @@ file-filter Templates テンプレート Left-Justified Tab 左揃えタブ Letter レター Light Blue ライトブルー + Light Red ライトレッド Lightness 明度 Link リンク Low 低 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 855a8f6fc..d0c69c179 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -590,6 +590,8 @@ Update Changed Links შეცვლილი ბმულების განახლება Magic Wand ჯადოსნური ჯოხი Navigator ნავიგატორი + Navigator Panel Options ნავიგატორის პანელის პარამეტრები + Draw Dashed Lines as Solid Lines ტირეებიანი ხაზების უწყვეტ ხაზებად დახატვა Pattern Options პატერნის პარამეტრები Properties თვისებები Separations Preview სეპარაციის წინასწარი ნახვა @@ -2117,6 +2119,7 @@ digits Malayalam მალაიალამური “{file}” is protected by a password. „{file}“ დაცულია პაროლით. Dim Images to: გამოსახულებების ჩაბნელება: Light Blue ღია ლურჯი + Light Red ღია წითელი Layers Panel Options შრეების პანელის პარამეტრები Row Size: სტრიქონის ზომა: Show Layers Only მხოლოდ შრეების ჩვენება diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 04006d209..926896ebb 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -685,6 +685,8 @@ digits Malayalam 말라얄람어 Links 링크 Magic Wand 마술봉 Navigator 내비게이터 + Navigator Panel Options 내비게이터 패널 옵션 + Draw Dashed Lines as Solid Lines 점선을 실선으로 그리기 Pattern Options 패턴 옵션 Properties 속성 Separations Preview 분판 미리 보기 @@ -3605,6 +3607,7 @@ file-filter Templates 템플릿 Left-Justified Tab 왼쪽 정렬 탭 Letter 레터 Light Blue 연한 파랑 + Light Red 연한 빨강 Lightness 명도 Link 링크 Low 낮음 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 671f78da3..8e23b89e8 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -654,6 +654,8 @@ Update Changed Links Gewijzigde koppelingen bijwerken Magic Wand Magische toverstaf Navigator Navigator + Navigator Panel Options Opties voor het paneel Navigator + Draw Dashed Lines as Solid Lines Streepjeslijnen als doorgetrokken lijnen tekenen Pattern Options Patroonopties Properties Eigenschappen Separations Preview Scheidingsvoorbeeld @@ -2228,6 +2230,7 @@ digits Malayalam Malayalam “{file}” is protected by a password. „{file}” is beveiligd met een wachtwoord. Dim Images to: Afbeeldingen dimmen tot: Light Blue Lichtblauw + Light Red Lichtrood Layers Panel Options Opties voor het paneel Lagen Row Size: Rijgrootte: Show Layers Only Alleen lagen weergeven diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index cf9c4edf8..6fd390d6d 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -639,6 +639,8 @@ Update Changed Links Aktualizuj zmienione łącza Magic Wand Różdżka Navigator Nawigator + Navigator Panel Options Opcje panelu Nawigator + Draw Dashed Lines as Solid Lines Rysuj linie przerywane jako ciągłe Pattern Options Opcje wzorka Properties Właściwości Separations Preview Podgląd rozbarwień @@ -2341,6 +2343,7 @@ digits Malayalam Malajalam “{file}” is protected by a password. Plik „{file}” jest chroniony hasłem. Dim Images to: Przyciemnij obrazy do: Light Blue Jasnoniebieski + Light Red Jasnoczerwony Layers Panel Options Opcje panelu Warstwy Row Size: Rozmiar wiersza: Show Layers Only Pokaż tylko warstwy diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index d29cac871..331edd6bc 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1676,6 +1676,8 @@ Name: Nome: Navigate Navegar Navigator Navegador + Navigator Panel Options Opções do painel Navegador + Draw Dashed Lines as Solid Lines Desenhar linhas tracejadas como linhas sólidas Needs the desktop app Requer o aplicativo de desktop Neutral Gradients Gradientes Neutros New Novo @@ -4643,6 +4645,7 @@ axis Both Ambos Left-Justified Tab Parada de Tabulação à Esquerda Letter Carta Light Blue Azul Claro + Light Red Vermelho Claro Lightness Luminosidade Link Vincular Low Baixo diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 13948efde..654e0a143 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1793,6 +1793,7 @@ file-filter Templates Шаблоны Ligatures are off while the text is tracked. Лигатуры отключены, пока для текста задан трекинг. Light Светлый Light Blue Голубой + Light Red Светло-красный Lighten Замена светлым Lightness Светлота Limit Color Guide to Library Ограничить цветовой гид библиотекой @@ -2011,6 +2012,8 @@ file-filter Templates Шаблоны Name: Имя: Navigate Навигация Navigator Навигатор + Navigator Panel Options Параметры панели «Навигатор» + Draw Dashed Lines as Solid Lines Рисовать штриховые линии сплошными Needs the desktop app Требуется настольное приложение Neutral Gradients Нейтральные градиенты New Создать diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index babde2822..58a91b888 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -1708,6 +1708,7 @@ font-technology Variable Variabla Ligatures are off while the text is tracked. Ligaturer är avstängda när texten har spärrning. Light Ljus Light Blue Ljusblå + Light Red Ljusröd Lighten Ljusare Lightness Ljushet Limit Color Guide to Library Begränsa färgguiden till bibliotek @@ -1926,6 +1927,8 @@ font-technology Variable Variabla Name: Namn: Navigate Navigera Navigator Navigator + Navigator Panel Options Alternativ för panelen Navigator + Draw Dashed Lines as Solid Lines Rita streckade linjer som heldragna linjer Needs the desktop app Kräver skrivbordsappen Neutral Gradients Neutrala gradienter New Nytt diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index f2850ce92..5f41a5198 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -1604,6 +1604,7 @@ font-technology Variable Değişken Ligatures are off while the text is tracked. Metin izlenirken bitişik harfler kapalıdır. Light Işık Light Blue Açık Mavi + Light Red Açık Kırmızı Lighten hafifletmek Lightness hafiflik Limit Color Guide to Library Renk Kılavuzunu Kitaplıkla Sınırlandır @@ -1815,6 +1816,8 @@ font-technology Variable Değişken Name: İsim: Navigate Gezin Navigator Gezgin + Navigator Panel Options Gezgin Paneli Seçenekleri + Draw Dashed Lines as Solid Lines Kesikli Çizgileri Düz Çizgi Olarak Çiz Needs the desktop app Masaüstü uygulaması gerekiyor Neutral Gradients Nötr Degradeler New Yeni diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 21b77da4b..ad511f3ed 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1790,6 +1790,7 @@ file-filter Templates Шаблони Ligatures are off while the text is tracked. Лігатури вимкнено, поки для тексту заданий трекінг. Light Світлий Light Blue Блакитний + Light Red Світло-червоний Lighten Освітлення Lightness Світлота Limit Color Guide to Library Обмежити колірний гід бібліотекою @@ -2008,6 +2009,8 @@ file-filter Templates Шаблони Name: Назва: Navigate Навігація Navigator Навігатор + Navigator Panel Options Параметри панелі «Навігатор» + Draw Dashed Lines as Solid Lines Малювати штрихові лінії суцільними Needs the desktop app Потрібен настільний додаток Neutral Gradients Нейтральні градієнти New Створити diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index c456663aa..fb24f7d04 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2051,6 +2051,7 @@ digits Hindi Hindi Ligatures are off while the text is tracked. Chữ ghép tắt khi văn bản được giãn chữ. Light Sáng Light Blue Xanh lam nhạt + Light Red Đỏ nhạt Light Direction Hướng ánh sáng Light Gray Xám nhạt Light Intensity Cường độ ánh sáng @@ -2315,6 +2316,8 @@ digits Hindi Hindi Name: Tên: Navigate Dẫn hướng Navigator Bộ dẫn hướng + Navigator Panel Options Tùy chọn bảng bộ dẫn hướng + Draw Dashed Lines as Solid Lines Vẽ đường nét đứt thành đường liền Needs the desktop app Cần ứng dụng máy tính để bàn Neutral Gradients Chuyển sắc trung tính New Mới diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index a55c343f2..bf2a49b37 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1679,6 +1679,7 @@ file-filter Templates 模板 Ligatures are off while the text is tracked. 设置字距时连字会关闭。 Light 浅色 Light Blue 浅蓝色 + Light Red 浅红色 Lighten 变亮 Lightness 亮度 Limit Color Guide to Library 将色彩参考限制于库 @@ -1897,6 +1898,8 @@ file-filter Templates 模板 Name: 名称: Navigate 导航 Navigator 导航器 + Navigator Panel Options 导航器面板选项 + Draw Dashed Lines as Solid Lines 将虚线绘制为实线 Needs the desktop app 需要桌面应用 Neutral Gradients 中性渐变 New 新建 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 3eab26ac6..5f48b4307 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2637,6 +2637,7 @@ file-filter Templates 範本 Ligatures are off while the text is tracked. 文字套用字距時,連字功能會關閉。 Light 淺色 Light Blue 淺藍色 + Light Red 淺紅色 Lighten 變亮 Lightness 亮度 Limit Color Guide to Library 將色彩參考限制於資料庫 @@ -2867,6 +2868,8 @@ file-filter Templates 範本 Name: 名稱: Navigate 導覽 Navigator 導覽器 + Navigator Panel Options 導覽器面板選項 + Draw Dashed Lines as Solid Lines 將虛線繪製為實線 Needs the desktop app 需要桌面版應用程式 Neutral Gradients 中性漸層 New 新增 diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index f54153cbd..c20e3b650 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -694,6 +694,7 @@ fn run_ui_part8(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option crate::dialogs::layer_options::open_new(app, p), "ui.newArtboard" => crate::dialogs::artboard_options::open_new(app), "ui.layersPanelOptions" => crate::dialogs::layers_panel_options::open(app), + "ui.navigatorPanelOptions" => crate::panels::navigator::set_options(app, p), "ui.layersExpand" => crate::panels::layers::expand(app, p), "ui.layersFilter" => crate::panels::layers::set_filter(app, p), "ui.flattenTransparencyDialog" => { diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 3af5f1b9f..5e9c10e35 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -867,6 +867,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{} open the Layers panel's Panel Options: dialog `layersPanelOptions`, fields layersOnly, locateSelected (Locate Selected Objects, off by default: when the last selected object changes outside the panel, on the canvas or by a command, the panel opens the layers and groups around it and scrolls to its row, as after layer.locate, without highlighting the row; when Show Layers Only, a search or the filter leaves the object without a row, the panel opens no rows for it and scrolls as with the option off; a change made during a drag is located when the drag ends), rowSize (small|medium|large|other), otherSize (12–100 pt), thumbLayers, thumbGroups, thumbObjects; OK applies them (kept with the UI state)", ), + ( + "ui.navigatorPanelOptions", + "Panel Options…", + "", + "{color?: #rrggbb or a preset name (Light Red, the default, then the layer colors such as \"Light Blue\"), solidDashes?: bool (Draw Dashed Lines as Solid Lines, off by default: the Navigator's thumbnail draws dashed strokes without their dashes; the canvas, printing and exports keep them)} set the Navigator panel's options, the color its view box is drawn in and solid dashes (kept with the UI state) → {color, solidDashes}; with neither, open its Panel Options (dialog `navigatorPanelOptions`, fields color, solidDashes; OK sets them); an error for a value that is not a color or not true or false, which changes nothing", + ), ( "ui.layersExpand", "Expand Layers Panel Rows", diff --git a/crates/ui-egui/src/panels/navigator.rs b/crates/ui-egui/src/panels/navigator.rs index 2b53ee61a..da8048407 100644 --- a/crates/ui-egui/src/panels/navigator.rs +++ b/crates/ui-egui/src/panels/navigator.rs @@ -1,12 +1,16 @@ //! Navigator panel: a thumbnail of the document (rendered with `Renderer::render_region_with`, //! cached by document revision, off the UI thread for heavy documents and not during drags: see -//! [`crate::thumb_worker`]) with the red view box you drag to pan, zoom out/in buttons, a zoom slider -//! and a zoom field. +//! [`crate::thumb_worker`]) with the view box you drag to pan, zoom out/in buttons, a zoom slider +//! and a zoom field. Panel Options… sets the view box's color and whether the thumbnail draws +//! dashed lines as solid lines ([`PanelOptions`]). use std::sync::Arc; use egui::{Color32, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; -use serde_json::json; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use vectorcraft_color::Color; +use vectorcraft_doc::LAYER_COLORS; use vectorcraft_geom::{Point, Rect as DRect}; use super::{pstate, set_pstate}; @@ -15,7 +19,77 @@ use crate::theme::Tokens; use crate::thumb_worker::{Settle, ThumbWorker}; use crate::widgets::{self, menu_item}; -/// What a navigator image shows: document `uid` at `revision`, `w`×`h` pixels, the artboards only. +/// The view box's color by default (Light Red, the first of [`view_box_colors`]). +pub const VIEW_BOX_RED: [u8; 3] = [0xff, 0x30, 0x30]; + +/// The view box color presets of Panel Options: Light Red, the default, then the layer colors. +pub fn view_box_colors() -> impl Iterator { + std::iter::once(("Light Red", VIEW_BOX_RED)).chain(LAYER_COLORS.iter().copied()) +} + +/// `color` (a preset's name, in any case, or `#rrggbb`) as RGB; `None` for anything else, +/// including a color with an alpha or a short `#rgb`. +pub fn resolve(color: &str) -> Option<[u8; 3]> { + let color = color.trim(); + if let Some((_, rgb)) = view_box_colors().find(|(name, _)| name.eq_ignore_ascii_case(color)) { + return Some(rgb); + } + let hex = color.strip_prefix('#').filter(|h| h.len() == 6 && h.bytes().all(|b| b.is_ascii_hexdigit()))?; + let byte = |at: usize| u8::from_str_radix(hex.get(at..at + 2)?, 16).ok(); + Some([byte(0)?, byte(2)?, byte(4)?]) +} + +/// Navigator panel options (≡ › Panel Options…), kept with the UI state. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct PanelOptions { + /// The view box's color, `#rrggbb`. + pub color: String, + /// Draw Dashed Lines as Solid Lines (off by default): the thumbnail draws dashed strokes + /// without their dashes. The canvas, printing and exports keep the dashes. + pub solid_dashes: bool, +} + +impl Default for PanelOptions { + fn default() -> Self { + let [r, g, b] = VIEW_BOX_RED; + Self { color: Color::rgb8(r, g, b).to_hex(), solid_dashes: false } + } +} + +impl PanelOptions { + /// The color the view box is drawn in: [`Self::color`], or Light Red when it is not a color. + pub fn view_box(&self) -> Color32 { + let [r, g, b] = resolve(&self.color).unwrap_or(VIEW_BOX_RED); + Color32::from_rgb(r, g, b) + } +} + +/// `ui.navigatorPanelOptions {color?, solidDashes?}`: with neither, open Panel Options on the +/// panel's options; otherwise set the ones given and answer the options. +pub fn set_options(app: &mut VectorcraftApp, p: &Value) -> Result { + let (color, solid) = (p.get("color"), p.get("solidDashes")); + if color.is_none() && solid.is_none() { + return crate::dialogs::navigator_panel_options::open(app); + } + let mut o = app.ui.navigator_panel.clone(); + if let Some(c) = color { + let [r, g, b] = c.as_str().and_then(resolve).ok_or_else(|| { + let color = c.as_str().map_or_else(|| c.to_string(), str::to_string); + format!("`{color}` is not a colour (#rrggbb or a preset name)") + })?; + o.color = Color::rgb8(r, g, b).to_hex(); + } + if let Some(s) = solid { + let key = "solidDashes"; + o.solid_dashes = s.as_bool().ok_or_else(|| format!("`{key}` must be true or false"))?; + } + app.ui.navigator_panel = o; + Ok(serde_json::to_value(&app.ui.navigator_panel).unwrap_or_default()) +} + +/// What a navigator image shows: document `uid` at `revision`, `w`×`h` pixels, the artboards only, +/// dashed lines drawn as solid lines. #[derive(Clone, Copy, Debug, PartialEq)] struct ImageKey { uid: u64, @@ -23,6 +97,7 @@ struct ImageKey { w: u32, h: u32, artboard_only: bool, + solid_dashes: bool, } #[derive(Default)] @@ -118,27 +193,44 @@ impl Cache { return; } let s = (key.w as f64 / region.width()).min(key.h as f64 / region.height()); - // As exported (template layers and guides left out), transparent; images far smaller than - // they are sample their mip levels. - let opts = vectorcraft_render::RenderOptions { skip_templates: true, image_mips: true, ..Default::default() }; + let opts = image_options(key); let heavy = self.last_ms > HEAVY_MS && have; if heavy && let Some(w) = &self.worker { let (doc, opts) = (doc.clone(), opts.clone()); - if w.submit((), (key, region), Box::new(move |r| Some(r.render_region_with(&doc, region, s, &opts)))) { + let job = move |r: &mut vectorcraft_render::Renderer| { + if opts.solid_dashes { + SOLID_RENDERER.with(|solid| solid.borrow_mut().render_region_with(&doc, region, s, &opts)) + } else { + r.render_region_with(&doc, region, s, &opts) + } + }; + if w.submit((), (key, region), Box::new(move |r| Some(job(r)))) { self.pending = Some(key); return; } self.worker = None; } let t0 = crate::now_ms(); - let img = NAV_RENDERER.with(|r| r.borrow_mut().render_region_with(doc, region, s, &opts)); + let renderer = if key.solid_dashes { &SOLID_RENDERER } else { &NAV_RENDERER }; + let img = renderer.with(|r| r.borrow_mut().render_region_with(doc, region, s, &opts)); self.upload(ctx, key, region, &img, crate::now_ms() - t0); } } +/// How the image `key` names is drawn: as exported (template layers and guides left out), +/// transparent, with images far smaller than they are sampling their mip levels, and with Draw +/// Dashed Lines as Solid Lines, dashed strokes without their dashes. +fn image_options(key: ImageKey) -> vectorcraft_render::RenderOptions { + vectorcraft_render::RenderOptions { skip_templates: true, image_mips: true, solid_dashes: key.solid_dashes, ..Default::default() } +} + thread_local! { /// Renders light documents' images on the UI thread. static NAV_RENDERER: std::cell::RefCell = std::cell::RefCell::new(vectorcraft_render::Renderer::new()); + /// Renders the images with Draw Dashed Lines as Solid Lines on, on the thread that renders + /// them: each renderer draws every image it makes with one setting, so the art it keeps from + /// one image is drawn the same way in the next. + static SOLID_RENDERER: std::cell::RefCell = std::cell::RefCell::new(vectorcraft_render::Renderer::new()); } /// Map a document point into the thumbnail rect (region → rect, aspect preserved). @@ -157,7 +249,7 @@ pub fn thumb_to_doc(region: DRect, thumb: Rect, p: egui::Pos2) -> Point { Point::new(region.x0 + (p.x as f64 - ox) / s, region.y0 + (p.y as f64 - oy) / s) } -/// The red view box in the thumbnail: the corners of the canvas `canvas` showing view `v`, mapped +/// The view box in the thumbnail: the corners of the canvas `canvas` showing view `v`, mapped /// into the document (turned with the view) and then into the thumbnail, cut to the thumbnail /// grown by a point. Fewer than three points when the view is outside the thumbnail. fn view_box(region: DRect, thumb: Rect, canvas: Rect, v: &crate::state::View) -> Vec { @@ -215,7 +307,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.painter().rect_filled(thumb, 0.0, t.pasteboard); let ppp = ui.ctx().pixels_per_point(); let (pw, ph) = ((w * ppp) as u32, (h * ppp) as u32); - let key = ImageKey { uid, revision, w: pw, h: ph, artboard_only }; + let key = ImageKey { uid, revision, w: pw, h: ph, artboard_only, solid_dashes: app.ui.navigator_panel.solid_dashes }; let (tex, tex_region) = CACHE.with(|c| { let mut c = c.borrow_mut(); c.update(ui.ctx(), &doc, key, region, busy); @@ -240,7 +332,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if let (Some(v), Some(cr)) = (view, app.canvas_rect) { let box_points = view_box(region, thumb, cr, &v); if box_points.len() > 2 { - ui.painter().with_clip_rect(thumb).add(egui::Shape::closed_line(box_points, Stroke::new(1.5, Color32::from_rgb(0xff, 0x30, 0x30)))); + let color = app.ui.navigator_panel.view_box(); + ui.painter().with_clip_rect(thumb).add(egui::Shape::closed_line(box_points, Stroke::new(1.5, color))); } if (resp.dragged() || resp.clicked()) && let Some(p) = resp.interact_pointer_pos() @@ -278,12 +371,14 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); } -pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let ab: bool = pstate(ui.ctx(), "nav-ab-only"); if menu_item(ui, tl!("View Artboard Only"), true, ab) { set_pstate(ui.ctx(), "nav-ab-only", !ab); } - menu_item(ui, tl!("Panel Options…"), false, false); + if menu_item(ui, tl!("Panel Options…"), true, false) { + app.run("ui.navigatorPanelOptions", json!({})).ok(); + } } #[cfg(test)] @@ -354,6 +449,108 @@ mod tests { assert!(points.iter().zip(&want).all(|(a, b)| a.distance(*b) < 0.5), "{points:?} is not {want:?}"); } + /// The panel menu's Panel Options… is enabled, and a click opens the Navigator Panel Options. + #[test] + fn panel_options_opens_from_the_panel_menu() { + use crate::widgets::accessibility_tests::{named, tree}; + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + // Menu rows pad their label where a check mark would go. + assert!(!named(&tree(|ui| menu(&mut app, ui)), " Panel Options…").is_disabled()); + let ctx = egui::Context::default(); + let screen = Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(240.0, 400.0))); + let raw = |events| egui::RawInput { events, screen_rect: screen, ..Default::default() }; + let mut out = ctx.run_ui(raw(vec![]), |ui| menu(&mut app, ui)); + out.textures_delta.clear(); + let item = out + .shapes + .iter() + .find_map(|c| match &c.shape { + egui::Shape::Text(t) if t.galley.text().trim_start() == "Panel Options…" => Some(t.visual_bounding_rect().center()), + _ => None, + }) + .expect("the menu item"); + let button = |pressed| egui::Event::PointerButton { pos: item, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + ctx.run_ui(raw(vec![egui::Event::PointerMoved(item), button(true), button(false)]), |ui| menu(&mut app, ui)).textures_delta.clear(); + assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(crate::dialogs::navigator_panel_options::KIND)); + } + + /// `ui.navigatorPanelOptions` sets the view box color and Draw Dashed Lines as Solid Lines: the + /// view box is drawn in that color (Light Red by default), and the thumbnail is made again with + /// the dashes left out. A value that is not a color, or not true or false, is refused and + /// changes nothing. + #[test] + fn panel_options_set_the_view_box_color_and_solid_dashes() { + use vectorcraft_engine::Session; + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + app.canvas_rect = Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(600.0, 400.0))); + app.run("view.setZoom", json!({"zoom": 400, "center": [200, 150]})).unwrap(); + let ctx = egui::Context::default(); + // The view boxes drawn in `color` in one frame. + let boxes = |app: &mut VectorcraftApp, color: Color32| { + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui)); + out.textures_delta.clear(); + let want = egui::epaint::ColorMode::Solid(color); + out.shapes.into_iter().filter(|c| matches!(&c.shape, egui::Shape::Path(p) if p.closed && p.stroke.color == want)).count() + }; + let (red, blue) = (Color32::from_rgb(0xff, 0x30, 0x30), Color32::from_rgb(0x4f, 0x80, 0xff)); + assert_eq!((boxes(&mut app, red), boxes(&mut app, blue)), (1, 0), "Light Red by default"); + let shown = || CACHE.with(|c| c.borrow().shown.map(|(k, _)| k.solid_dashes)); + assert_eq!(shown(), Some(false)); + let set = app.run("ui.navigatorPanelOptions", json!({"color": "light blue", "solidDashes": true})).unwrap(); + assert_eq!(set, json!({"color": "#4f80ff", "solidDashes": true})); + assert_eq!((boxes(&mut app, red), boxes(&mut app, blue)), (0, 1)); + for _ in 0..12 { + boxes(&mut app, blue); + } + assert_eq!(shown(), Some(true), "the thumbnail is made again with solid dashes"); + let bad = [ + json!({"color": 12}), + json!({"color": "#zz0000"}), + json!({"color": "#ff000080"}), + json!({"color": "#f00"}), + json!({"color": null}), + json!({"color": "Red", "solidDashes": "yes"}), + ]; + for bad in bad { + assert!(app.run("ui.navigatorPanelOptions", bad.clone()).is_err(), "{bad}"); + } + assert_eq!(serde_json::to_value(&app.ui.navigator_panel).unwrap(), json!({"color": "#4f80ff", "solidDashes": true}), "nothing changed"); + app.run("ui.navigatorPanelOptions", json!({"color": "#123456"})).unwrap(); + assert!(app.ui.navigator_panel.solid_dashes, "an option left out keeps its value"); + } + + /// The thumbnail's render options drop a dashed stroke's dashes with Draw Dashed Lines as Solid + /// Lines, so it draws as the same stroke without dashes; without the option the dashes stay. + #[test] + fn the_thumbnail_draws_dashed_lines_as_solid_lines_with_the_option() { + use vectorcraft_color::Paint; + use vectorcraft_doc::{Appearance, Document, Node}; + let line = |dash: bool| { + let mut d = Document::new(100.0, 100.0); + let mut bp = vectorcraft_geom::BezPath::new(); + bp.move_to((10.0, 50.0)); + bp.line_to((90.0, 50.0)); + let id = d.alloc_id(); + let mut n = + Node::path(id, vectorcraft_geom::PathData::from_bezpath(&bp), Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 6.0)); + n.appearance.stroke_mut().unwrap().dash = dash.then(|| vectorcraft_doc::Dash { pattern: vec![8.0, 8.0], ..Default::default() }); + let l = d.layers[0].id; + d.insert(Some(l), 0, n).unwrap(); + d + }; + let key = ImageKey { uid: 1, revision: 1, w: 100, h: 100, artboard_only: false, solid_dashes: false }; + let draw = |d: &Document, key: ImageKey| { + vectorcraft_render::Renderer::new().render_region_with(d, DRect::new(0.0, 0.0, 100.0, 100.0), 1.0, &image_options(key)).pixels + }; + let solid = draw(&line(false), key); + assert_ne!(draw(&line(true), key), solid, "the dashes show without the option"); + let on = ImageKey { solid_dashes: true, ..key }; + assert!(image_options(on).solid_dashes && !image_options(key).solid_dashes); + assert_eq!(draw(&line(true), on), solid, "the dashes go with the option"); + assert_eq!(draw(&line(false), on), solid, "a solid stroke draws as it did"); + } + #[test] fn zoom_slider_roundtrip() { assert!((zoom_to_slider(3.13)).abs() < 1e-9); diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 294b5bb4d..84cf014c8 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -645,6 +645,9 @@ pub struct UiState { /// Layers panel › Panel Options… (row size, thumbnails, Show Layers Only, Locate Selected Objects). #[serde(default)] pub layers_panel: crate::panels::layers::PanelOptions, + /// Navigator panel › Panel Options… (the view box color, Draw Dashed Lines as Solid Lines). + #[serde(default)] + pub navigator_panel: crate::panels::navigator::PanelOptions, /// The Layers panel's Filter (`ui.layersFilter`). It is not saved and lasts the session, as the /// Search All text does. #[serde(skip)] @@ -1007,6 +1010,7 @@ impl Default for UiState { home: None, trace_hold_source: None, layers_panel: Default::default(), + navigator_panel: Default::default(), layers_filter: Default::default(), image_trace_advanced: true, image_trace_preview: true, diff --git a/docs/mcp.md b/docs/mcp.md index 549b5f6b1..837b0ad24 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1574,6 +1574,21 @@ enabled while the document has a saved view. In the desktop app the views are li checked when it opens: past 25, blank or repeated names, and views without a finite centre and a positive zoom are dropped. +## Navigator panel options + +The Navigator panel menu's Panel Options… opens the `navigatorPanelOptions` dialog (fields `color`, `solidDashes`), +and `ui.navigatorPanelOptions {color?, solidDashes?}` sets the options without it → `{color, solidDashes}`; with +neither key it opens the dialog. `color` is the color the panel's view box is drawn in: `#rrggbb` or a preset name. +The presets are VectorCraft's own list: Light Red (`#ff3030`, the default), then the layer colors ("Light Blue", +"Red", "Green" and the rest). `solidDashes` (Draw Dashed Lines as Solid Lines, off by default) makes the panel's +thumbnail draw dashed strokes without their dashes; the canvas, printing and exports keep the dashes. Both are kept +with the UI state. A `color` that is not a color, or a `solidDashes` that is not true or false, is an error and +changes neither option. Behavior reference: [View artwork](https://helpx.adobe.com/illustrator/using/viewing-artwork.html). + +```json +{"name":"invoke_menu","arguments":{"command":"ui.navigatorPanelOptions","params":{"color":"Light Blue","solidDashes":true}}} +``` + ## Ruler guides Ruler guides are numbered in the order they were made. `guide.add {vertical, pos, artboard?}` makes one (the x of a diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 1027fc25a..ed036d189 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -845,6 +845,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Snap to Glyph options (#4232, [Snap to Glyph options](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/snap-to-glyph-options.html)): the Character panel menu's Show Snap to Glyph Options (Hide Snap to Glyph Options) shows a Snap to Glyph section in the panel, with or without type selected, with a checkbox for each kind of guide: Baseline, x-height, Glyph Bounds, Proximity Guides (the edges of each character's width), Angular Guides and Anchor Point. Each off takes its guides away and leaves the others (`view.snapToGlyph.options`, the `glyphSnap` preference group, all on by default). Angular Guides covers a glyph's slanted strokes and every guide of rotated type, which runs at its angle; the boxes are dimmed while View › Snap to Glyph is off (VectorCraft choices; the page settles neither). Tested in `crates/tools/src/guides/tests.rs`, `crates/ui-egui/src/panels/character/tests.rs` and `crates/engine/src/tests/prefs.rs`. - **Artboard click-to-fit (#1227):** a plain click fits the topmost editable object or group to a new artboard, including its stroke like Fit to Selected Art, and makes the new artboard active. Art and its selection stay unchanged; creation is one undo step. Handles, modified clicks, drags, Escape and tool changes retain their meaning. Tested inside an artboard and on the pasteboard (`crates/engine/tests/it/artboard_click.rs`); the public [CS6 Printing Guide](https://helpx.adobe.com/content/dam/help/en/indesign/pdf/CS6_PrintGuide.pdf) documents the object-click gesture. This legacy gesture adds evidence without regrading the 2026 canvas controls. - **Rotated views:** with the view rotated, Fit Artboard in Window, Fit All in Window and the status bar's artboard navigator fit the artboard or the art as the view turns it, and a Zoom tool marquee (Animated Zoom or GPU Performance off) zooms to the area dragged across. Ruler guides on no artboard turn with the view. The canvas draws the selection marquee where the tools select, Show Hidden Characters draws the marks of type partly inside the canvas, the Contextual Task Bar stays under the selection, and the Navigator's red box outlines what the window shows. +- **Navigator panel options:** the Navigator panel menu's Panel Options… sets the color the view box is drawn in, from a preset list (VectorCraft's own: Light Red, the default, then the layer colors) or a custom color, and Draw Dashed Lines as Solid Lines, off by default, which draws dashed strokes without their dashes in the panel's thumbnail only (`ui.navigatorPanelOptions`; kept with the UI state; behavior reference: [View artwork](https://helpx.adobe.com/illustrator/using/viewing-artwork.html)). - **Views:** the status bar's artboard navigator (first, previous, next, last; `view.goToArtboard`) goes to an artboard, fits it and synchronizes the Artboard tool selection when that tool is active; ordinary object selection is preserved. Its four buttons are also commands of their own (`view.firstArtboard`, `view.previousArtboard`, `view.nextArtboard`, `view.lastArtboard`), listed in Edit › Keyboard Shortcuts without a default key, so a user can give them keys such as Page Up and Page Down (#5218). Actual Next-button regressions cover keyboard/Properties deletion and Undo in `chrome.rs`. Fit Artboard in Window and Actual Size show the navigator's artboard. The Artboards panel highlights that active artboard: clicking a row makes it active (the Artboard tool's too, while it is chosen) as does clicking an artboard with the Artboard tool, double-clicking its number also fits it in the window and double-clicking its name renames it. Duplicate Artboards, or a row dragged onto New Artboard, copies the artboard with its art (#446). Escape leaves Presentation Mode, `view.screenMode` from it goes back to Normal Screen Mode, and the screen mode isn't saved with the preferences: the app always starts in Normal Screen Mode with its menus and panels (#472). View › Pixel Preview shows the art as the document pixels it rasterizes to, one per point and hard-edged when magnified, turning with a rotated view (it used to draw the smooth vectors, with only the pixel grid over them). In Outline view a template layer keeps drawing in Preview, dimmed, so a picture placed to be traced stays visible under the outlines (#1505, fixes #1486). In Presentation Mode, → and ← go to the next and the previous artboard, and a click anywhere goes to the next one, as the status bar navigator's Next and Previous do (`view.goToArtboard`): on the last artboard → and a click leave the view as it is, and so does ← on the first. The arrow keys do not move the selection there, and the tool does not receive a click, so a click selects and draws nothing (behavior reference: [View artwork](https://helpx.adobe.com/illustrator/using/viewing-artwork.html)). Full Screen Mode and Presentation Mode put the window full screen, covering the taskbar and the window's frame, and Normal Screen Mode or Full Screen Mode with Menu Bar puts it back (#4365); a window the user made full screen stays so.