diff --git a/README.md b/README.md index 36fe11fe9..cc7942e0b 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,7 @@ The scores are self-assessed from our code against Illustrator's public document - reliable exchange of Illustrator's own `.ai` files (layer structure in real files; Illustrator's editing data isn't written); - an interaction-fidelity pass covering every tool's modifiers and small behaviours; - 3D and Materials beyond the initial live Revolve (Extrude & Bevel, Inflate, Rotate, materials); -- about half of the Photoshop-style raster effects (27 of 57 still missing; Film Grain is available under Artistic, Halftone Pattern under Sketch) and the Effect Gallery; +- about half of the Photoshop-style raster effects (24 of 57 still missing; Cutout, Film Grain, Neon Glow and Poster Edges are available under Artistic, Halftone Pattern under Sketch) and the Effect Gallery; - CJK composition for vertical type (vertical type itself has initial support, with a Japanese interface); - scripting, and the rest of Variables (image and graph kinds, dataset import); - generative AI features. diff --git a/ROADMAP.md b/ROADMAP.md index 42c5885db..fa8152f64 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -29,7 +29,7 @@ blocking it ([Alpha gate](docs/roadmap.md#alpha-gate)). | | Value | Kind | |---|---|---| -| **Feature breadth** (Illustrator's menu items, tools, effects, formats exist) | **~88%** | measured in part: menu items 348/375 wired (92.8%), tools 89/92, Illustrator effects 44/50, Photoshop effects 30/57, 3D 4/4 (initial types, no materials) | +| **Feature breadth** (Illustrator's menu items, tools, effects, formats exist) | **~88%** | measured in part: menu items 348/375 wired (92.8%), tools 89/92, Illustrator effects 44/50, Photoshop effects 33/57, 3D 4/4 (initial types, no materials) | | **Feature depth** (weighted by use, scored by behaviour) | **~75%** | estimated | | **To beta** | **~170–270 h** one agent · ~50–80 h with 4–6 agents | estimated | | **To full parity** | **~360–590 h** one agent · ~95–165 h with 4–6 agents | estimated | @@ -76,7 +76,7 @@ The dimension hours overlap (feature rows hold some UI, format and AI work), so | Strokes, brushes, width profiles | 88% | 8–15 | | Appearance, transparency, graphic styles, masks | 97% | 1–2 | | Live vector effects | 85% | 6–10 | -| Raster effects (Effect Gallery) — [effects-parity.md](docs/effects-parity.md) | 49% | 23–39 | +| Raster effects (Effect Gallery) — [effects-parity.md](docs/effects-parity.md) | 49% | 21–35 | | 3D and Materials | ~25% | 30–55 | | Type core — [type-parity.md](docs/type-parity.md) | 80% | 14–19 | | Type advanced (CJK, spelling, Touch Type) | 50% | 20–28 | @@ -123,7 +123,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap 2. **`.ai` files from real users** (G9): layer structure, guides, speed: 10–20 h. 3. **Launch and stability on real machines** (G10): 20–35 h. 4. **Core-tool correctness** (G11: Shape Builder regions, Asset Export borders): 8–15 h. -5. **Raster effects** (G2) alongside: 28 filters and the Effect Gallery, 23–39 h. +5. **Raster effects** (G2) alongside: 24 filters and the Effect Gallery, 21–35 h. 6. **Then:** type core and advanced type (G4), the Illustrator 2026 additions (G13), 3D (G3). ## Progress log diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 8292c9ec7..76e1a8824 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -336,6 +336,13 @@ pub fn effect_catalog() -> Vec { ), r("stylize.feather", "Feather…", STYLIZE, "{radius: pt (5)}", json!({"radius": 5.0})), r("blur.gaussian", "Gaussian Blur…", BLUR, "{radius: pt (5)}", json!({"radius": 5.0})), + r( + "artistic.cutout", + "Cutout…", + ARTISTIC, + "{levels: 2..8 (4; steps per colour channel: more levels, more flat colours), edgeSimplicity: 0..10 (4; simpler, smoother edges), edgeFidelity: 1..3 (2; keeps more of the edges' detail)} redraws the object as flat pieces of coloured paper, each piece painted in the average colour that falls into it, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.", + json!({"levels": 4.0, "edgeSimplicity": 4.0, "edgeFidelity": 2.0}), + ), r( "artistic.filmGrain", "Film Grain…", @@ -343,6 +350,20 @@ pub fn effect_catalog() -> Vec { "{grain: 0..20 (4; monochrome noise in shadows and midtones), highlightArea: 0..20 (0; widens the bright highlight area), intensity: 0..10 (10; highlight strength and quieter highlight grain)}. Smooth document-space grain follows translations; minification uses approximate footprint filtering. Coverage and pure white are preserved; black takes light speckles, and saturated colour channels may clip. Grain 0 with either highlightArea 0 or intensity 0 leaves the art unchanged. Original visual approximation; chosen VectorCraft defaults.", json!({"grain": 4.0, "highlightArea": 0.0, "intensity": 10.0}), ), + r( + "artistic.neonGlow", + "Neon Glow…", + ARTISTIC, + "{glowSize: -24..24 (5; above 0 the light areas glow, below 0 the dark ones; farther from 0 spreads the glow wider), glowBrightness: 0..50 (15; a stronger glow over dimmer greys), color: \"#rrggbb\" (\"#4da6ff\"; the glow's colour)} redraws the object in greys lit by a coloured glow, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.", + json!({"glowSize": 5.0, "glowBrightness": 15.0, "color": "#4da6ff"}), + ), + r( + "artistic.posterEdges", + "Poster Edges…", + ARTISTIC, + "{edgeThickness: 0..10 (2; wider lines), edgeIntensity: 0..10 (1; darker lines, on fainter edges too), posterization: 0..6 (2; posterization + 2 levels per colour channel)} reduces the object's colours and draws black lines on its edges, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.", + json!({"edgeThickness": 2.0, "edgeIntensity": 1.0, "posterization": 2.0}), + ), r( "blur.radial", "Radial Blur…", diff --git a/crates/effects/src/pixel/artistic.rs b/crates/effects/src/pixel/artistic.rs index 87cc139f7..0a7e3aacc 100644 --- a/crates/effects/src/pixel/artistic.rs +++ b/crates/effects/src/pixel/artistic.rs @@ -1,11 +1,13 @@ -//! Effect › Artistic. Original filters built from public behaviour documentation: +//! Effect › Artistic (Cutout, Film Grain, Neon Glow, Poster Edges). Original filters built from +//! public behaviour documentation: //! and //! . //! These are visual approximations, not reproductions of a proprietary algorithm. -use super::PixelSpace; +use super::edges::sobel; use super::pixelate::{Place, cell, rand}; use super::texture::{luma, premultiply, straight}; +use super::{PixelSpace, blur_plane}; use vectorcraft_geom::Point; const SALT_FILM: u64 = 256; @@ -134,3 +136,135 @@ pub(super) fn film_grain(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSp *p = premultiply(c, f32::from(p[3]) / 255.0); }); } + +/// The coverage-weighted Gaussian softening of the planes `planes` (premultiplied by `alpha`) by +/// `sigma` pixels: each divided by the softened coverage, so transparency doesn't darken edges. +fn soften(planes: &mut [Vec], alpha: &[f32], w: usize, h: usize, sigma: f64) { + let mut a = alpha.to_vec(); + blur_plane(&mut a, w, h, sigma); + for plane in planes { + for (v, c) in plane.iter_mut().zip(alpha) { + *v *= c; + } + blur_plane(plane, w, h, sigma); + for (v, c) in plane.iter_mut().zip(&a) { + *v = if *c > 1e-4 { (*v / c).clamp(0.0, 1.0) } else { 0.0 }; + } + } +} + +/// The pixels' straight colour channels and their coverage (0..1), as planes. +fn planes(px: &[[u8; 4]]) -> ([Vec; 3], Vec) { + let colours = px.iter().map(|p| straight(*p)).collect::>(); + (std::array::from_fn(|k| colours.iter().map(|c| c[k]).collect()), px.iter().map(|p| f32::from(p[3]) / 255.0).collect()) +} + +/// Cutout: the object as flat pieces of coloured paper. Its colours, softened as `simplicity` +/// (0..10) asks and less as `fidelity` (1..3) keeps detail, fall into `levels` (2..8) steps per +/// colour channel; each step's piece is painted in the average colour of what falls into it, so +/// colours keep their hue. Slivers (the blends along softened edges) take the nearest large +/// piece's colour, so the pieces meet with crisp edges. Coverage is kept. +pub(super) fn cutout(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, levels: f64, simplicity: f64, fidelity: f64) { + if ![levels, simplicity, fidelity].iter().all(|v| v.is_finite()) || !(space.px.is_finite() && space.px > 0.0) { + return; + } + let levels = levels.round().clamp(2.0, 8.0) as usize; + // Simpler edges come from softer colours; more fidelity softens them less. + let sigma = 0.6 * simplicity.clamp(0.0, 10.0) * (4.0 - fidelity.clamp(1.0, 3.0)) / 3.0 / space.px; + let (mut colour, alpha) = planes(px); + soften(&mut colour, &alpha, w, h, sigma); + let at = |k: usize| [0, 1, 2].map(|i| colour[i].get(k).copied().unwrap_or(0.0)); + let piece = |c: [f32; 3]| c.iter().fold(0, |n, v| n * levels + ((v * levels as f32) as usize).min(levels - 1)); + // Each piece's colour: the coverage-weighted mean of the softened colours in it. + let mut sums = vec![[0.0f64; 4]; levels * levels * levels]; + for (k, a) in alpha.iter().enumerate() { + let c = at(k); + let Some(sum) = sums.get_mut(piece(c)) else { continue }; + for (s, v) in sum.iter_mut().zip(c) { + *s += f64::from(v * a); + } + sum[3] += f64::from(*a); + } + let mean = |sum: &[f64; 4]| [0, 1, 2].map(|i| (sum[i] / sum[3]) as f32); + let total: f64 = sums.iter().map(|s| s[3]).sum(); + let heaviest = sums.iter().map(|s| s[3]).fold(0.0, f64::max); + // Pieces of at least 1% of the object (and always the largest one) are kept. + let large: Vec<[f32; 3]> = sums.iter().filter(|s| s[3] > 0.0 && (s[3] >= 0.01 * total || s[3] >= heaviest)).map(mean).collect(); + let paint: Vec> = sums + .iter() + .map(|s| { + let c = (s[3] > 0.0).then(|| mean(s))?; + let far = |o: &[f32; 3]| o.iter().zip(c).map(|(a, b)| (a - b) * (a - b)).sum::(); + large.iter().min_by(|a, b| far(a).total_cmp(&far(b))).copied() + }) + .collect(); + for (k, p) in px.iter_mut().enumerate() { + if p[3] == 0 { + continue; + } + if let Some(Some(c)) = paint.get(piece(at(k))) { + *p = premultiply(*c, f32::from(p[3]) / 255.0); + } + } +} + +/// Poster Edges: the object's colours posterized to `posterization` + 2 (2..8) levels per +/// channel, with black lines drawn on the edges of any of its colour channels and of its outline. `thickness` (0..10) widens the lines and +/// `intensity` (0..10) darkens them and lets fainter edges take a line. Coverage is kept. +pub(super) fn poster_edges(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, thickness: f64, intensity: f64, posterization: f64) { + if ![thickness, intensity, posterization].iter().all(|v| v.is_finite()) || !(space.px.is_finite() && space.px > 0.0) { + return; + } + let steps = (posterization.round().clamp(0.0, 6.0) + 1.0) as f32; + // The edges of each premultiplied channel (so the object's outline too), a step of 1 reading 1. + let mut edges = vec![0.0f32; px.len()]; + for k in 0..4 { + let plane: Vec = px.iter().map(|p| f32::from(p[k]) / 255.0).collect(); + for (e, s) in edges.iter_mut().zip(sobel(&plane, w, h)) { + *e = e.max(s / 4.0); + } + } + // Wider lines spread the edges; scale them back up so a spread line stays as dark. + let sigma = (0.3 + 0.25 * thickness.clamp(0.0, 10.0)) / space.px; + blur_plane(&mut edges, w, h, sigma); + let gain = ((0.3 + 0.25 * intensity.clamp(0.0, 10.0)) * (1.0 + 1.5 * sigma.max(0.0))) as f32; + for (p, e) in px.iter_mut().zip(edges) { + if p[3] == 0 { + continue; + } + let t = ((e * gain - 0.25) / 0.5).clamp(0.0, 1.0); + let ink = t * t * (3.0 - 2.0 * t); + let c = straight(*p).map(|v| (v * steps).round() / steps * (1.0 - ink)); + *p = premultiply(c, f32::from(p[3]) / 255.0); + } +} + +/// How far (document units) Neon Glow's glow spreads at `size` (-24..24). +pub(super) fn glow_spread(size: f64) -> f64 { + 0.25 * size.abs().min(24.0) +} + +/// Neon Glow: the object in dimmed greys, lit by `colour` glowing from its light areas (`size` +/// above 0) or from its dark areas (below 0), spread as far as |`size`| (0..24) says and as +/// strong as `brightness` (0..50). Coverage is kept. +pub(super) fn neon_glow(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, size: f64, brightness: f64, colour: [f32; 3]) { + if ![size, brightness].iter().all(|v| v.is_finite()) || !(space.px.is_finite() && space.px > 0.0) { + return; + } + let strength = (brightness / 50.0).clamp(0.0, 1.0) as f32; + let tones: Vec = px.iter().map(|p| luma(straight(*p))).collect(); + let alpha: Vec = px.iter().map(|p| f32::from(p[3]) / 255.0).collect(); + let mut glow = [tones.iter().map(|l| if size < 0.0 { 1.0 - l } else { *l }).collect::>()]; + soften(&mut glow, &alpha, w, h, glow_spread(size) / space.px); + let [glow] = glow; + for ((p, tone), g) in px.iter_mut().zip(tones).zip(glow) { + if p[3] == 0 { + continue; + } + // The greys dim as the glow brightens, and the glow is screened over them. + let base = tone * (1.0 - 0.6 * strength); + let lit = (3.0 * g * strength).clamp(0.0, 1.0); + let c = colour.map(|v| 1.0 - (1.0 - base) * (1.0 - v.clamp(0.0, 1.0) * lit)); + *p = premultiply(c, f32::from(p[3]) / 255.0); + } +} diff --git a/crates/effects/src/pixel/mod.rs b/crates/effects/src/pixel/mod.rs index 9129951c3..7554efbca 100644 --- a/crates/effects/src/pixel/mod.rs +++ b/crates/effects/src/pixel/mod.rs @@ -1,4 +1,4 @@ -//! Photoshop-style raster effects (Effect › Artistic › Film Grain, Blur › Radial Blur and Smart Blur, Brush Strokes › +//! Photoshop-style raster effects (Effect › Artistic › Cutout, Film Grain, Neon Glow and Poster Edges, Blur › Radial Blur and Smart Blur, Brush Strokes › //! Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes //! and Sumi-e, Distort › Diffuse Glow, Glass and Ocean Ripple, Pixelate › Color Halftone, //! Crystallize, Mezzotint and Pointillize, Sharpen › Unsharp Mask, Sketch › Halftone Pattern, Stylize › @@ -39,8 +39,11 @@ pub use sketch::{HALFTONE_TYPES, HalftonePattern}; pub use texture::{GRAIN_TYPES, Grain, LIGHT_DIRECTIONS, Light, TEXTURES, Texture}; /// The Photoshop-style effect ids (all raster effects, see [`crate::is_raster`]). -pub const PIXEL_EFFECTS: [&str; 29] = [ +pub const PIXEL_EFFECTS: [&str; 32] = [ + "artistic.cutout", "artistic.filmGrain", + "artistic.neonGlow", + "artistic.posterEdges", "blur.radial", "blur.smart", "brushStrokes.accentedEdges", @@ -75,9 +78,19 @@ pub const PIXEL_EFFECTS: [&str; 29] = [ /// are in document units. #[derive(Clone, Debug, PartialEq)] pub enum PixelFx { + /// Artistic › Cutout: flat pieces of colour in `levels` (2..8) steps per channel, their + /// edges as simple as `simplicity` (0..10) and as faithful as `fidelity` (1..3) say. + Cutout { levels: f64, simplicity: f64, fidelity: f64 }, /// Artistic › Film Grain: monochrome grain in shadows and midtones, a wider bright /// highlight area (0..20), and tonal intensity (0..10), preserving coverage. FilmGrain { grain: f64, highlight: f64, intensity: f64 }, + /// Artistic › Neon Glow: dimmed greys lit by `colour` (straight RGB, 0..1) glowing from the + /// light areas (`size` above 0) or the dark ones (below 0), spread by |`size`| (-24..24), as + /// strong as `brightness` (0..50). + NeonGlow { size: f64, brightness: f64, colour: [f32; 3] }, + /// Artistic › Poster Edges: colours posterized to `posterization` + 2 levels (0..6) with black + /// lines on the edges, as thick as `thickness` (0..10) and as dark as `intensity` (0..10). + PosterEdges { thickness: f64, intensity: f64, posterization: f64 }, /// Blur › Radial Blur around the object's centre: along arcs (spin, an arc of `amount`°) or /// along rays (zoom, the content scaled by up to about ±`amount`/2 %), averaging 2^`passes` samples. RadialBlur { amount: f64, zoom: bool, passes: u32 }, @@ -209,6 +222,21 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option { levels.iter().find(|(name, _)| name.eq_ignore_ascii_case(q)).map_or(default, |(_, v)| *v) }; Some(match id { + "artistic.cutout" => PixelFx::Cutout { + levels: num(p, "levels", 4.0).round().clamp(2.0, 8.0), + simplicity: num(p, "edgeSimplicity", 4.0).clamp(0.0, 10.0), + fidelity: num(p, "edgeFidelity", 2.0).round().clamp(1.0, 3.0), + }, + "artistic.neonGlow" => PixelFx::NeonGlow { + size: num(p, "glowSize", 5.0).clamp(-24.0, 24.0), + brightness: num(p, "glowBrightness", 15.0).clamp(0.0, 50.0), + colour: crate::raster::color_param(p, vectorcraft_doc::color::Color::rgb(0.3, 0.65, 1.0)).to_rgb(), + }, + "artistic.posterEdges" => PixelFx::PosterEdges { + thickness: num(p, "edgeThickness", 2.0).clamp(0.0, 10.0), + intensity: num(p, "edgeIntensity", 1.0).clamp(0.0, 10.0), + posterization: num(p, "posterization", 2.0).round().clamp(0.0, 6.0), + }, "artistic.filmGrain" => PixelFx::FilmGrain { grain: num(p, "grain", 4.0).clamp(0.0, 20.0), highlight: num(p, "highlightArea", 0.0).clamp(0.0, 20.0), @@ -367,6 +395,9 @@ impl PixelFx { PixelFx::UnsharpMask { .. } | PixelFx::HalftonePattern { .. } => 0.0, PixelFx::GlowingEdges { width, smoothness, .. } => width.max(smoothness * 3.0), PixelFx::FilmGrain { .. } + | PixelFx::Cutout { .. } + | PixelFx::NeonGlow { .. } + | PixelFx::PosterEdges { .. } | PixelFx::ColorHalftone { .. } | PixelFx::Mezzotint { .. } | PixelFx::Craquelure { .. } @@ -409,6 +440,12 @@ impl PixelFx { PixelFx::ColorHalftone { max_radius, .. } => Some(3.0 * std::f64::consts::SQRT_2 * max_radius), PixelFx::Crystallize { cell } => Some(1.5 * cell), PixelFx::FilmGrain { .. } | PixelFx::Mezzotint { .. } | PixelFx::HalftonePattern { .. } => Some(0.0), + // Cutout's palette comes from the whole object. + PixelFx::Cutout { .. } => None, + // The softened tones around the pixel. + PixelFx::NeonGlow { size, .. } => Some(3.0 * artistic::glow_spread(size)), + // The edges' lines, spread round them. + PixelFx::PosterEdges { thickness, .. } => Some(3.0 * (0.3 + 0.25 * thickness) + 1.0), // The dots' points, and the softened colour around them. PixelFx::Pointillize { cell } => Some(2.5 * cell), // The softened tones around the pixel. @@ -429,7 +466,12 @@ impl PixelFx { let Some(px) = pixels(data, w, h) else { return }; let to_px = |len: f64| len / space.px.max(1e-9); match *self { + PixelFx::Cutout { levels, simplicity, fidelity } => artistic::cutout(px, w, h, space, levels, simplicity, fidelity), PixelFx::FilmGrain { grain, highlight, intensity } => artistic::film_grain(px, w, h, space, grain, highlight, intensity), + PixelFx::NeonGlow { size, brightness, colour } => artistic::neon_glow(px, w, h, space, size, brightness, colour), + PixelFx::PosterEdges { thickness, intensity, posterization } => { + artistic::poster_edges(px, w, h, space, thickness, intensity, posterization) + } PixelFx::RadialBlur { amount, zoom, passes } => blur::radial(px, w, h, space, amount, zoom, passes), PixelFx::SmartBlur { radius, threshold, samples, mode: SmartBlurMode::Normal } => { blur::smart(px, w, h, to_px(radius), threshold, samples) diff --git a/crates/effects/src/pixel/tests.rs b/crates/effects/src/pixel/tests.rs index caa08a70b..1a599a534 100644 --- a/crates/effects/src/pixel/tests.rs +++ b/crates/effects/src/pixel/tests.rs @@ -2093,3 +2093,156 @@ fn halftone_draws_in_black_and_white_only() { } } } + +/// The distinct colours of the covered pixels. +fn colours(d: &[u8]) -> std::collections::BTreeSet<[u8; 4]> { + d.as_chunks::<4>().0.iter().filter(|p| p[3] > 0).copied().collect() +} + +/// A left-to-right rainbow ramp, half covered at its bottom rows. +fn ramp(w: usize, h: usize) -> Vec { + image(w, h, |x, y| { + let t = x * 255 / (w - 1); + let a = if y + 4 >= h { 128 } else { 255 }; + let c = [t, 255 - t, (t * 3) % 256].map(|v| (v * a / 255) as u8); + [c[0], c[1], c[2], a as u8] + }) +} + +#[test] +fn artistic_params_take_defaults_and_stay_in_range() { + assert_eq!(fx("artistic.cutout", json!({})), PixelFx::Cutout { levels: 4.0, simplicity: 4.0, fidelity: 2.0 }); + assert_eq!( + fx("artistic.cutout", json!({"levels": 1e308, "edgeSimplicity": -5, "edgeFidelity": "NaN"})), + PixelFx::Cutout { levels: 8.0, simplicity: 0.0, fidelity: 2.0 } + ); + assert_eq!(fx("artistic.posterEdges", json!({})), PixelFx::PosterEdges { thickness: 2.0, intensity: 1.0, posterization: 2.0 }); + assert_eq!( + fx("artistic.posterEdges", json!({"edgeThickness": 99, "edgeIntensity": -1, "posterization": 6.4})), + PixelFx::PosterEdges { thickness: 10.0, intensity: 0.0, posterization: 6.0 } + ); + let PixelFx::NeonGlow { size, brightness, colour } = fx("artistic.neonGlow", json!({})) else { panic!() }; + assert_eq!((size, brightness), (5.0, 15.0)); + assert!((colour[0] - 0.3).abs() < 0.01 && (colour[1] - 0.65).abs() < 0.01 && colour[2] == 1.0, "{colour:?}"); + assert_eq!( + fx("artistic.neonGlow", json!({"glowSize": -1e308, "glowBrightness": 80, "color": "#ff0000"})), + PixelFx::NeonGlow { size: -24.0, brightness: 50.0, colour: [1.0, 0.0, 0.0] } + ); + for id in ["artistic.cutout", "artistic.neonGlow", "artistic.posterEdges"] { + assert_eq!(fx(id, json!({})).outset(Rect::new(0.0, 0.0, 40.0, 30.0)), 0.0, "{id} keeps the object's shape"); + } +} + +#[test] +fn cutout_paints_a_few_flat_colours_and_keeps_coverage() { + let (w, h) = (64, 24); + let src = ramp(w, h); + for levels in [2, 3, 5, 8] { + let mut out = src.clone(); + fx("artistic.cutout", json!({"levels": levels})).apply(&mut out, w, h, &space(w, h)); + let opaque: std::collections::BTreeSet<_> = colours(&out).into_iter().filter(|p| p[3] == 255).collect(); + assert!(opaque.len() <= levels * levels * levels && opaque.len() >= 2, "{levels} levels: {} colours", opaque.len()); + assert!(opaque.len() < 64, "flat pieces, not a ramp: {levels} levels, {} colours", opaque.len()); + assert!(out.as_chunks::<4>().0.iter().zip(src.as_chunks::<4>().0).all(|(o, s)| o[3] == s[3])); + } +} + +#[test] +fn cutout_simplicity_smooths_the_pieces_edges() { + // A noisy grey: simpler edges join the speckles into fewer changes between neighbours. + let (w, h) = (48, 48); + let src = image(w, h, |x, y| grey(if (x * 7 + y * 13) % 5 < 2 { 60 } else { 190 })); + let changes = |simplicity: f64| { + let mut out = src.clone(); + fx("artistic.cutout", json!({"levels": 2, "edgeSimplicity": simplicity, "edgeFidelity": 1})).apply(&mut out, w, h, &space(w, h)); + out.as_chunks::<4>().0.windows(2).filter(|p| p[0] != p[1]).count() + }; + assert!(changes(10.0) * 4 < changes(0.0), "{} vs {}", changes(10.0), changes(0.0)); +} + +#[test] +fn poster_edges_draw_black_lines_on_edges_and_posterize_the_rest() { + let (w, h) = (40, 20); + // Two flat halves: a line where they meet, posterized colour away from it. + let src = image(w, h, |x, _| if x < w / 2 { [200, 90, 30, 255] } else { [30, 120, 210, 255] }); + let mut out = src.clone(); + fx("artistic.posterEdges", json!({"edgeThickness": 2, "edgeIntensity": 10, "posterization": 0})).apply(&mut out, w, h, &space(w, h)); + let line = at(&out, w, w / 2, h / 2); + assert!(line[0] < 40 && line[1] < 40 && line[2] < 40, "a dark line on the edge: {line:?}"); + // 2 levels per channel: every far pixel is 0 or 255 in each channel. + for x in [3, w - 4] { + let p = at(&out, w, x, h / 2); + assert!(p[..3].iter().all(|v| *v == 0 || *v == 255), "{x}: {p:?}"); + } + // Inside a flat colour, away from the object's outline, there are no lines: it only posterizes. + let mut flat = image(w, h, |_, _| [200, 90, 30, 255]); + fx("artistic.posterEdges", json!({"edgeIntensity": 10, "posterization": 6})).apply(&mut flat, w, h, &space(w, h)); + let inside: std::collections::BTreeSet<_> = (6..h - 6).flat_map(|y| (6..w - 6).map(move |x| (x, y))).map(|(x, y)| at(&flat, w, x, y)).collect(); + assert_eq!(inside.len(), 1, "{inside:?}"); + assert!(at(&flat, w, 0, h / 2)[0] < 60, "the outline takes a line"); +} + +#[test] +fn poster_edges_thickness_widens_the_lines() { + let (w, h) = (60, 10); + let src = image(w, h, |x, _| grey(if x < w / 2 { 30 } else { 230 })); + let dark = |thickness: f64| { + let mut out = src.clone(); + fx("artistic.posterEdges", json!({"edgeThickness": thickness, "edgeIntensity": 10, "posterization": 6})).apply(&mut out, w, h, &space(w, h)); + (0..w).filter(|x| at(&out, w, *x, h / 2)[0] < 20 && (*x >= w / 2 || at(&src, w, *x, h / 2)[0] > 20)).count() + }; + assert!(dark(10.0) > dark(0.0), "{} vs {}", dark(10.0), dark(0.0)); +} + +#[test] +fn neon_glow_lights_the_bright_or_the_dark_areas_in_its_colour() { + let (w, h) = (40, 20); + let src = image(w, h, |x, _| grey(if x < w / 2 { 20 } else { 235 })); + let run = |size: f64| { + let mut out = src.clone(); + fx("artistic.neonGlow", json!({"glowSize": size, "glowBrightness": 40, "color": "#ff0000"})).apply(&mut out, w, h, &space(w, h)); + (at(&out, w, 3, h / 2), at(&out, w, w - 4, h / 2)) + }; + // Red glow: the red channel rises above green and blue where it glows. + let red = |p: [u8; 4]| i32::from(p[0]) - i32::from(p[1]); + let (dark, light) = run(8.0); + assert!(red(light) > 60 && red(light) > red(dark) + 40, "light areas glow: {dark:?} {light:?}"); + let (dark, light) = run(-8.0); + assert!(red(dark) > 60 && red(dark) > red(light) + 40, "dark areas glow: {dark:?} {light:?}"); + // No brightness: dimmed greys only. + let mut out = src.clone(); + fx("artistic.neonGlow", json!({"glowBrightness": 0})).apply(&mut out, w, h, &space(w, h)); + assert!(out.as_chunks::<4>().0.iter().all(|p| p[0] == p[1] && p[1] == p[2] && p[3] == 255)); +} + +#[test] +fn artistic_filters_look_the_same_at_any_resolution() { + // The same art at 1 and at 2 pixels per point: the filter at the coarse resolution matches + // the fine result sampled at the same points. + let (w, h) = (48, 32); + let art = |x: f64, y: f64| grey(if (x - 24.0).hypot(y - 16.0) < 10.0 { 230 } else { 40 }); + let coarse = image(w, h, |x, y| art(x as f64 + 0.5, y as f64 + 0.5)); + let fine = image(2 * w, 2 * h, |x, y| art((x as f64 + 0.5) / 2.0, (y as f64 + 0.5) / 2.0)); + let mut half = space(2 * w, 2 * h); + half.to_doc = Affine::scale(0.5); + half.px = 0.5; + half.center = Point::new(w as f64 / 2.0, h as f64 / 2.0); + for (id, p) in [ + ("artistic.cutout", json!({"edgeSimplicity": 6})), + ("artistic.neonGlow", json!({"glowSize": 12, "glowBrightness": 30})), + ("artistic.posterEdges", json!({"edgeThickness": 6, "edgeIntensity": 6})), + ] { + let (mut a, mut b) = (coarse.clone(), fine.clone()); + fx(id, p.clone()).apply(&mut a, w, h, &space(w, h)); + fx(id, p).apply(&mut b, 2 * w, 2 * h, &half); + let diff: f64 = (0..h) + .flat_map(|y| (0..w).map(move |x| (x, y))) + .map(|(x, y)| { + let (p, q) = (at(&a, w, x, y), at(&b, 2 * w, 2 * x, 2 * y)); + (0..3).map(|k| f64::from(p[k].abs_diff(q[k]))).sum::() / 3.0 + }) + .sum::() + / (w * h) as f64; + assert!(diff < 12.0, "{id}: mean difference {diff}"); + } +} diff --git a/crates/engine/src/tests_rasterfilters.rs b/crates/engine/src/tests_rasterfilters.rs index 3ac0fdfc5..fe7be6208 100644 --- a/crates/engine/src/tests_rasterfilters.rs +++ b/crates/engine/src/tests_rasterfilters.rs @@ -1,4 +1,4 @@ -//! The Photoshop-style raster effects (Effect › Blur › Radial Blur and Smart Blur, Brush Strokes, +//! The Photoshop-style raster effects (Effect › Artistic, Blur › Radial Blur and Smart Blur, Brush Strokes, //! Distort › Diffuse Glow, Glass and Ocean Ripple, Pixelate › Color Halftone, Crystallize, Mezzotint and Pointillize, Sharpen › Unsharp Mask, Sketch › Halftone Pattern, Texture › //! Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass and Texturizer, Video): applied and //! edited as commands, listed in the catalogue, drawn on the canvas, and written to PDF and SVG @@ -30,8 +30,9 @@ const BRUSH_STROKES: [(&str, &str); 8] = [ ("brushStrokes.sumiE", "{}"), ]; -/// Effect › Artistic › Film Grain, at its defaults. -const ARTISTIC: [(&str, &str); 1] = [("artistic.filmGrain", "{}")]; +/// Effect › Artistic, at their defaults. +const ARTISTIC: [(&str, &str); 4] = + [("artistic.cutout", "{}"), ("artistic.filmGrain", "{}"), ("artistic.neonGlow", "{}"), ("artistic.posterEdges", "{}")]; /// Effect › Distort (Diffuse Glow with its highlights glowing from a low brightness up, as the /// striped art's reds are dark). @@ -175,6 +176,51 @@ fn halftone_pattern_settings_survive_a_save_and_open() { } } +/// The Artistic filters' settings are saved with the document and draw the same when it opens +/// again; changing any one of them draws differently. +#[test] +fn artistic_settings_survive_a_save_and_open() { + for (id, set, changes) in [ + ( + "artistic.cutout", + json!({"levels": 3.0, "edgeSimplicity": 2.0, "edgeFidelity": 3.0}), + [json!({"levels": 2.0}), json!({"edgeSimplicity": 10.0})], + ), + ( + "artistic.neonGlow", + json!({"glowSize": -6.0, "glowBrightness": 30.0, "color": "#ff3300"}), + [json!({"glowSize": 6.0}), json!({"color": "#00ff00"})], + ), + ( + "artistic.posterEdges", + json!({"edgeThickness": 4.0, "edgeIntensity": 5.0, "posterization": 1.0}), + [json!({"edgeIntensity": 0.0}), json!({"posterization": 6.0})], + ), + ] { + let (mut s, g) = striped(); + // Black and a grey beside the reds: Cutout puts the grey with black at 2 levels and apart + // from it at 3. + let mut ids = vec![g]; + for (x, color) in [(55, "#000000"), (115, "#666666")] { + let id = s.execute("shape.ellipse", &json!({"x": x, "y": 60, "width": 30, "height": 30})).unwrap()["id"].as_u64().unwrap(); + s.execute("paint.setFill", &json!({"color": color, "ids": [id]})).unwrap(); + ids.push(id); + } + s.execute("select.set", &json!({"ids": ids})).unwrap(); + let g = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap(); + s.execute("effect.apply", &json!({"effect": id, "ids": [g], "params": set})).unwrap(); + let drawn = render(&s); + let doc = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap(); + assert_eq!(doc.node(NodeId(g)).unwrap().appearance.effects[0].params, set, "{id}"); + assert_eq!(Renderer::new().render_region(&doc, Rect::new(0.0, 0.0, 200.0, 200.0), 1.0, true).pixels, drawn.pixels, "{id}"); + for change in changes { + s.execute("effect.setParams", &json!({"index": 0, "ids": [g], "params": change})).unwrap(); + assert!(mean_diff(&drawn, &render(&s)) > 0.001, "{id}: {change}"); + s.execute("effect.setParams", &json!({"index": 0, "ids": [g], "params": set})).unwrap(); + } + } +} + /// A PNG of rings around its center, 12 × 10 pixels. fn rings_png() -> Vec { let img = image::RgbaImage::from_fn(12, 10, |x, y| { diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index 1a24e5a25..a34046c22 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -329,6 +329,9 @@ fn structured_junk() { ("effect.apply", json!({"effect": "pixelate.crystallize", "params": {"cellSize": -1e308}})), ("effect.apply", json!({"effect": "pixelate.mezzotint", "params": {"type": 5}})), ("effect.apply", json!({"effect": "pixelate.pointillize", "params": {"cellSize": "NaN"}})), + ("effect.apply", json!({"effect": "artistic.cutout", "params": {"levels": 1e308, "edgeSimplicity": "NaN", "edgeFidelity": -1e308}})), + ("effect.apply", json!({"effect": "artistic.neonGlow", "params": {"glowSize": -1e308, "glowBrightness": "1e999", "color": [1]}})), + ("effect.apply", json!({"effect": "artistic.posterEdges", "params": {"edgeThickness": 1e308, "edgeIntensity": [], "posterization": -7}})), ("effect.apply", json!({"effect": "sketch.halftonePattern", "params": {"size": -1e308, "contrast": "NaN", "patternType": 3}})), ("effect.apply", json!({"effect": "texture.craquelure", "params": {"crackSpacing": -1e308, "crackDepth": "NaN", "crackBrightness": 1e308}})), ("effect.apply", json!({"effect": "texture.grain", "params": {"intensity": 1e308, "contrast": [1], "grainType": 7}})), diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index fdd8309cb..ec4efb4d9 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -198,6 +198,9 @@ fn effect_dialog_refuses_a_color_it_cannot_draw() { #[test] fn raster_filter_dialogs_preview_and_apply() { for (effect, key, value) in [ + ("artistic.cutout", "levels", "6"), + ("artistic.neonGlow", "color", "#ff00ff"), + ("artistic.posterEdges", "edgeThickness", "5"), ("blur.radial", "method", "zoom"), ("blur.smart", "quality", "high"), ("blur.smart", "blurMode", "overlayEdge"), diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 868d37be3..f49e0373b 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4385,6 +4385,20 @@ lighting Light Светлина Reset Ruler Origin Нулиране началото на линийката Artistic Художествени Film Grain… Филмова зърнистост… + Cutout… Апликация… + Cutout Апликация + Neon Glow… Неоново сияние… + Neon Glow Неоново сияние + Poster Edges… Плакатни ръбове… + Poster Edges Плакатни ръбове + Levels Нива + Edge Simplicity Простота на ръбовете + Edge Fidelity Точност на ръбовете + Glow Size Размер на сиянието + Glow Brightness Яркост на сиянието + Edge Thickness Дебелина на ръбовете + Edge Intensity Интензивност на ръбовете + Posterization Постеризация Highlight Area Зона на осветяване @msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) компресираният файл се разпакетира до повече, отколкото VectorCraft чете ({_1} МБ документ, {_2} МБ на изображение) @msg the document is too large to save compressed ({0}): save it with Use Compression off документът е твърде голям за запазване в компресиран вид ({0}): запазете го с изключена опция „Използване на компресия“ diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 11cd140b9..1b606dbf1 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4864,6 +4864,20 @@ imageTrace Outline Obrys DWG (use DXF) DWG (použijte DXF) Artistic Umělecké Film Grain… Filmové zrno… + Cutout… Výřez… + Cutout Výřez + Neon Glow… Neonová záře… + Neon Glow Neonová záře + Poster Edges… Plakátové okraje… + Poster Edges Plakátové okraje + Levels Úrovně + Edge Simplicity Jednoduchost okrajů + Edge Fidelity Věrnost okrajů + Glow Size Velikost záře + Glow Brightness Jas záře + Edge Thickness Tloušťka okrajů + Edge Intensity Intenzita okrajů + Posterization Posterizace Highlight Area Oblast světel # Czech render-site follow-up (AI drafts; native review requested). diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 111546678..e17e32848 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4434,6 +4434,20 @@ lighting Light Licht Reset Ruler Origin Linealursprung zurücksetzen Artistic Kunstfilter Film Grain… Filmkörnung… + Cutout… Ausschnitt… + Cutout Ausschnitt + Neon Glow… Neonglühen… + Neon Glow Neonglühen + Poster Edges… Posterkanten… + Poster Edges Posterkanten + Levels Stufen + Edge Simplicity Kantenvereinfachung + Edge Fidelity Kantentreue + Glow Size Leuchtgröße + Glow Brightness Leuchthelligkeit + Edge Thickness Kantenstärke + Edge Intensity Kantenintensität + Posterization Tontrennung Grain Körnung Highlight Area Lichterbereich @msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) die komprimierte Datei entpackt sich auf mehr, als VectorCraft liest ({_1} MB Dokument, {_2} MB pro Bild) diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 28b3d74b0..7b1e32278 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2146,6 +2146,20 @@ lighting Light Φως Left Diagonal Αριστερή διαγώνιος Artistic Καλλιτεχνικό Film Grain… Κόκκος φιλμ… + Cutout… Αποκοπή… + Cutout Αποκοπή + Neon Glow… Λάμψη νέον… + Neon Glow Λάμψη νέον + Poster Edges… Άκρες αφίσας… + Poster Edges Άκρες αφίσας + Levels Επίπεδα + Edge Simplicity Απλότητα άκρων + Edge Fidelity Πιστότητα άκρων + Glow Size Μέγεθος λάμψης + Glow Brightness Φωτεινότητα λάμψης + Edge Thickness Πάχος άκρων + Edge Intensity Ένταση άκρων + Posterization Αφισοποίηση Highlight Area Περιοχή φωτεινών τόνων Skew: Λοξότητα: Inside Εσωτερικά diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index d70c81d50..7e166b82f 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4452,6 +4452,20 @@ lighting Light Luz Reset Ruler Origin Restablecer el origen de las reglas Artistic Artístico Film Grain… Granulado de película… + Cutout… Recorte… + Cutout Recorte + Neon Glow… Resplandor de neón… + Neon Glow Resplandor de neón + Poster Edges… Bordes de póster… + Poster Edges Bordes de póster + Levels Niveles + Edge Simplicity Simplicidad de bordes + Edge Fidelity Fidelidad de bordes + Glow Size Tamaño del resplandor + Glow Brightness Brillo del resplandor + Edge Thickness Grosor de bordes + Edge Intensity Intensidad de bordes + Posterization Posterización Highlight Area Área de iluminaciones @msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) el archivo comprimido ocupa al descomprimirse más de lo que VectorCraft lee ({_1} MB de documento, {_2} MB por imagen) @msg the document is too large to save compressed ({0}): save it with Use Compression off el documento es demasiado grande para guardarlo comprimido ({0}): guárdelo con Usar compresión desactivado diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 7e7e2658b..f66c35932 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4772,6 +4772,20 @@ imageTrace Outline Ääriviiva DWG (use DXF) DWG (käytä DXF:ää) Artistic Taiteelliset Film Grain… Filmin rakeisuus… + Cutout… Leikkaus… + Cutout Leikkaus + Neon Glow… Neonhehku… + Neon Glow Neonhehku + Poster Edges… Julisteen reunat… + Poster Edges Julisteen reunat + Levels Tasot + Edge Simplicity Reunojen yksinkertaisuus + Edge Fidelity Reunojen tarkkuus + Glow Size Hehkun koko + Glow Brightness Hehkun kirkkaus + Edge Thickness Reunojen paksuus + Edge Intensity Reunojen voimakkuus + Posterization Posterointi Highlight Area Korostusalue file-filter All readable files Kaikki luettavat tiedostot file-filter All placeable files Kaikki sijoitettavat tiedostot diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 196c7d187..08543af71 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4462,6 +4462,20 @@ lighting Light Lumière Reset Ruler Origin Réinitialiser l’origine des règles Artistic Artistique Film Grain… Grain photographique… + Cutout… Découpage… + Cutout Découpage + Neon Glow… Néon… + Neon Glow Néon + Poster Edges… Bords d’affiche… + Poster Edges Bords d’affiche + Levels Niveaux + Edge Simplicity Simplicité des contours + Edge Fidelity Fidélité des contours + Glow Size Taille de la lueur + Glow Brightness Luminosité de la lueur + Edge Thickness Épaisseur des contours + Edge Intensity Intensité des contours + Posterization Postérisation Highlight Area Zone des tons clairs @msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) le fichier compressé dépasse une fois décompressé ce que VectorCraft lit ({_1} Mo de document, {_2} Mo par image) @msg the document is too large to save compressed ({0}): save it with Use Compression off le document est trop volumineux pour être enregistré compressé ({0}) : enregistrez-le avec Utiliser la compression désactivé diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 95439c826..ae91093f2 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1556,6 +1556,20 @@ file-filter Templates Sablonok Select Segments Szakaszok kijelölése Artistic Művészi Film Grain… Filmszemcse… + Cutout… Kivágás… + Cutout Kivágás + Neon Glow… Neonfény… + Neon Glow Neonfény + Poster Edges… Poszterszélek… + Poster Edges Poszterszélek + Levels Szintek + Edge Simplicity Élek egyszerűsége + Edge Fidelity Élek hűsége + Glow Size Fény mérete + Glow Brightness Fény fényereje + Edge Thickness Élek vastagsága + Edge Intensity Élek intenzitása + Posterization Poszterizálás Highlight Area Csúcsfény-terület English English Graph Design Diagramminta diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 6eb20afdb..8794550ee 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4479,6 +4479,20 @@ lighting Light Luce Reset Ruler Origin Ripristina origine dei righelli Artistic Artistico Film Grain… Grana della pellicola… + Cutout… Ritaglio… + Cutout Ritaglio + Neon Glow… Bagliore neon… + Neon Glow Bagliore neon + Poster Edges… Bordi poster… + Poster Edges Bordi poster + Levels Livelli + Edge Simplicity Semplicità dei bordi + Edge Fidelity Fedeltà dei bordi + Glow Size Dimensione del bagliore + Glow Brightness Luminosità del bagliore + Edge Thickness Spessore dei bordi + Edge Intensity Intensità dei bordi + Posterization Posterizzazione Highlight Area Area luci @msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) il file compresso, una volta decompresso, supera ciò che VectorCraft legge ({_1} MB di documento, {_2} MB per immagine) @msg the document is too large to save compressed ({0}): save it with Use Compression off il documento è troppo grande per essere salvato compresso ({0}): salvalo con Usa compressione disattivato diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 017e4bdb7..814d5cfbc 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4519,6 +4519,20 @@ lighting Light ライト Reset Ruler Origin 定規の原点をリセット Artistic アーティスティック Film Grain… フィルム粒子… + Cutout… カットアウト… + Cutout カットアウト + Neon Glow… ネオングロー… + Neon Glow ネオングロー + Poster Edges… ポスターエッジ… + Poster Edges ポスターエッジ + Levels レベル数 + Edge Simplicity エッジの単純さ + Edge Fidelity エッジの正確さ + Glow Size 光彩のサイズ + Glow Brightness 光彩の明るさ + Edge Thickness エッジの太さ + Edge Intensity エッジの強さ + Posterization ポスタリゼーション Highlight Area ハイライト領域 @msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) 圧縮ファイルを展開すると VectorCraft が読み込める量を超えます (ドキュメント {_1} MB、画像 1 枚あたり {_2} MB) @msg the document is too large to save compressed ({0}): save it with Use Compression off ドキュメントが大きすぎて圧縮保存できません ({0}): 「圧縮を使用」をオフにして保存してください diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 273ec27bb..5435a5220 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4443,6 +4443,20 @@ lighting Light 광원 X11 (XWayland) X11 (XWayland) Artistic 예술 효과 Film Grain… 필름 그레인… + Cutout… 오려내기… + Cutout 오려내기 + Neon Glow… 네온 글로… + Neon Glow 네온 글로 + Poster Edges… 포스터 가장자리… + Poster Edges 포스터 가장자리 + Levels 레벨 수 + Edge Simplicity 가장자리 단순화 + Edge Fidelity 가장자리 충실도 + Glow Size 광선 크기 + Glow Brightness 광선 밝기 + Edge Thickness 가장자리 두께 + Edge Intensity 가장자리 강도 + Posterization 포스터화 Highlight Area 하이라이트 영역 @msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) 압축된 파일의 압축을 풀면 VectorCraft가 읽을 수 있는 한도를 넘습니다(문서 {_1}MB, 이미지당 {_2}MB) @msg the document is too large to save compressed ({0}): save it with Use Compression off 문서가 너무 커서 압축 저장할 수 없습니다({0}): 압축 사용을 끄고 저장하세요 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index bee75dea0..964a12199 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4691,6 +4691,20 @@ imageTrace Outline Kontur DWG (use DXF) DWG (użyj DXF) Artistic Artystyczne Film Grain… Ziarno filmu… + Cutout… Wycinanka… + Cutout Wycinanka + Neon Glow… Neonowa poświata… + Neon Glow Neonowa poświata + Poster Edges… Krawędzie plakatu… + Poster Edges Krawędzie plakatu + Levels Poziomy + Edge Simplicity Prostota krawędzi + Edge Fidelity Wierność krawędzi + Glow Size Rozmiar poświaty + Glow Brightness Jasność poświaty + Edge Thickness Grubość krawędzi + Edge Intensity Intensywność krawędzi + Posterization Posteryzacja Highlight Area Obszar świateł file-filter All readable files Wszystkie obsługiwane pliki file-filter All placeable files Wszystkie pliki do umieszczenia diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 7e0491f82..1c7ab2b44 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3858,6 +3858,20 @@ axis Both Ambos Reset Ruler Origin Redefinir origem das réguas Artistic Artístico Film Grain… Granulação de filme… + Cutout… Recorte… + Cutout Recorte + Neon Glow… Brilho neon… + Neon Glow Brilho neon + Poster Edges… Bordas de pôster… + Poster Edges Bordas de pôster + Levels Níveis + Edge Simplicity Simplicidade das bordas + Edge Fidelity Fidelidade das bordas + Glow Size Tamanho do brilho + Glow Brightness Intensidade do brilho + Edge Thickness Espessura das bordas + Edge Intensity Intensidade das bordas + Posterization Posterização Grain Grão Highlight Area Área de realces @msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) o arquivo compactado se expande para mais do que o VectorCraft lê ({_1} MB de documento, {_2} MB por imagem) diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index f2da1ab1f..569d8523c 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4475,6 +4475,20 @@ lighting Light Свет Reset Ruler Origin Сбросить начало координат линеек Artistic Художественные Film Grain… Зернистость плёнки… + Cutout… Аппликация… + Cutout Аппликация + Neon Glow… Неоновое свечение… + Neon Glow Неоновое свечение + Poster Edges… Края плаката… + Poster Edges Края плаката + Levels Уровни + Edge Simplicity Простота краёв + Edge Fidelity Точность краёв + Glow Size Размер свечения + Glow Brightness Яркость свечения + Edge Thickness Толщина краёв + Edge Intensity Интенсивность краёв + Posterization Постеризация Highlight Area Область светов @msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) сжатый файл распаковывается в объём больше, чем читает VectorCraft ({_1} МБ документа, {_2} МБ на изображение) @msg the document is too large to save compressed ({0}): save it with Use Compression off документ слишком велик для сохранения со сжатием ({0}): сохраните его с выключенным параметром «Использовать сжатие» diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 2af5f24ec..602df2611 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4384,6 +4384,20 @@ lighting Light Ljus Reset Ruler Origin Återställ linjalens origo Artistic Konstnärligt Film Grain… Filmkorn… + Cutout… Urklipp… + Cutout Urklipp + Neon Glow… Neonsken… + Neon Glow Neonsken + Poster Edges… Affischkanter… + Poster Edges Affischkanter + Levels Nivåer + Edge Simplicity Kantförenkling + Edge Fidelity Kanttrohet + Glow Size Skenets storlek + Glow Brightness Skenets ljusstyrka + Edge Thickness Kanttjocklek + Edge Intensity Kantintensitet + Posterization Posterisering Highlight Area Högdagerområde @msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) den komprimerade filen packas upp till mer än VectorCraft läser ({_1} MB dokument, {_2} MB per bild) @msg the document is too large to save compressed ({0}): save it with Use Compression off dokumentet är för stort för att sparas komprimerat ({0}): spara det med Använd komprimering avstängt diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 72d8cd754..a0b0be3c4 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4459,6 +4459,20 @@ lighting Light Світло Reset Ruler Origin Скинути початок координат лінійок Artistic Художні Film Grain… Зернистість плівки… + Cutout… Аплікація… + Cutout Аплікація + Neon Glow… Неонове сяйво… + Neon Glow Неонове сяйво + Poster Edges… Плакатні краї… + Poster Edges Плакатні краї + Levels Рівні + Edge Simplicity Простота країв + Edge Fidelity Точність країв + Glow Size Розмір сяйва + Glow Brightness Яскравість сяйва + Edge Thickness Товщина країв + Edge Intensity Інтенсивність країв + Posterization Постеризація Highlight Area Область світлих тонів @msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) стислий файл розпаковується в обсяг більший, ніж читає VectorCraft ({_1} МБ документа, {_2} МБ на зображення) @msg the document is too large to save compressed ({0}): save it with Use Compression off документ завеликий для збереження зі стиском ({0}): збережіть його з вимкненим параметром «Використовувати стиск» diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index a9bf4905f..bd71a9633 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1248,6 +1248,20 @@ Fill: click for swatches, Shift-click for the color mixer Tô: bấm để xem mẫu màu, Shift-bấm để trộn màu Fills Vùng tô Film Grain… Hạt phim… + Cutout… Cắt dán… + Cutout Cắt dán + Neon Glow… Ánh neon… + Neon Glow Ánh neon + Poster Edges… Viền áp phích… + Poster Edges Viền áp phích + Levels Số mức + Edge Simplicity Độ đơn giản của cạnh + Edge Fidelity Độ trung thực của cạnh + Glow Size Kích thước ánh sáng + Glow Brightness Độ sáng của ánh sáng + Edge Thickness Độ dày cạnh + Edge Intensity Cường độ cạnh + Posterization Áp phích hóa Find Tìm Find and Replace… Tìm và thay thế… Find Font Tìm phông diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 6b9fbe775..96d26f3c4 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4747,6 +4747,20 @@ lighting Light 光源 Reset Ruler Origin 重置标尺原点 Artistic 艺术效果 Film Grain… 胶片颗粒… + Cutout… 剪纸… + Cutout 剪纸 + Neon Glow… 霓虹灯光… + Neon Glow 霓虹灯光 + Poster Edges… 海报边缘… + Poster Edges 海报边缘 + Levels 色阶数 + Edge Simplicity 边缘简化度 + Edge Fidelity 边缘逼真度 + Glow Size 发光大小 + Glow Brightness 发光亮度 + Edge Thickness 边缘厚度 + Edge Intensity 边缘强度 + Posterization 海报化 Highlight Area 高光区域 Concentric Dividers: 同心分隔线: Radial Dividers: 径向分隔线: diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 1fca9a5de..b85fdfc75 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3819,6 +3819,20 @@ lighting Light 光源 Reset Ruler Origin 重設尺標原點 Artistic 藝術效果 Film Grain… 底片顆粒… + Cutout… 剪紙… + Cutout 剪紙 + Neon Glow… 霓虹光… + Neon Glow 霓虹光 + Poster Edges… 海報邊緣… + Poster Edges 海報邊緣 + Levels 色階數 + Edge Simplicity 邊緣簡化度 + Edge Fidelity 邊緣逼真度 + Glow Size 光暈大小 + Glow Brightness 光暈亮度 + Edge Thickness 邊緣厚度 + Edge Intensity 邊緣強度 + Posterization 海報化 Highlight Area 亮部範圍 Concentric Dividers: 同心分隔線: Radial Dividers: 徑向分隔線: diff --git a/crates/ui-egui/src/tests_labels.rs b/crates/ui-egui/src/tests_labels.rs index 73fb99b42..a394f1a99 100644 --- a/crates/ui-egui/src/tests_labels.rs +++ b/crates/ui-egui/src/tests_labels.rs @@ -69,13 +69,14 @@ fn effect_menu_lists_outline_object_between_offset_path_and_outline_stroke() { } #[test] -fn effect_menu_lists_film_grain_in_artistic_before_blur() { +fn effect_menu_lists_the_artistic_filters_before_blur() { let app = app(); let entries = menus::menu_entries(&app); let artistic: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Artistic"]).collect(); - assert_eq!(artistic.len(), 1); - assert_eq!(artistic[0].label, "Film Grain…"); - assert_eq!(artistic[0].command.as_deref(), Some("effect.dialog")); + let labels: Vec<&str> = artistic.iter().map(|e| e.label.as_str()).collect(); + assert_eq!(labels, ["Cutout…", "Film Grain…", "Neon Glow…", "Poster Edges…"]); + assert!(artistic.iter().all(|e| e.command.as_deref() == Some("effect.dialog"))); + assert_eq!(artistic[2].params, json!({"effect": "artistic.neonGlow"})); let at = |name: &str| entries.iter().position(|e| e.path == ["Effect", name]).unwrap(); assert!(at("Artistic") < at("Blur")); } diff --git a/docs/effects-parity.md b/docs/effects-parity.md index 2379ba760..235c25708 100644 --- a/docs/effects-parity.md +++ b/docs/effects-parity.md @@ -12,7 +12,7 @@ Part of [target-app-parity.md](target-app-parity.md); open work is in [gaps.md]( | Group | Illustrator | VectorCraft | % | Remaining (h) | |---|---:|---:|---:|---:| | Illustrator effects (vector and stylize) | 50 | 45 | 90% | 6–10 | -| Photoshop effects (raster, Effect Gallery) | 57 | 30 | 53% | 22–38 | +| Photoshop effects (raster, Effect Gallery) | 57 | 33 | 58% | 21–35 | | 3D and Materials | 4 (+3 Classic) | 4 (initial: Extrude & Bevel, Revolve, Inflate, Rotate) | ~25% | 25–45 | | Beyond Illustrator: Effect › Color Adjustments (6), effect plug-ins (WebAssembly) | — | 6 + plug-ins | — | — | @@ -38,7 +38,7 @@ its resolution, PDF export warns once per object with the requested and the lowe | Submenu | Items | Ours | Missing | |---|---|---:|---| -| Artistic | Colored Pencil, Cutout, Dry Brush, Film Grain, Fresco, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor | 1/15 (approximate Film Grain) | Colored Pencil, Cutout, Dry Brush, Fresco, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor | +| Artistic | Colored Pencil, Cutout, Dry Brush, Film Grain, Fresco, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor | 4/15 (approximate Cutout, Film Grain, Neon Glow, Poster Edges) | Colored Pencil, Dry Brush, Fresco, Paint Daubs, Palette Knife, Plastic Wrap, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor | | Blur | Gaussian Blur, Radial Blur, Smart Blur | 3/3 | — | | Brush Strokes | Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e | 8/8 | — | | Distort | Diffuse Glow, Glass, Ocean Ripple | 3/3 | — | @@ -52,7 +52,7 @@ its resolution, PDF export warns once per object with the requested and the lowe The pixel-filter pipeline (`vectorcraft_effects::pixel`) is in place: each new filter is one function, a catalogue row, a menu entry and translations. Measured throughput: 0.7–1.2 agent hours per filter -(see [calibration](target-app-parity.md#calibration)), so the 27 missing filters are ~18–33 h and the Effect +(see [calibration](target-app-parity.md#calibration)), so the 24 missing filters are ~17–29 h and the Effect Gallery dialog another 4–6 h. These parallelize well across agents. Film Grain (`artistic.filmGrain`, Effect › Artistic) adds stable monochrome noise to shadows and @@ -68,6 +68,21 @@ mid-grey to a quarter-strength floor at full Intensity before highlight headroom soft grain, black takes light speckles and white stays clean. The tonal curve and endpoint behaviour are interpretations, not verified proprietary behaviour. Saturated colour channels may clip. The parameter ranges and defaults are chosen for VectorCraft, not asserted as measured proprietary defaults. + +Cutout, Neon Glow and Poster Edges (`artistic.cutout`, `artistic.neonGlow`, `artistic.posterEdges`) +follow the same two public references, with original algorithms. Cutout softens the object's colours (Edge Simplicity, less with Edge +Fidelity), splits each colour channel into Number of Levels steps, and paints each resulting piece +in the average colour that falls into it, so hues survive. Poster Edges +posterizes each colour channel to Posterization + 2 levels and draws black lines on the edges of +any channel and of the object's outline, wider with Edge Thickness and darker, on fainter edges +too, with Edge Intensity. Neon Glow redraws the object in dimmed greys screened with a glow colour +from its light areas (positive Glow Size) or its dark ones (negative), spread by the size and as +strong as Glow Brightness; with no foreground and background colours in Illustrator's model, the +glow colour is the effect's own parameter. All three keep coverage, work in document units at any +Document Raster Effects resolution, and use the generic effect dialog with live preview. Ranges and +defaults are chosen for VectorCraft. Evidence: `crates/effects/src/pixel/artistic.rs`, the +`artistic_*`, `cutout_*`, `poster_edges_*` and `neon_glow_*` tests in `crates/effects/src/pixel/tests.rs`, +and `artistic_settings_survive_a_save_and_open` in `crates/engine/src/tests_rasterfilters.rs`. Near white, highlight headroom can limit grain equally at every Intensity; highlight lift still depends on Intensity when Highlight Area is nonzero. The two rotated, variance-normalised noise layers can leave a faint diagonal weave at high diff --git a/docs/gaps.md b/docs/gaps.md index 811e5ee99..16a879e57 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -22,7 +22,7 @@ stage ([ROADMAP.md](../ROADMAP.md)). | 5 | [G11 Correctness bugs in core tools](#g11-correctness-bugs-in-core-tools) | 8–15 | yes | [target-app-parity.md](target-app-parity.md) | | 6 | [G4 Advanced type](#g4-advanced-type) | 34–47 | core part | [type-parity.md](type-parity.md) | | 7 | [G12 Preferences and panels that don't act yet](#g12-preferences-and-panels-that-dont-act-yet) | 13–20 | yes | [ui-parity.md](ui-parity.md) | -| 8 | [G2 Photoshop-style raster effects and the Effect Gallery](#g2-photoshop-style-raster-effects-and-the-effect-gallery) | 22–38 | no | [effects-parity.md](effects-parity.md) | +| 8 | [G2 Photoshop-style raster effects and the Effect Gallery](#g2-photoshop-style-raster-effects-and-the-effect-gallery) | 21–35 | no | [effects-parity.md](effects-parity.md) | | 9 | [G13 Illustrator 2026 (30.x) additions](#g13-illustrator-2026-30x-additions) | 15–25 | no | [target-app-parity.md](target-app-parity.md) | | 10 | [G14 Pen hardware beyond Windows pressure](#g14-pen-hardware-beyond-windows-pressure) | 8–16 | macOS pressure | [hardware-parity.md](hardware-parity.md) | | 11 | [G5 Brushes, symbols and libraries](#g5-brushes-symbols-and-libraries) | 16–27 | no | [target-app-parity.md](target-app-parity.md) | @@ -209,13 +209,13 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall ## G2 Photoshop-style raster effects and the Effect Gallery -- **Missing:** 27 of 57 filters (the remaining 14 Artistic filters and 13 of Sketch's 14), the Effect Gallery dialog, Load +- **Missing:** 24 of 57 filters (the remaining 11 Artistic filters and 13 of Sketch's 14), the Effect Gallery dialog, Load Texture for Texturizer. Measured in [effects-parity.md](effects-parity.md). -- **Done:** the pixel-filter pipeline and 30 filters (Artistic: Film Grain, Blur, Brush Strokes, Distort, Pixelate, Sharpen, Sketch: - Halftone Pattern, Stylize, Texture, Video), Brush Strokes on 2026-10-11, Film Grain on 2026-10-12, Halftone Pattern on 2026-10-16, and Smart Blur's Edge Only and Overlay Edge +- **Done:** the pixel-filter pipeline and 33 filters (Artistic: Cutout, Film Grain, Neon Glow, Poster Edges, Blur, Brush Strokes, Distort, Pixelate, Sharpen, Sketch: + Halftone Pattern, Stylize, Texture, Video), Brush Strokes on 2026-10-11, Film Grain on 2026-10-12, Halftone Pattern on 2026-10-16, Cutout, Neon Glow and Poster Edges on 2026-10-16, and Smart Blur's Edge Only and Overlay Edge modes. -- **Estimate:** 22–38 h (measured 0.7–1.2 h per filter); parallelizes well. +- **Estimate:** 21–35 h (measured 0.7–1.2 h per filter); parallelizes well. ## G13 Illustrator 2026 (30.x) additions diff --git a/docs/roadmap.md b/docs/roadmap.md index 16c9c4b63..8f0ce4e2d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -33,7 +33,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V 2. **`.ai` files from real users** (G9): layer structure (#951), guides (#779). 10–20 h. 3. **Launch and stability** (G10): the Windows and Linux start-up reports. 20–35 h. 4. **Core-tool bugs** (G11): Shape Builder regions (#937). Asset Export ghost borders closed (#983 / #1037). 8–15 h. -5. **Raster effects** (G2) continue alongside: Artistic (Film Grain done; 14 left), Sketch (Halftone Pattern done; 13 left), then the Effect Gallery. 22–38 h. +5. **Raster effects** (G2) continue alongside: Artistic (Cutout, Film Grain, Neon Glow and Poster Edges done; 11 left), Sketch (Halftone Pattern done; 13 left), then the Effect Gallery. 21–35 h. ## Next @@ -59,7 +59,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V | M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (single-result Pathfinder names and SVG IDs retained (#1293); Divide open-cutting-path geometry, appearance and history verified (#1307; existing segment cap remains); Simplify preview/counts and rewritten-path component selection verified (#1272; auto/slider/advanced UI remains open); Compound appearance and member fill-rule edits verified (#1197); Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open) | 3–6 | | M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Fullwidth `!` / `?` keep an upright fallback in vertical type; Document-point character controls after object/group scaling (#1034; optional font-size preservation remains separate), Character/Paragraph Styles (explicit Auto-leading resets and local leading overrides), per-paragraph paragraph attributes, mixed paragraph alignment indication (headless regressions, full integration CI and visual review pass), Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import (full-name aliases and Missing Fonts searches verified, #1258; Windows font-service QA pending), variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel (Above Text floats it over the selected text, its ruler at the canvas zoom so stops line up with the columns, #729), inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; tab leaders, spell check, vertical type pending | 45–60 | | M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ✅ done (M8.1–M8.20 on 2026-10-06, fidelity pass): Liquify Tool Options with one shared brush, pen pressure, hold-to-apply, a safe scope and incremental strokes; tool options that last; Puppet Warp rotation, multi-select, Control bar and a rest-shape session that follows Undo; Width tool on compound paths; Envelope dialogs with preview, Reset with Warp/Mesh, full Envelope Options, type/images/symbols/appearance/gradients/patterns distorted on canvas and in every export, warps that keep their frame, Edit Contents, mesh handles and Control bar; Blend Options, anchor-targeted Blend tool, editable spine, keys picked on the canvas, faithful interpolation and knockout; Perspective Grid with Define Grid, presets, lock/snap/rulers, widgets, the 1–4 plane switch, movable planes, Perspective Selection scale/copy/perpendicular/Transform Again, and type and symbols truly projected with Edit Text. Left: Anti-Alias and Preserve Shape output, warp-envelope point editing, brush interpolation in blends, Free Transform/Scale/Reflect along a rotated box | 2–4 | -| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder, Color Adjustments and Document Raster Effects Settings; PDF export warns when the pixel cap lowers raster-effect resolution; Artistic › Film Grain and Sketch › Halftone Pattern done (30/57 Photoshop filters); Revolve with automatic smooth vector shading, default-on circular caps and continuous rims, Extrude & Bevel, Inflate and Rotate; SVG Filters, remaining raster effects and 3D materials, lights and shadows pending | 81–130 | +| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder, Color Adjustments and Document Raster Effects Settings; PDF export warns when the pixel cap lowers raster-effect resolution; Artistic › Cutout, Film Grain, Neon Glow and Poster Edges and Sketch › Halftone Pattern done (33/57 Photoshop filters); Revolve with automatic smooth vector shading, default-on circular caps and continuous rims, Extrude & Bevel, Inflate and Rotate; SVG Filters, remaining raster effects and 3D materials, lights and shadows pending | 81–130 | | M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 | | M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; multiple windows, presentation polish) | 🟡 | 15–25 | | M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (15 presets, the 14 documented ones each with an enhanced version; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending | 6–10 | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 03af095d2..17a16499a 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -12,7 +12,7 @@ the work list made from it. Deep areas have their own checklists: [UI and intera | Number | Value | Kind | |---|---|---| | 2026-10-15 | minor | Image Trace retains nonzero positioned boundaries when a fixed-end returning piece has fewer than two fitted cubics. Source-generated tests check survival, winding, shared-edge reversal and Noise controls; see `image-trace-quality.md`. Scores and hours unchanged. | -| **Feature breadth** (does each Illustrator menu item, tool, effect and format exist?) | **~88%** | measured in part: menu tree 353 of 375 items wired (94.1%), tools 89 of 92 (97%), vector effects 44 of 50 (88%), Photoshop-style effects 30 of 57 (53%), 3D 4 of 4 (initial: Extrude & Bevel, Revolve, Inflate, Rotate); blended by the area weights below | +| **Feature breadth** (does each Illustrator menu item, tool, effect and format exist?) | **~88%** | measured in part: menu tree 353 of 375 items wired (94.1%), tools 89 of 92 (97%), vector effects 44 of 50 (88%), Photoshop-style effects 33 of 57 (58%), 3D 4 of 4 (initial: Extrude & Bevel, Revolve, Inflate, Rotate); blended by the area weights below | | **Feature depth** (each area scored by behaviour, weighted) | **~75%** | estimated, area table below | | **Ready for real work, mainstream illustrator** | **~65%** (60–70%) | estimated, see [Ready for real work](#ready-for-real-work) | | **Ready for real work, full Illustrator** (decides the stage) | **~62%** (57–67%) | estimated: additive weighted sum over the dimensions, see [Ready for real work](#ready-for-real-work) | @@ -75,7 +75,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Strokes, brushes, width profiles | 5 | 88% | Shift-double-click adjoining adjustment is implemented; remaining: Art brush Overlap, Pattern brush auto corners, brush libraries, pen pressure on macOS, tilt/bearing/rotation | 8–15 | estimated; width-point creation and adjoining adjustment regression-tested; dialog layout reviewed; evidence below; finite-arithmetic follow-up regressions and full CI passed (evidence below) | | Appearance, transparency, graphic styles, masks | 5 | 97% | raster effect reach in preview bounds | 1–2 | estimated | | Live vector effects | 5 | 85% | Outline Object, Effect › Rasterize, Pathfinder Hard Mix/Soft Mix/Trap, SVG Filters, the Transform effect's missing options (#885) | 6–10 | measured count 44/50, depth estimated; Warp Options controls verified by focused checks and original-image review | -| Raster effects (Effect Gallery) | 4 | 49% | 27 Photoshop-style filters (14 Artistic, 13 Sketch), the Effect Gallery dialog, Load Texture | 22–38 | measured count 30/57; texture redraw distances optimized (#1257), pixels verified; depth estimated | +| Raster effects (Effect Gallery) | 4 | 49% | 24 Photoshop-style filters (11 Artistic, 13 Sketch), the Effect Gallery dialog, Load Texture | 21–35 | measured count 33/57; texture redraw distances optimized (#1257), pixels verified; depth estimated | | 3D and Materials (and Turntable) | 4 | ~25% | live Revolve with automatic smooth vector shading and default-on continuous circular caps, Extrude & Bevel (twist, taper, caps, five bevel shapes), Inflate and Rotate with Position presets (other types use flat shading; one light, painter visibility); materials, several lights, shadows, ray tracing, 3D Classic, Turntable views, 3D export | 30–55 | estimated (Extrude & Bevel, Inflate and Rotate added 2026-10-12) | | Type core | 9 | 80% | Auto-leading style resets are implemented; remaining: hyphenation and justification options, Optical Margin Alignment, Middle Eastern features beyond bidi layout, optional group font-size preservation; transformed character controls fixed (#1034), see [type-parity.md](type-parity.md) | 14–19 | estimated; full-name font matches regression-tested (#1258); mixed paragraph alignment indication implemented; headless regressions, full integration CI and visual review pass; localized legacy family-name fallback with loaded/catalog regressions (#2208) | | Type advanced (CJK, spelling, Touch Type, Retype) | 4 | 50% | CJK vertical composition (ruby, mojikumi), variable font axes, tab leaders, spell check, Touch Type, Retype | 20–28 | estimated; upright fallback for fullwidth `!` / `?` regression-tested; score and hours unchanged | @@ -291,6 +291,12 @@ its `film_grain_*` tests and the canvas/export cases in `crates/engine/src/tests This raises the measured Photoshop filter count to 29/57; the depth estimate is conservatively 49%. The full feature/readiness headline remains unchanged at its existing rounding. +Cutout, Neon Glow and Poster Edges (G2, 2026-10-16) follow as original Artistic filters with live +preview, undo, appearance editing and save/reopen, keeping coverage at any Document Raster Effects +resolution. Evidence: `crates/effects/src/pixel/artistic.rs`, its `cutout_*`, `neon_glow_*` and +`poster_edges_*` tests, and `artistic_settings_survive_a_save_and_open` in +`crates/engine/src/tests_rasterfilters.rs`. Measured Photoshop filter count 33/57; depth unchanged. + ## Detailed missing items per area (2026-10-05 table, still current unless the area table above says otherwise) > **Duplicated rows (2026-10-14):** this table lists "Selection, transform & align tools" three times and "Drawing tools"