From 89abc2c9d213eac945bdf7f17790fb3aeae72a3e Mon Sep 17 00:00:00 2001 From: Taha-Mahmoodi <85902429+Taha-Mahmoodi@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:17:15 +0430 Subject: [PATCH] Symbols: a symbol's thumbnail draws the symbols it nests and the patterns and images it uses Review follow-up (CodeRabbit): thumbnails rendered the symbol alone in an empty document, so a symbol nesting another one, or painting with a pattern or showing an image, lost that art in the Symbols panel and in symbol library panels. The preview now renders an instance alone on a copy of the document (or library) the symbol comes from, which keeps its resources but none of its art, and the thumbnail cache also follows the art of the document's symbols and patterns. --- crates/ui-egui/src/panels/symbols.rs | 80 ++++++++++++++++++++++------ docs/target-app-parity.md | 2 +- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/crates/ui-egui/src/panels/symbols.rs b/crates/ui-egui/src/panels/symbols.rs index 9dc1beb92..7cfe65a40 100644 --- a/crates/ui-egui/src/panels/symbols.rs +++ b/crates/ui-egui/src/panels/symbols.rs @@ -19,31 +19,20 @@ use crate::menus::Item; use crate::theme::Tokens; use crate::widgets::{self, PanelDrag, menu_item}; -/// A thumbnail of symbol `name` (an instance at its natural size), cached by the art's identity. +/// A thumbnail of symbol `name` (an instance at its natural size), cached by the identity of the +/// art it shows: its own and that of the symbols and patterns it may use. fn thumb(ui: &Ui, doc: &Document, name: &str, size: f32) -> Option { thread_local! { static RENDERER: RefCell = RefCell::new(vectorcraft_render::Renderer::new()); - static CACHE: crate::graphics::TexCache> = crate::graphics::TexCache::default(); + static CACHE: crate::graphics::TexCache> = crate::graphics::TexCache::default(); } let sym = doc.symbols.iter().find(|s| s.name == name)?; let px = (size * ui.ctx().pixels_per_point()).round().max(8.0) as u32; - let key = (std::sync::Arc::as_ptr(&sym.art) as usize, name.to_string(), px); + let key = (Arc::as_ptr(&sym.art) as usize, resources_key(doc), name.to_string(), px); if let Some(t) = CACHE.with(|c| c.borrow().get(&key).cloned()) { return Some(t); } - let (w, h) = doc - .unknown - .get("symbolSizes") - .and_then(|m| m.get(name)) - .and_then(|v| Some((v.get(0)?.as_f64()?, v.get(1)?.as_f64()?))) - .unwrap_or((20.0, 20.0)); - let mut d = Document::new(w.max(1.0), h.max(1.0)); - d.symbols.push(sym.clone()); - let id = d.alloc_id(); - let xf = vectorcraft_geom::Affine::scale_non_uniform(w / 20.0, h / 20.0); - let l = d.layers[0].id; - d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: name.into(), xf })).ok()?; - let img = RENDERER.with(|r| r.borrow_mut().render_thumbnail(&d, id, px))?; + let img = RENDERER.with(|r| render_thumb(&mut r.borrow_mut(), doc, name, px))?; let color = crate::canvas::color_image(&img); let tex = ui.ctx().load_texture(format!("sym-{name}-{px}"), color, egui::TextureOptions::LINEAR); CACHE.with(|c| { @@ -56,6 +45,33 @@ fn thumb(ui: &Ui, doc: &Document, name: &str, size: f32) -> Option u64 { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + for s in &doc.symbols { + (Arc::as_ptr(&s.art) as usize).hash(&mut h); + } + for p in &doc.patterns { + p.art.iter().for_each(|a| (Arc::as_ptr(a) as usize).hash(&mut h)); + } + h.finish() +} + +/// Symbol `name` of `doc` rendered `px` pixels wide: an instance at its natural size, alone on a +/// copy of `doc` that keeps its resources (the symbols it nests, patterns, images, swatches, +/// brushes, graphic styles) but none of its art. +fn render_thumb(r: &mut vectorcraft_render::Renderer, doc: &Document, name: &str, px: u32) -> Option { + let (w, h) = symbol_size(doc, name); + let mut d = doc.clone(); + let layer = d.alloc_id(); + d.layers = vec![Arc::new(Node::layer(layer, "Preview", vectorcraft_doc::LayerColor::Preset(0)))]; + let id = d.alloc_id(); + let xf = vectorcraft_geom::Affine::scale_non_uniform(w / 20.0, h / 20.0); + d.insert(Some(layer), 0, Node::new(id, NodeKind::SymbolInstance { symbol: name.into(), xf })).ok()?; + r.render_thumbnail(&d, id, px) +} + /// Symbol `name`'s thumbnail on white in `r` (the panel's tiles and the chip a dragged symbol /// shows at the pointer). pub(crate) fn chip(ui: &Ui, doc: &Document, r: egui::Rect, name: &str) { @@ -404,6 +420,38 @@ mod tests { list["symbols"].as_array().unwrap().iter().map(|s| s["name"].as_str().unwrap_or_default().to_string()).collect() } + /// Pixels of `img` that something paints opaquely, and those of them that are red. + fn painted(img: &vectorcraft_render::Rendered) -> (usize, usize) { + let px = img.pixels.chunks_exact(4); + let painted: Vec<&[u8]> = px.filter(|p| p[3] > 200).collect(); + let red = painted.iter().filter(|p| p[0] > 150 && p[1] < 80 && p[2] < 80).count(); + (painted.len(), red) + } + + /// A symbol that nests another (a library's included) shows the nested art in its + /// thumbnail: the preview keeps the document's symbols and patterns, not its art. + #[test] + fn thumbnails_show_the_symbols_a_symbol_nests() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 40, "height": 40})).unwrap(); + s.execute("paint.setFill", &json!({"color": "#ff0000"})).unwrap(); + s.execute("paint.setStroke", &json!({"none": true})).unwrap(); + // The instance "Dot" leaves behind becomes all of "Badge". + s.execute("symbol.new", &json!({"name": "Dot"})).unwrap(); + s.execute("symbol.new", &json!({"name": "Badge"})).unwrap(); + // Other art of the document stays out of the preview. + s.execute("shape.ellipse", &json!({"x": 0, "y": 0, "width": 200, "height": 200})).unwrap(); + s.execute("paint.setFill", &json!({"color": "#0000ff"})).unwrap(); + let lib = vectorcraft_engine::cmd::symbollib::SymbolLibrary::from_document(&s.doc().unwrap().doc, &["Badge".into()], "Kit".into()).unwrap(); + let mut r = vectorcraft_render::Renderer::new(); + for doc in [&*s.doc().unwrap().doc, &lib.doc] { + let img = render_thumb(&mut r, doc, "Badge", 32).expect("a thumbnail"); + let (painted, red) = painted(&img); + assert!(painted > 0 && red * 10 >= painted * 9, "the nested Dot paints it: {painted} painted, {red} red"); + } + } + #[test] fn art_dropped_on_the_panel_becomes_a_symbol_and_a_symbol_drags_out() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 57be69bcd..c4b436165 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -571,7 +571,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The macOS menu bar is built from the in-window menus (one model, `native_menu`) and laid out as a Mac app's: the VectorCraft menu holds About, Settings ▸ (each Preferences page, General… ⌘K), Language ▸, Appearance ▸ (UI brightness), Services, Hide VectorCraft ⌃⌘H (⌘H stays Hide Edges), Hide Others, Show All and Quit; Window gains Minimize ⌃⌘M, Zoom and Bring All to Front, and Help the system's search field. Its key equivalents go back to egui as key presses, so user shortcuts, focused text fields and the Type tool's input method behave as in the window, and nothing fires twice; enabled, check marks and labels update in place. `--in-window-menus` or `VECTORCRAFT_IN_WINDOW_MENUS=1` keeps the menus in the window; `ui.inspect` reports `nativeMenuBar`. - macOS: Finder opens every format File › Open reads with VectorCraft (double-click, Open With, a drop on the Dock icon, `open -a`), at launch or while it runs; the release and `cargo xtask bundle` apps share one `Info.plist` that declares those document types, owning only VectorCraft's own formats. The DMG opens on a designed Finder window, VectorCraft.app beside the `Applications` link over a background with the app icon (#493). - Swatch and graphic style libraries, Libraries panel files, presets and plug-ins open wherever they are dropped on the window, as File › Open opens them. - - **Symbol libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html)): Window › Symbol Libraries and the Symbols panel's Symbol Libraries Menu open each library in a library panel of its own, as swatch and graphic style libraries do. The built-in library, Basic Shapes, is original art generated in code (`crates/engine/src/cmd/symbollib.rs`). A click on a symbol adds it to the document's Symbols as one undo step, with its Symbol Options (size, registration, export type, 9-slice guides) and every resource it uses (the symbols it nests, patterns, swatches, brushes, graphic styles, images), and makes it the current symbol; a symbol the document already has is not added again and another symbol of the same name keeps its name, the library's comes in numbered. Dragging a library symbol onto the canvas also places an instance there, in the same step ([placing from a library](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html)). Save Symbols (the library menu) and Save Symbol Library… (the Symbols panel menu) write the document's symbols as a `.vectorcraft` document holding only them, into the user library folder (listed under User Defined) or a file; Other Library… reads the symbols of any document VectorCraft opens; a file it can't read is an error and opens no panel. Tests: `crates/engine/src/tests_symbollib.rs`, `crates/ui-egui/src/panels/tests_symbol_libraries.rs`. + - **Symbol libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html)): Window › Symbol Libraries and the Symbols panel's Symbol Libraries Menu open each library in a library panel of its own, as swatch and graphic style libraries do. The built-in library, Basic Shapes, is original art generated in code (`crates/engine/src/cmd/symbollib.rs`). A click on a symbol adds it to the document's Symbols as one undo step, with its Symbol Options (size, registration, export type, 9-slice guides) and every resource it uses (the symbols it nests, patterns, swatches, brushes, graphic styles, images), and makes it the current symbol; a symbol the document already has is not added again and another symbol of the same name keeps its name, the library's comes in numbered. Thumbnails, in the library panels and the Symbols panel, draw a symbol with the symbols it nests and the patterns and images it uses. Dragging a library symbol onto the canvas also places an instance there, in the same step ([placing from a library](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html)). Save Symbols (the library menu) and Save Symbol Library… (the Symbols panel menu) write the document's symbols as a `.vectorcraft` document holding only them, into the user library folder (listed under User Defined) or a file; Other Library… reads the symbols of any document VectorCraft opens; a file it can't read is an error and opens no panel. Tests: `crates/engine/src/tests_symbollib.rs`, `crates/ui-egui/src/panels/tests_symbol_libraries.rs`, `panels::symbols::tests::thumbnails_show_the_symbols_a_symbol_nests`, `canvas::tests::a_library_symbol_dropped_on_the_canvas_is_added_and_placed_there_in_one_step`. - A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, Remove Anchor Points when anchors are selected, masks, compound paths, guides, Transform, Arrange, Select, Export Selection), or the view commands on empty canvas. - A right-click over locked objects adds an Unlock submenu to the canvas context menu. The submenu lists up to 20 locked objects under the pointer, topmost first, by the name the Layers panel shows, and choosing one unlocks it (`layer.setProps`). A locked group is listed as one item. Objects on hidden, locked or template layers are not listed. - Native macOS save sheets offer a localised Format menu (#866), built from the request's existing format filters. It selects the suggested filename's format and updates the suffix when changed, including legacy `.drawcraft` names. `apps/vectorcraft/src/mac_save_format.rs` has filename regressions; an actual native SVG save, selection of PDF and cancellation without changing the saved document were verified.