diff --git a/ROADMAP.md b/ROADMAP.md index b1f75b2735..af46cb5a4e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -127,6 +127,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap - 2026-11-02: Mouse wheel over a closed font-family field in Character, Properties or Control changes selected text through the family list using saved font filters (excluding search text), one undo step per notch, stopping at the ends (#5253). Scores and estimates unchanged. - 2026-10-31: Pattern libraries: Window › Swatch Libraries › Patterns and the Swatches panel's library menu open Geometric Patterns, a built-in library of eleven pattern swatches (dots, stripes, checks, a grid and shapes); clicking one adds its pattern and swatch to the document, once, and applies it to the active fill or stroke (`swatch.library.add`). - 2026-11-11: Pattern libraries: Patterns › Grit Textures, a second built-in pattern library, holds eight seamless black textures (Fine Grain, Coarse Grit, Sand, Speckle, Stipple, Scratches, Sparse Flecks and Dense Flecks) on tiles of 18 to 36 pt, each drawn the same way every time; marks that reach past a tile edge continue on the next tile, so the tiles join without a seam (`swatch.library.add`). Scores and estimates unchanged. +- 2026-11-11: Pattern libraries: Patterns › Optical Effects, a third built-in pattern library, of VectorCraft's own designs, holds eight black wave, moiré and op-art tiles (Sine Bands, Ripple Lines, Interference Rings, Offset Rings, Warped Checks, Pinched Stripes, Chevron Pulse and Radiating Lenses) of 32 to 48 pt, each drawn the same way every time; each tile is one period of its design, and art that reaches past a tile edge continues on the next tile, so the tiles join without a seam (`swatch.library.add`). Scores and estimates unchanged. - 2026-11-01: Find Nearest Swatch joins the Color Effects add-on (Object › Plug-ins › Color Effects): for each colour of the selection, the closest swatches of the document, a built-in library or a library or colour book the user loaded, with their CIEDE2000 difference; Apply gives each colour its nearest swatch as one undo step, linking global and spot swatches and adding a library's to the document first, so a process colour becomes the closest spot colour (`addon.color.nearestSwatch`, `addon.color.applyNearestSwatch`, #4923). - 2026-11-01: Expand Dashes into Paths joins the Path Precision Tools add-on (Object › Plug-ins › Path Precision Tools): each selected path's dashed stroke becomes separate open paths, one per dash, laid out exactly as the stroke draws them (pattern, offset, Align Dashes to Corners and Path Ends), each keeping the stroke with the dashes off, so the gaps are real gaps for cutters and plotters, as one undo step (`addon.pathtools.expandDashes`, #4922). - 2026-11-01: Randomize Opacity joins the Arrange Tools add-on (Object › Plug-ins › Arrange Tools): each selected object gets its own opacity at random between a minimum and a maximum percent, with a seed that repeats the result, as one undo step (`addon.arrange.randomizeOpacity`, #4925). diff --git a/crates/engine/src/cmd/patternlib.rs b/crates/engine/src/cmd/patternlib.rs index 8fd2a0ea5e..f69e4b889e 100644 --- a/crates/engine/src/cmd/patternlib.rs +++ b/crates/engine/src/cmd/patternlib.rs @@ -26,6 +26,7 @@ pub struct BuiltinPatternLibrary { pub const PATTERN_LIBRARIES: &[BuiltinPatternLibrary] = &[ BuiltinPatternLibrary { id: "geometric-patterns", name: "Geometric Patterns", make: geometric_patterns }, BuiltinPatternLibrary { id: "grit-textures", name: "Grit Textures", make: grit_textures }, + BuiltinPatternLibrary { id: "optical-effects", name: "Optical Effects", make: optical_effects }, ]; /// The library category of the built-in pattern libraries. @@ -256,6 +257,162 @@ fn grit_textures(d: &mut Document) { }); } +/// `n` + 1 points of the curve `(t, f(t))` for `t` from `t0` to `t1`, evenly spaced. +fn curve(t0: f64, t1: f64, n: u32, f: impl Fn(f64) -> f64) -> Vec<(f64, f64)> { + (0..=n) + .map(|i| { + let t = t0 + (t1 - t0) * f64::from(i) / f64::from(n); + (t, f(t)) + }) + .collect() +} + +/// The closed band between two curves drawn from the same side to the same side: `upper` forward, +/// then `lower` back. +fn band(upper: &[(f64, f64)], lower: &[(f64, f64)]) -> PathData { + let points: Vec<(f64, f64)> = upper.iter().chain(lower.iter().rev()).copied().collect(); + polygon(&points) +} + +/// 72 corners of the circle of radius `r` about (`cx`, `cy`), turning the other way with `reverse`. +fn circle(cx: f64, cy: f64, r: f64, reverse: bool) -> SubPath { + const CORNERS: u32 = 72; + let turn = if reverse { -std::f64::consts::TAU } else { std::f64::consts::TAU }; + let points: Vec = (0..CORNERS) + .map(|i| { + let a = turn * f64::from(i) / f64::from(CORNERS); + Point::new(cx + r * a.cos(), cy + r * a.sin()) + }) + .collect(); + SubPath::polyline(&points, true) +} + +/// The ring from radius `inner` to `outer` about (`cx`, `cy`): the outer circle and the inner one +/// turning the other way, so the middle stays clear under either fill rule. An `inner` of 0 gives +/// a disk. +fn ring(cx: f64, cy: f64, inner: f64, outer: f64) -> PathData { + let mut subpaths = vec![circle(cx, cy, outer, false)]; + if inner > 0.0 { + subpaths.push(circle(cx, cy, inner, true)); + } + PathData::new(subpaths) +} + +/// A lens `2 × l` long and `2 × h` high about (`cx`, `cy`), lying along x: two half sine waves that +/// meet at its tips, turning the other way with `reverse`. +fn lens(cx: f64, cy: f64, l: f64, h: f64, reverse: bool) -> SubPath { + const STEPS: u32 = 32; + let at = |i: u32, side: f64| { + let t = f64::from(i) / f64::from(STEPS); + Point::new(cx - l + 2.0 * l * t, cy + side * h * (std::f64::consts::PI * t).sin()) + }; + let mut points: Vec = (0..=STEPS).map(|i| at(i, -1.0)).chain((1..STEPS).rev().map(|i| at(i, 1.0))).collect(); + if reverse { + points.reverse(); + } + SubPath::polyline(&points, true) +} + +/// Optical Effects: black waves, rings, warped checks, pinched stripes, chevrons and lenses on a +/// clear background. Each tile is one period of its design, so a band that leaves one edge comes +/// back at the same place on the opposite edge. Art that reaches past an edge is kept whole: the +/// tiling draws the part outside the tile on the next tile. +fn optical_effects(d: &mut Document) { + use std::f64::consts::TAU; + // Three bands 5 pt wide, 12 pt apart, each one sine wave across the tile. + let bands = (0..3u32) + .map(|k| { + let mid = move |x: f64| 6.0 + 12.0 * f64::from(k) + 3.0 * (TAU * x / 36.0).sin(); + band(&curve(0.0, 36.0, 48, |x| mid(x) - 2.5), &curve(0.0, 36.0, 48, |x| mid(x) + 2.5)) + }) + .collect(); + add(d, "Sine Bands", 36.0, 36.0, TileType::Grid, bands); + // Nine thin waves 4 pt apart, two waves across the tile, each a ninth of a wave on from the one + // above it, so the ninth leads into the first wave of the next tile down. + let lines = (0..9u32) + .map(|k| { + let (y, phase) = (2.0 + 4.0 * f64::from(k), TAU * f64::from(k) / 9.0); + let mid = move |x: f64| y + 2.5 * (TAU * x / 18.0 + phase).sin(); + band(&curve(0.0, 36.0, 72, |x| mid(x) - 0.6), &curve(0.0, 36.0, 72, |x| mid(x) + 0.6)) + }) + .collect(); + add(d, "Ripple Lines", 36.0, 36.0, TileType::Grid, lines); + // Thin rings 3 pt apart about the center and rings 3.3 pt apart about a point 4 pt to its + // right. The two sets cross, and the outer rings reach into the next tile across. + let rings = (1..=7u32) + .map(|k| (24.0, 3.0 * f64::from(k))) + .chain((1..=7u32).map(|k| (28.0, 3.3 * f64::from(k)))) + .map(|(cx, r)| ring(cx, 24.0, r - 0.6, r + 0.6)) + .collect(); + add(d, "Interference Rings", 48.0, 48.0, TileType::Grid, rings); + // Rings 2 pt and 1 pt wide by turns about the tile's corner and about its center, the center's + // starting with the narrower one. The corner's rings are the same rings at all four corners. + let mut rings = vec![]; + for (c, first) in [(0.0, 0), (20.0, 1)] { + let mut r = 0.0; + for k in 0..5 { + let width = if (k + first) % 2 == 0 { 2.0 } else { 1.0 }; + rings.push(ring(c, c, r, r + width)); + r += width + 1.2; + } + } + add(d, "Offset Rings", 40.0, 40.0, TileType::Grid, rings); + // An 8 × 8 checkerboard of 4 pt cells whose columns are bent by a sine wave down the tile and + // whose rows by one across it, each edge drawn through five points. + let warp = |x: f64, y: f64| (x + 2.0 * (TAU * y / 32.0).sin(), y + 2.0 * (TAU * x / 32.0).sin()); + let cells = (0..8u32) + .flat_map(|row| (0..8u32).map(move |col| (col, row))) + .filter(|(col, row)| (col + row) % 2 == 0) + .map(|(col, row)| { + let (x, y) = (4.0 * f64::from(col), 4.0 * f64::from(row)); + let corners = [(x, y), (x + 4.0, y), (x + 4.0, y + 4.0), (x, y + 4.0), (x, y)]; + let points: Vec<(f64, f64)> = corners + .windows(2) + .flat_map(|e| { + let (a, b) = (e[0], e[1]); + (0..4u32).map(move |i| { + let t = f64::from(i) / 4.0; + warp(a.0 + (b.0 - a.0) * t, a.1 + (b.1 - a.1) * t) + }) + }) + .collect(); + polygon(&points) + }) + .collect(); + add(d, "Warped Checks", 32.0, 32.0, TileType::Grid, cells); + // Six upright stripes 6 pt apart that widen from 0.8 pt to 5.6 pt and narrow again down the tile, + // each a sixth of a wave on from the one to its left. + let stripes = (0..6u32) + .map(|k| { + let (x, phase) = (3.0 + 6.0 * f64::from(k), TAU * f64::from(k) / 6.0); + let half = move |y: f64| 1.6 + 1.2 * (TAU * y / 36.0 + phase).cos(); + let side = |s: f64| -> Vec<(f64, f64)> { curve(0.0, 36.0, 48, |y| x + s * half(y)).into_iter().map(|(y, at)| (at, y)).collect() }; + band(&side(-1.0), &side(1.0)) + }) + .collect(); + add(d, "Pinched Stripes", 36.0, 36.0, TileType::Grid, stripes); + // Four zigzag bands 8 pt apart, two zigzags across the tile; the bend and the width of the bands + // rise and fall from band to band. + let chevrons = [(0.0, 3.0, 4.0), (8.0, 4.5, 2.5), (16.0, 3.0, 1.0), (24.0, 1.5, 2.5)] + .into_iter() + .map(|(y, bend, width)| { + let upper: Vec<(f64, f64)> = (0..=4u32).map(|i| (8.0 * f64::from(i), if i % 2 == 1 { y + bend } else { y })).collect(); + let lower: Vec<(f64, f64)> = upper.iter().map(|&(x, y)| (x, y + width)).collect(); + band(&upper, &lower) + }) + .collect(); + add(d, "Chevron Pulse", 32.0, 32.0, TileType::Grid, chevrons); + // Five nested lens outlines 1.5 pt wide about the center, 14 pt to 78 pt long. The longer ones + // reach across the side edges and cross the next tiles' lenses. + let lenses = (0..5u32) + .map(|k| { + let (l, h) = (7.0 + 8.0 * f64::from(k), 4.0 + 4.0 * f64::from(k)); + PathData::new(vec![lens(24.0, 24.0, l, h, false), lens(24.0, 24.0, l - 1.5, h - 1.5, true)]) + }) + .collect(); + add(d, "Radiating Lenses", 48.0, 48.0, TileType::Grid, lenses); +} + #[cfg(test)] mod tests { use super::*; @@ -326,4 +483,101 @@ mod tests { } assert!(crossing > 0, "no mark reaches past an edge"); } + + const OPTICAL: [&str; 8] = + ["Sine Bands", "Ripple Lines", "Interference Rings", "Offset Rings", "Warped Checks", "Pinched Stripes", "Chevron Pulse", "Radiating Lenses"]; + + fn optical_doc() -> Document { + let mut d = Document::new(100.0, 100.0); + optical_effects(&mut d); + d + } + + /// The paths of pattern `p`'s art, each with its fill rule. + fn paths(p: &PatternDef) -> Vec<(&PathData, vectorcraft_geom::FillRule)> { + p.art + .iter() + .map(|n| match &n.kind { + vectorcraft_doc::NodeKind::Path { path, rule, .. } => (path, *rule), + _ => panic!("{}: art that is not a path", p.name), + }) + .collect() + } + + #[test] + fn optical_effects_is_a_pattern_library_of_eight_bounded_tiles() { + let b = PATTERN_LIBRARIES.iter().find(|b| b.id == "optical-effects").expect("Optical Effects is listed"); + assert_eq!(b.name, "Optical Effects"); + let names: Vec = library_doc(b.id).unwrap().patterns.iter().map(|p| p.name.clone()).collect(); + assert_eq!(names, OPTICAL); + // Made twice, the art is the same. + let (a, again) = (optical_doc(), optical_doc()); + assert_eq!(a.patterns, again.patterns); + for p in &a.patterns { + let (w, h) = (p.width(), p.height()); + assert!((32.0..=48.0).contains(&w) && w == h && p.tile.x0 == 0.0 && p.tile.y0 == 0.0, "{}", p.name); + assert_eq!(p.tile_type, TileType::Grid, "{}", p.name); + let paths = paths(p); + let segments: usize = paths.iter().flat_map(|(path, _)| &path.subpaths).map(SubPath::segment_count).sum(); + assert!(!paths.is_empty() && paths.len() <= 600 && segments <= 20_000, "{}: {} paths, {segments} segments", p.name, paths.len()); + for (path, _) in &paths { + let b = path.bounds().unwrap_or_else(|| panic!("{}: an empty path", p.name)); + assert!([b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite()) && b.width() > 0.0 && b.height() > 0.0, "{}: {b:?}", p.name); + assert!(b.x1 > 0.0 && b.y1 > 0.0 && b.x0 < w && b.y0 < h, "{}: {b:?} does not touch the tile", p.name); + } + } + } + + /// Just either side of a tile's right and bottom edges, the tiling paints the same points: a band + /// cut off at an edge, or one that comes back at another height or width, paints one side only. + /// No path is another moved by a whole tile, which the tiling would draw twice in one place. + #[test] + fn optical_tiles_join_without_a_seam() { + const SAMPLES: u32 = 400; + const STEP: f64 = 1e-4; + for p in &optical_doc().patterns { + let (w, h) = (p.width(), p.height()); + let shapes: Vec<(BezPathFill, Rect)> = + paths(p).into_iter().map(|(path, rule)| ((path.to_bezpath(), rule), path.bounds().unwrap_or(Rect::ZERO))).collect(); + let paints = |q: Point| { + let near = Rect::new(q.x - STEP, q.y - STEP, q.x + STEP, q.y + STEP); + p.offsets_covering(near).into_iter().any(|o| { + let at = q - o; + shapes.iter().any(|((bp, rule), b)| { + let inside = b.x0 <= at.x && at.x <= b.x1 && b.y0 <= at.y && at.y <= b.y1; + let turns = if inside { kurbo::Shape::winding(bp, at) } else { 0 }; + match rule { + vectorcraft_geom::FillRule::EvenOdd => turns % 2 != 0, + vectorcraft_geom::FillRule::NonZero => turns != 0, + } + }) + }) + }; + let (mut differ, mut painted) = (0, 0); + for i in 0..SAMPLES { + let t = (f64::from(i) + 0.37) / f64::from(SAMPLES); + let (right, bottom) = (Point::new(w, t * h), Point::new(t * w, h)); + let (dx, dy) = (kurbo::Vec2::new(STEP, 0.0), kurbo::Vec2::new(0.0, STEP)); + for (a, b) in [(right - dx, right + dx), (bottom - dy, bottom + dy)] { + let (pa, pb) = (paints(a), paints(b)); + differ += usize::from(pa != pb); + painted += usize::from(pa); + } + } + assert!(differ <= 2, "{}: {differ} of {} points across the edges differ", p.name, 2 * SAMPLES); + assert!(painted > 0, "{}: nothing crosses an edge", p.name); + let bounds: Vec = shapes.iter().map(|(_, b)| *b).collect(); + let same = |a: &Rect, b: Rect| { + (a.x0 - b.x0).abs() < 1e-9 && (a.y0 - b.y0).abs() < 1e-9 && (a.x1 - b.x1).abs() < 1e-9 && (a.y1 - b.y1).abs() < 1e-9 + }; + for b in &bounds { + for (dx, dy) in [(w, 0.0), (-w, 0.0), (0.0, h), (0.0, -h), (w, h), (w, -h), (-w, h), (-w, -h)] { + let moved = *b + kurbo::Vec2::new(dx, dy); + assert!(!bounds.iter().any(|o| same(o, moved)), "{}: {b:?} is drawn twice by the tiling", p.name); + } + } + } + } + + type BezPathFill = (vectorcraft_geom::BezPath, vectorcraft_geom::FillRule); } diff --git a/crates/engine/src/tests/swatchlib.rs b/crates/engine/src/tests/swatchlib.rs index 57d6c23d06..a02fb079c9 100644 --- a/crates/engine/src/tests/swatchlib.rs +++ b/crates/engine/src/tests/swatchlib.rs @@ -806,7 +806,7 @@ fn a_grit_texture_adds_once_undoes_and_saves() { let list = run(&mut s, "swatch.library.list", json!({})); let ids: Vec<&str> = list["libraries"].as_array().unwrap().iter().filter(|l| l["category"] == "patterns").map(|l| l["id"].as_str().unwrap()).collect(); - assert_eq!(ids, ["geometric-patterns", "grit-textures"]); + assert_eq!(ids, ["geometric-patterns", "grit-textures", "optical-effects"]); let got = run(&mut s, "swatch.library.get", json!({"library": "Grit Textures"})); let names: Vec<&str> = got["swatches"].as_array().unwrap().iter().map(|w| w["name"].as_str().unwrap()).collect(); assert_eq!(names, ["Fine Grain", "Coarse Grit", "Sand", "Speckle", "Stipple", "Scratches", "Sparse Flecks", "Dense Flecks"]); @@ -831,6 +831,49 @@ fn a_grit_texture_adds_once_undoes_and_saves() { assert_eq!(back.swatch("Stipple").map(|w| &w.paint), doc(&s).swatch("Stipple").map(|w| &w.paint)); } +/// Optical Effects is listed after Grit Textures, and reading it adds nothing. Clicking one of +/// its patterns brings it with its swatch and fills the selection with it, once, as one undo step +/// that also takes both away, and both stay after a save and reopen. +#[test] +fn an_optical_effect_adds_once_fills_undoes_and_saves() { + let mut s = session(); + let counts = |d: &vectorcraft_doc::Document| (d.patterns.len(), d.swatches_iter().count()); + let empty = counts(doc(&s)); + let list = run(&mut s, "swatch.library.list", json!({})); + let ids: Vec<&str> = + list["libraries"].as_array().unwrap().iter().filter(|l| l["category"] == "patterns").map(|l| l["id"].as_str().unwrap()).collect(); + assert_eq!(ids, ["geometric-patterns", "grit-textures", "optical-effects"]); + let got = run(&mut s, "swatch.library.get", json!({"library": "Optical Effects"})); + let names: Vec<&str> = got["swatches"].as_array().unwrap().iter().map(|w| w["name"].as_str().unwrap()).collect(); + assert_eq!( + names, + ["Sine Bands", "Ripple Lines", "Interference Rings", "Offset Rings", "Warped Checks", "Pinched Stripes", "Chevron Pulse", "Radiating Lenses"] + ); + assert_eq!(counts(doc(&s)), empty, "reading a library adds no pattern or swatch"); + let id = vectorcraft_doc::NodeId(run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].as_u64().unwrap()); + let undo = undo_len(&s); + let r = run(&mut s, "swatch.library.add", json!({"library": "optical-effects", "names": ["Warped Checks"], "apply": "fill"})); + assert_eq!((r["added"].clone(), r["applied"].clone()), (json!(["Warped Checks"]), json!("Warped Checks"))); + assert_eq!((counts(doc(&s)), undo_len(&s)), ((empty.0 + 1, empty.1 + 1), undo + 1)); + let def = doc(&s).pattern("Warped Checks").expect("the pattern comes with its swatch").clone(); + assert_eq!(def.art.len(), 32, "the black cells of an 8 × 8 checkerboard"); + assert!(matches!(doc(&s).swatch("Warped Checks").map(|w| &w.paint), Some(Paint::Pattern { pattern, .. }) if pattern == "Warped Checks")); + let filled = |d: &vectorcraft_doc::Document| matches!(d.node(id).unwrap().appearance.fill_paint(), Paint::Pattern { pattern, .. } if pattern == "Warped Checks"); + assert!(filled(doc(&s))); + let r = run(&mut s, "swatch.library.add", json!({"library": "optical-effects", "names": ["Warped Checks"]})); + assert_eq!((r["added"].clone(), r["existing"].clone()), (json!([]), json!(["Warped Checks"]))); + assert_eq!((counts(doc(&s)), undo_len(&s)), ((empty.0 + 1, empty.1 + 1), undo + 1), "added once"); + run(&mut s, "edit.undo", json!({})); + assert_eq!(counts(doc(&s)), empty); + assert!(doc(&s).pattern("Warped Checks").is_none() && doc(&s).swatch("Warped Checks").is_none() && !filled(doc(&s))); + run(&mut s, "edit.redo", json!({})); + assert!(filled(doc(&s))); + let back = vectorcraft_format::load(&vectorcraft_format::save(doc(&s), false)).unwrap(); + assert_eq!(back.pattern("Warped Checks"), Some(&def)); + assert_eq!(back.swatch("Warped Checks").map(|w| &w.paint), doc(&s).swatch("Warped Checks").map(|w| &w.paint)); + assert!(filled(&back)); +} + /// A device name is saved with a `-` after it, which can be another library's own name: neither /// library replaces the other, and saving either again replaces only itself. #[test] diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 97e5b335d3..240444ac20 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5851,6 +5851,7 @@ theme Appearance Външен вид Ruby… Руби… Ruby Руби Geometric Guides Геометрични водачи + Optical Effects Оптични ефекти Geometric Patterns Геометрични мостри Grit Textures Зърнести текстури Ease In Плавно начало diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 75353321ff..22d1455f73 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5868,6 +5868,7 @@ mark-style Japanese Japanski Ruby… Ruby… Ruby Ruby Geometric Guides Geometrijske vodilice + Optical Effects Optički efekti Geometric Patterns Geometrijski uzorci Grit Textures Zrnaste teksture Ease In Ublaži ulaz diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 75e0b9ea20..2d6e8799d0 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -2914,6 +2914,7 @@ digits Malayalam Malaiàlam Opens network connections Obre connexions de xarxa Open… Obre… Optical Òptic + Optical Effects Efectes òptics Optical Margin Alignment Alineació òptica de marges Dynamic Text Text dinàmic Dynamic Text Presets Valors predefinits de text dinàmic diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 5d37ee136e..3c1170bf9d 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6033,6 +6033,7 @@ mark-style Japanese Japonské Ruby… Ruby… Ruby Ruby Geometric Guides Geometrická vodítka + Optical Effects Optické efekty Geometric Patterns Geometrické vzorky Grit Textures Zrnité textury Ease In Pozvolný začátek diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 8167f80138..2a1b9d8007 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5866,6 +5866,7 @@ swatch Bright Violet Kraftig violet Ruby… Ruby… Ruby Ruby Geometric Guides Geometriske hjælpelinjer + Optical Effects Optiske effekter Geometric Patterns Geometriske mønstre Grit Textures Kornede teksturer Ease In Langsom start diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index bd4bb38b43..0805be2caf 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5622,6 +5622,7 @@ effect Soft Weich Ruby… Ruby… Ruby Ruby Geometric Guides Geometrische Hilfslinien + Optical Effects Optische Effekte Geometric Patterns Geometrische Muster Grit Textures Körnige Texturen Ease In Langsam starten diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 9860f8ad50..278b6cf81a 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -5984,6 +5984,7 @@ effect Points Σημεία Ruby… Ruby… Ruby Ruby Geometric Guides Γεωμετρικοί οδηγοί + Optical Effects Οπτικά εφέ Geometric Patterns Γεωμετρικά μοτίβα Grit Textures Κοκκώδεις υφές Ease In Ομαλή αρχή diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 10c0c0bf87..f0f804e008 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5975,6 +5975,7 @@ mark-style Japanese Japonesas Ruby… Ruby… Ruby Ruby Geometric Guides Guías geométricas + Optical Effects Efectos ópticos Geometric Patterns Motivos geométricos Grit Textures Texturas granuladas Ease In Entrada suave diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 7845235f65..471b98e76c 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6063,6 +6063,7 @@ swatch Bright Violet Kirkas violetti Ruby… Ruby… Ruby Ruby Geometric Guides Geometriset apuviivat + Optical Effects Optiset tehosteet Geometric Patterns Geometriset kuviot Grit Textures Rakeiset tekstuurit Ease In Hidas alku diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 5e2895f9c2..fe3b98d0e8 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6028,6 +6028,7 @@ theme Appearance Apparence Ruby… Ruby… Ruby Ruby Geometric Guides Repères géométriques + Optical Effects Effets optiques Geometric Patterns Motifs géométriques Grit Textures Textures granuleuses Ease In Accélération progressive diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index aca21d7eec..74fb288598 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -3170,6 +3170,7 @@ antiAlias Sharp शार्प Opens network connections नेटवर्क कनेक्शन ओपन करें OpenType Features ओपन टाइप सुविधाएँ Optical ऑप्टिकल + Optical Effects ऑप्टिकल इफ़ेक्ट्स Optical Margin Alignment ऑप्टिकल मार्जिन संरेखण Dynamic Text डायनैमिक टेक्स्ट Dynamic Text Presets डायनैमिक टेक्स्ट प्रीसेट diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 45d19b0ca9..55b97d4d21 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6016,6 +6016,7 @@ imageTrace Outline Körvonal Ruby… Ruby… Ruby Ruby Geometric Guides Geometriai segédvonalak + Optical Effects Optikai effektusok Geometric Patterns Geometriai mintázatok Grit Textures Szemcsés textúrák Ease In Lassú kezdés diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 8e2b383e2d..a55f7699bf 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5866,6 +5866,7 @@ swatch Bright Violet Violet cerah Ruby… Ruby… Ruby Ruby Geometric Guides Panduan Geometris + Optical Effects Efek Optis Geometric Patterns Pola Geometris Grit Textures Tekstur Butiran Ease In Awal Lambat diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 26e1f29651..a9ba306b9f 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5656,6 +5656,7 @@ lighting Light Luce Ruby… Ruby… Ruby Ruby Geometric Guides Guide geometriche + Optical Effects Effetti ottici Geometric Patterns Motivi geometrici Grit Textures Texture granulose Ease In Inizio graduale diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 30873028e8..fab0450610 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5706,6 +5706,7 @@ lighting Light ライト Ruby… ルビ… Ruby ルビ Geometric Guides 幾何ガイド + Optical Effects オプティカル効果 Geometric Patterns 幾何パターン Grit Textures 粒状テクスチャ Ease In イーズイン diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 8e9d9d049d..c8274c9b31 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5973,6 +5973,7 @@ imageTrace Outline კონტური Ruby… რუბი… Ruby რუბი Geometric Guides გეომეტრიული მიმმართველები + Optical Effects ოპტიკური ეფექტები Geometric Patterns გეომეტრიული პატერნები Grit Textures მარცვლოვანი ტექსტურები Ease In ნელი დასაწყისი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index f00e6cdeba..93b4ce1bb9 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5617,6 +5617,7 @@ lighting Light 광원 Ruby… 루비… Ruby 루비 Geometric Guides 기하학적 안내선 + Optical Effects 광학 효과 Geometric Patterns 기하학적 패턴 Grit Textures 입자 텍스처 Ease In 서서히 시작 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index bc00b76698..282b409a58 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5917,6 +5917,7 @@ swatch Bright Violet Felviolet Ruby… Ruby… Ruby Ruby Geometric Guides Geometrische hulplijnen + Optical Effects Optische effecten Geometric Patterns Geometrische patronen Grit Textures Korrelige texturen Ease In Langzaam beginnen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 23940fb80f..1899b72951 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5887,6 +5887,7 @@ mark-style Japanese Japońskie Ruby… Ruby… Ruby Ruby Geometric Guides Geometryczne linie pomocnicze + Optical Effects Efekty optyczne Geometric Patterns Wzorki geometryczne Grit Textures Tekstury ziarniste Ease In Łagodny początek diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 32c4b4bcc0..13eee61ed3 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5948,6 +5948,7 @@ mark-style Japanese Japonesas Ruby… Ruby… Ruby Ruby Geometric Guides Guias geométricas + Optical Effects Efeitos ópticos Geometric Patterns Padrões geométricos Grit Textures Texturas granuladas Ease In Entrada suave diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 15d3c3a380..f1321b48d8 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5642,6 +5642,7 @@ lighting Light Свет Ruby… Руби… Ruby Руби Geometric Guides Геометрические направляющие + Optical Effects Оптические эффекты Geometric Patterns Геометрические узоры Grit Textures Зернистые текстуры Ease In Плавное начало diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 5ffc963fad..999be89513 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6055,6 +6055,7 @@ font-script Japanese Japanska Ruby… Ruby… Ruby Ruby Geometric Guides Geometriska stödlinjer + Optical Effects Optiska effekter Geometric Patterns Geometriska mönster Grit Textures Korniga texturer Ease In Långsam start diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 86356d74ba..753f6024f6 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6028,6 +6028,7 @@ axis Y Axis Y ekseni Snap to Tangent Teğete Yasla Snap to Perpendicular Dikeye Yasla Geometric Guides Geometrik Kılavuzlar + Optical Effects Optik Efektler Geometric Patterns Geometrik Desenler Grit Textures Grenli Dokular @msg `method` must be classic or perceptual `method` classic veya perceptual olmalıdır diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index ebba0f5c19..ad649f4ab0 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5636,6 +5636,7 @@ lighting Light Світло Ruby… Рубі… Ruby Рубі Geometric Guides Геометричні напрямні + Optical Effects Оптичні ефекти Geometric Patterns Геометричні візерунки Grit Textures Зернисті текстури Ease In Плавний початок diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 0ec0fd4e40..93e9d93881 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5637,6 +5637,7 @@ theme Appearance Giao diện Ruby… Ruby… Ruby Ruby Geometric Guides Đường dẫn hình học + Optical Effects Hiệu ứng quang học Geometric Patterns Họa tiết hình học Grit Textures Kết cấu hạt Ease In Chậm lúc đầu diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 9fc1cdc4e8..45cd676f69 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5870,6 +5870,7 @@ lighting Light 光源 Ruby… 注音… Ruby 注音 Geometric Guides 几何参考线 + Optical Effects 光学效果 Geometric Patterns 几何图案 Grit Textures 颗粒纹理 Ease In 缓入 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 685f7887fb..e6393341b3 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5674,6 +5674,7 @@ lighting Light 光源 Ruby… 注音… Ruby 注音 Geometric Guides 幾何參考線 + Optical Effects 光學效果 Geometric Patterns 幾何圖樣 Grit Textures 顆粒紋理 Ease In 緩入 diff --git a/crates/ui-egui/src/panels/library_panel/tests.rs b/crates/ui-egui/src/panels/library_panel/tests.rs index f28e7e5540..10acd4603e 100644 --- a/crates/ui-egui/src/panels/library_panel/tests.rs +++ b/crates/ui-egui/src/panels/library_panel/tests.rs @@ -155,9 +155,10 @@ fn find_filters_list_view_and_arrows_step_through_the_libraries() { assert!(ctx.read_response(tile_id(panel, "#00FF00")).is_none()); let first = SWATCH_IDS[0]; let last = SwatchLibraries::list(&app).pop().unwrap().id; - assert_eq!(last, "grit-textures", "gradient and then pattern libraries come after the swatch libraries"); + assert_eq!(last, "optical-effects", "gradient and then pattern libraries come after the swatch libraries"); assert_eq!(neighbour::(&app, "radial-glows", 1).as_deref(), Some("geometric-patterns")); assert_eq!(neighbour::(&app, "geometric-patterns", 1).as_deref(), Some("grit-textures")); + assert_eq!(neighbour::(&app, "grit-textures", 1).as_deref(), Some("optical-effects")); assert_eq!(neighbour::(&app, first, -1), Some(last), "wraps around"); assert_eq!(neighbour::(&app, "web-safe-216", 1).as_deref(), Some(SWATCH_IDS[1])); } diff --git a/docs/mcp.md b/docs/mcp.md index 7bf5ae9956..dd910b162a 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3373,9 +3373,10 @@ in the `swatch.list` shape (`library` is an id or a name). `swatch.library.add { into the document as one undo step: a colour group's name brings the whole group, a swatch's name the swatch alone, no names the whole library; swatches the document already has (same name and paint) are reported under `existing` and not added again. `apply: "fill"|"stroke"` also applies the first one, in the same undo step (what clicking a -swatch in the library panel does). The pattern libraries Geometric Patterns (`geometric-patterns`) and Grit Textures -(`grit-textures`: seamless black grain, grit, sand, speckle, stipple, scratch and fleck tiles), category `patterns`, -hold pattern swatches drawn in code; adding one also adds its pattern, as pasting art painted with it does +swatch in the library panel does). The pattern libraries Geometric Patterns (`geometric-patterns`), Grit Textures +(`grit-textures`: seamless black grain, grit, sand, speckle, stipple, scratch and fleck tiles) and Optical Effects +(`optical-effects`: VectorCraft's own black wave, ring, warped-check, pinched-stripe, chevron and lens tiles), +category `patterns`, hold pattern swatches drawn in code; adding one also adds its pattern, as pasting art painted with it does (the same pattern the document has is reused, and a name another pattern or swatch has gets a number). `swatch.resetDefaults {replace?}` brings back the missing default swatches. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index b1ceaf70ea..e6bcb23301 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1294,7 +1294,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Double-clicking the Objects on Path tool opens Objects on Path Tool Options ([Objects on Path](https://helpx.adobe.com/illustrator/using/objects-on-path.html)): the nine-point Pivot grid and the Rotate field set the attachment point and angle of the objects attached next, and groups already attached keep theirs; Reset puts the center and 0° back. Save keeps the options across tool switches and saves them with the preferences (`tool.setOption {tool: "objectsOnPath"}`). - **Paint and appearance (M3):** - **CSS swatch libraries:** fractional linear and radial gradient stop positions are preserved, so nearby stops such as 12.5% and 12.890625% remain distinct instead of collapsing to 13%. Reference: [CSS Images 3 color stops](https://www.w3.org/TR/css-images-3/#color-stop-syntax). The original synthetic regression is `palette_io::tests::css_preserves_fractional_gradient_stop_positions` in `crates/color/src/palette_io.rs`. Scores and hours are unchanged. - - **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries, six gradient libraries (Organic Fades among them: soft, perceptually mixed multi-stop fades that thin out to transparent) and two pattern libraries (Geometric Patterns, and Grit Textures with eight seamless grain, speckle, stipple, scratch and fleck tiles; their swatches bring their patterns) in a library panel, saved as `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or CSS, and loaded from `.vcswatches`, `.gpl` and `.ase` files (`.ase` both ways: RGB, CMYK, Lab and Gray; global, spot and process; groups) and from color books (`.acb`, read only: the RGB, CMYK or Lab colors of a book such as a Pantone book a user owns, named with its prefix and suffix, spot unless it says process, #831); `swatch.library.copyToUser` copies a loaded library into the User Defined folder without replacing a file, and a file with the same extension and bytes as a User Defined library opens as that library. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate. + - **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries, six gradient libraries (Organic Fades among them: soft, perceptually mixed multi-stop fades that thin out to transparent) and three pattern libraries (Geometric Patterns; Grit Textures with eight seamless grain, speckle, stipple, scratch and fleck tiles; and Optical Effects with eight of VectorCraft's own seamless wave, ring, warped-check, stripe, chevron and lens tiles; their swatches bring their patterns) in a library panel, saved as `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or CSS, and loaded from `.vcswatches`, `.gpl` and `.ase` files (`.ase` both ways: RGB, CMYK, Lab and Gray; global, spot and process; groups) and from color books (`.acb`, read only: the RGB, CMYK or Lab colors of a book such as a Pantone book a user owns, named with its prefix and suffix, spot unless it says process, #831); `swatch.library.copyToUser` copies a loaded library into the User Defined folder without replacing a file, and a file with the same extension and bytes as a User Defined library opens as that library. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate. Editing, renaming, deleting or merging a global swatch also changes the colors linked to it in graph designs, the art of brushes and the tile that pattern editing's Cancel puts back, in the same undo step as the rest of the art; Default Swatches with replace unlinks them there too. Add Used Colors (`swatch.addUsedColors`) and Select All Unused (`swatch.unused`) look colors, swatch names, links, tints and gradients up in hash sets instead of searching every swatch and color for each color or linked paint (New Color Group from artwork too), and New Swatch with several colors (`swatch.new`), New Color Group (`swatch.newGroup`) and Edit Color Group (`swatch.editGroup`) name their new swatches from one set of the names already taken. The new swatches keep their names, order and links, and Select All Unused lists the same swatches. Evidence: `add_used_colors_adds_each_new_color_once_with_free_names`, `add_used_colors_global_links_every_paint_of_a_new_color`, `unused_matches_links_tints_colors_gradients_and_patterns_exactly`, `new_swatches_from_colors_are_numbered_after_names_already_taken`, `new_color_group_numbers_its_colors_after_the_group_and_its_moved_swatches`, `new_color_group_from_artwork_lists_each_color_once_with_free_names` and `edit_color_group_numbers_extra_colors_after_the_names_it_keeps` in `crates/engine/src/tests/swatchcmds.rs`; `free_swatch_names_match_naming_each_swatch_as_it_is_added` and `free_swatch_names_match_new_swatch_names_as_swatches_are_added` in `crates/doc/src/swatches.rs`. - **Colorized images (#1466):** a fill color applied to a selected grayscale or 1-bit image (`paint.setFill`) colorizes it with black, white, a process color or a spot color, and refuses a process color when the image has an alpha channel ([Convert color modes](https://web.archive.org/web/20251220223850/https://helpx.adobe.com/illustrator/desktop/manage-colors/select-and-adjust-colors/convert-color-modes.html), "Colorize Grayscale or 1-bit images"); black and white are accepted there too. The canvas draws each pixel in the color as far as the pixel is dark, with white pixels transparent; PDF export writes the image in the color (a spot color as its ink, Registration in every ink) with the image as its soft mask; Separations Preview and printed separations show it on its color's plates; native files keep the color. SVG writes the fill color through a coverage mask (or a colorized PNG in Tiny 1.2); EPS writes binary coverage as an image mask in the color's ink, and a grayscale image as ink samples scaled by coverage, with white pixels left out at Level 3 and gray midtones written over white.