Merge pull request #7196 from dotCooCoo/fix/item-4459

Brushes: strokes draw with the first of two brushes with one name
This commit is contained in:
BFlat
2026-11-14 08:38:40 +07:00
committed by GitHub
5 changed files with 74 additions and 3 deletions
+4 -1
View File
@@ -781,7 +781,10 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
// Each brush the art uses with its type, found by name in the Brushes panel as the renderer finds it (the first of two
// brushes with one name); a brush the panel does not hold draws as a plain stroke and has no type. A row names the type
// as the New Brush dialog does (`Art Brush`), and the panel translates these values when it shows them.
let kinds: HashMap<&str, &vectorcraft_brush::BrushKind> = library.iter().map(|b| (b.name.as_str(), &b.kind)).collect();
let mut kinds: HashMap<&str, &vectorcraft_brush::BrushKind> = HashMap::new();
for b in &library {
kinds.entry(b.name.as_str()).or_insert(&b.kind);
}
let brush_entries: Vec<Value> =
brushes.iter().map(|name| json!({ "name": name, "type": kinds.get(name.as_str()).map(|k| k.type_id()) })).collect();
let brush_rows: Vec<(String, String)> = brushes
+24 -1
View File
@@ -107,7 +107,12 @@ impl BrushCache {
vectorcraft_brush::serde_json::to_string(v).unwrap_or_default().hash(&mut h);
h.finish()
});
self.lib = vectorcraft_brush::library(doc).into_iter().map(|b| (b.name.clone(), Arc::new(b))).collect();
// Of two brushes with one name, a stroke that names it draws with the first, as
// `vectorcraft_brush::find` and the exports take it.
self.lib.clear();
for b in vectorcraft_brush::library(doc) {
self.lib.entry(b.name.clone()).or_insert_with(|| Arc::new(b));
}
}
/// [`Self::key`] for the brush library `doc` holds (checked once a frame, `stamp`).
@@ -388,6 +393,24 @@ mod tests {
assert!(!dark(a.pixel(100, 58)) && dark(b.pixel(100, 58)));
}
/// Of two brushes with one name, a stroke that names it draws with the first.
#[test]
fn a_stroke_draws_with_the_first_of_two_brushes_with_its_name() {
use vectorcraft_brush::{Brush, BrushKind, Calligraphic};
let nib = |size: f64| Brush { name: "Twin".into(), kind: BrushKind::Calligraphic(Calligraphic { size, ..Default::default() }) };
let doc = |lib: &[Brush]| {
let mut d = doc_with_line(Some("Twin"), 1.0);
vectorcraft_brush::store(&mut d, lib);
d
};
let (thin, wide) = (nib(1.0), nib(20.0));
let first = render(&mut Renderer::new(), &doc(std::slice::from_ref(&thin))).pixels;
let second = render(&mut Renderer::new(), &doc(std::slice::from_ref(&wide))).pixels;
assert_ne!(first, second, "the two nibs paint differently");
let both = render(&mut Renderer::new(), &doc(&[thin, wide])).pixels;
assert!(both == first, "the stroke draws with the first brush named Twin");
}
#[test]
fn stained_symbol_instance() {
let mut d = Document::new(100.0, 100.0);
+11 -1
View File
@@ -69,6 +69,16 @@ pub(crate) fn document_brushes(st: &DocState) -> Rc<Vec<Shown>> {
MEMO.with(|m| m.borrow_mut().get(&st.doc, st.revision, || vectorcraft_brush::library(&st.doc).iter().map(|b| Shown::of(&st.doc, b)).collect()))
}
/// The brushes of `shown` by name: of two with one name, the first, the one a stroke that names
/// it draws with and `brush.get` gives.
fn by_name(shown: &[Shown]) -> HashMap<&str, &Shown> {
let mut map = HashMap::new();
for b in shown {
map.entry(b.name.as_str()).or_insert(b);
}
map
}
/// The cache key of the stroke preview of brush definition `def` in `doc` (`art`: its art): the
/// definition and every resource of `doc` the art uses ([`art_resources`]: patterns with their
/// options, symbols, swatches and colour groups, gradients, images, styles…), so the preview
@@ -298,7 +308,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
// definitions and preview keys are the document version's, worked out once.
let doc = app.session.active().map(|d| d.doc.clone());
let memo = app.session.active().map(document_brushes).unwrap_or_default();
let defs: HashMap<&str, &Shown> = memo.iter().map(|b| (b.name.as_str(), b)).collect();
let defs = by_name(&memo);
let mut clicked: Option<(String, egui::Modifiers)> = None;
let mut options: Option<String> = None;
let mut moved: Option<Value> = None;
@@ -821,3 +821,36 @@ fn previews_follow_the_swatches_images_and_pattern_options_their_art_uses() {
assert_eq!(change(&mut app, what, f), (brushes, symbols, what.to_string()));
}
}
/// Of two brushes with one name, the row of that name previews and drags the first, the one
/// `brush.get` gives and a stroke that names it draws with. Reading the library numbers the second
/// ("Twin 2"), so it gets a row of its own.
#[test]
fn rows_of_two_brushes_with_one_name_show_the_first() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
let mut twins: Vec<Brush> = Vec::new();
app.session
.edit("Brushes", |d, _| {
let mut lib = vectorcraft_brush::library(d);
let kinds: Vec<_> = ["Charcoal", "Dots"].iter().map(|n| lib.iter().find(|b| b.name == *n).unwrap().kind.clone()).collect();
twins = kinds.into_iter().map(|kind| Brush { name: "Twin".into(), kind }).collect();
lib.extend(twins.iter().cloned());
vectorcraft_brush::store(d, &lib);
Ok(())
})
.unwrap();
let rows: Vec<(String, String)> = brushes(&mut app).0.into_iter().filter(|b| b.0.starts_with("Twin")).map(|b| (b.0, b.1)).collect();
assert_eq!(rows, [("Twin".to_string(), "art".to_string()), ("Twin 2".to_string(), "scatter".to_string())], "the panel lists both");
let got = app.run("brush.get", json!({"name": "Twin"})).unwrap();
let memo = document_brushes(app.session.active().unwrap());
let shown = by_name(&memo)["Twin"];
assert_eq!(shown.ty, "art");
assert_eq!(shown.def, got, "the row shows and drags what brush.get gives");
// Rows of one name that reach the panel unnumbered still show the first.
let st = app.session.active().unwrap();
let raw: Vec<Shown> = twins.iter().map(|b| Shown::of(&st.doc, b)).collect();
let shown = by_name(&raw)["Twin"];
assert_eq!(shown.ty, "art");
assert_eq!(shown.def, got, "the first of two rows of one name is the one shown");
}
+2
View File
@@ -1844,6 +1844,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Brush art of copies:** symbol instances, inline graphics and repeat copies of a brushed path keep their own brush art between frames, as the same path on the page does. The renderer also keeps the stroke outlines of brush art and the evaluation of a live object in it, such as an envelope that a scatter brush places. The brush art is made again when the path, its stroke or the brush changes, and when the art of a symbol or pattern that the brush art can show changes. On one thread at 1000 × 1000 pixels (the median frame, with the median fastest frame in brackets), 2,000 paths on the page whose scatter brush art is an envelope take 94 ms (80 ms) a frame instead of 400 ms (306 ms), and 2,000 instances of a symbol whose art is a brushed path take 13.4 ms (12.0 ms) instead of 19.3 ms (15.6 ms), with the same pixels.
- **Two brushes with one name:** when a document stores two brushes with the same name, a stroke that names it draws on the canvas with the first of them, as `brush.get`, Brush Options, expanding, outlining and the SVG, PDF, EPS and `.ai` exports take it. Document Info gives that brush's type, and the Brushes panel's row of that name previews and drags the first brush. Reading the library numbers the second brush ("Name 2"), so the panel lists it in a row of its own that shows it.
- **Live objects in symbols and masks:** symbol instances and inline graphics whose symbol is a blend, an envelope, a repeat, an Intertwine object or a gradient mesh, or a group that holds one, draw the evaluation the renderer keeps for that object between frames, as the same object on the page does. So do opacity masks whose art is such an object, and such objects in clip groups in Outline view. The object is evaluated again when the symbols, patterns, fonts or color settings it shows change. Symbol art with effects of its own, or that holds a symbol instance, an image, a placed file, type with an inline graphic or a raster effect, is still placed and evaluated again in every frame. At 1000 × 1000 pixels, a page of 2,000 instances of a symbol that holds an envelope takes 28 ms a frame instead of 126 ms, with the same pixels.
- **Live objects in documents with many symbols:** a live object, or an object with effects, that shows symbols or patterns is not walked again in a later frame to find them while the document's symbols and patterns stay the same. One that shows a placed file is walked in every frame. At 1000 × 1000 pixels, a page of 2,000 envelopes around instances of the last 2,000 of 10,000 symbols takes 30 ms a frame, and 83 ms when every frame walks them again, with the same pixels.