Merge pull request #3044 from nrq6993-a11y/stream/vc-feat3-g2-artistic-rough-pastels

G2: Effect › Artistic › Rough Pastels raster filter
This commit is contained in:
BFlat
2026-10-22 00:38:27 +07:00
committed by GitHub
34 changed files with 203 additions and 13 deletions
+1 -1
View File
@@ -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 46/50, Photoshop effects 47/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 46/50, Photoshop effects 51/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 |
+15
View File
@@ -406,6 +406,21 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
"{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(
"artistic.roughPastels",
"Rough Pastels…",
ARTISTIC,
"{strokeLength: 0..40 (6; longer pastel strokes), strokeDetail: 1..20 (10; bristle texture along the strokes), texture: \"brick\"|\"burlap\"|\"canvas\"|\"sandstone\" (\"canvas\"), scaling: % 50..200 (100), relief: 0..50 (4), lightDirection: \"bottom\"|\"bottomLeft\"|\"left\"|\"topLeft\"|\"top\"|\"topRight\"|\"right\"|\"bottomRight\" (\"top\"), invert: bool (false)} repaints the object in soft diagonal pastel strokes over a textured surface, keeping its coverage. Slider ranges follow the documented controls; numeric defaults are chosen for VectorCraft and are not verified against proprietary defaults.",
json!({
"strokeLength": 6.0,
"strokeDetail": 10.0,
"texture": "canvas",
"scaling": 100.0,
"relief": 4.0,
"lightDirection": "top",
"invert": false
}),
),
r(
"artistic.smudgeStick",
"Smudge Stick…",
+64 -3
View File
@@ -1,5 +1,5 @@
//! Effect › Artistic (Cutout, Dry Brush, Film Grain, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster
//! Edges, Smudge Stick, Sponge, Underpainting and Watercolor). Original filters built from
//! Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting and Watercolor). Original filters built from
//! public behaviour documentation:
//! <https://helpx.adobe.com/illustrator/using/summary-effects.html> and
//! <https://helpx.adobe.com/photoshop-elements/using/artistic-filters.html>.
@@ -7,10 +7,10 @@
use std::f64::consts::TAU;
use super::brushstrokes::{Smear, mix, stroke_reach, unit, unpremultiply};
use super::brushstrokes::{LEFT, RIGHT, Smear, TONE_SOFTNESS, mix, streak, stroke_reach, unit, unpremultiply};
use super::edges::sobel;
use super::pixelate::{Around, Place, Soft, cell, point_in, rand, value_noise};
use super::texture::{self, Light, Texture, luma, premultiply, straight};
use super::texture::{self, Light, Texture, Tones, luma, premultiply, straight, texturizer};
use super::{PixelSpace, blur_plane, pick};
use vectorcraft_geom::{Point, Vec2};
@@ -18,6 +18,8 @@ const SALT_DRY: u64 = 264;
const MARGIN: f64 = 1.0;
const SALT_PLASTIC: u64 = 512;
const SALT_PASTEL: u64 = 200;
const SALT_FILM: u64 = 256;
const SALT_DAUB: u64 = 272;
@@ -526,6 +528,65 @@ pub(super) fn underpainting(
}
}
/// How far (document units) Rough Pastels reaches with strokes `len` long.
pub(super) fn rough_pastels_reach(len: f64) -> f64 {
super::brushstrokes::stroke_reach(len).max(super::brushstrokes::TONE_REACH)
}
/// Rough Pastels: soft diagonal pastel strokes `length` (0..40) long with bristle `detail` (1..20),
/// then the shared Texturizer surface (`texture`, `scaling`, `relief`, `light`, `invert`).
/// Coverage is kept.
#[allow(clippy::too_many_arguments)]
pub(super) fn rough_pastels(
px: &mut [[u8; 4]],
w: usize,
h: usize,
space: &PixelSpace,
length: f64,
detail: f64,
texture: &Texture,
scaling: f64,
relief: f64,
light: Light,
invert: bool,
) {
if !([length, detail, scaling, relief].iter().all(|v| v.is_finite()) && space.px.is_finite() && space.px > 0.0) {
return;
}
let length = length.clamp(0.0, 40.0);
let detail = detail.clamp(1.0, 20.0);
if length > 0.0 {
let Some(place) = Place::new(space, w, h) else { return };
let smear = Smear::new(px, w, h, &place, length.max(1.0));
let tones = Tones::new(px, w, h, TONE_SOFTNESS / place.px);
let row = (0.18 * smear.len).max(1.0);
let stroke_mix = (0.35 + 0.0225 * detail).clamp(0.35, 0.8) as f32;
let bristle = (0.015 + 0.0035 * detail) as f32;
let src = px.to_vec();
place.each(px, |x, y, p| {
if p[3] == 0 {
return;
}
let q = place.at(x, y);
let pm = mix(smear.stroke(SALT_PASTEL, q, RIGHT, row), smear.stroke(SALT_PASTEL + 1, q, LEFT, row), 0.5);
let orig = unit(src.get(y * w + x).copied().unwrap_or([0; 4]));
let pm = mix(orig, pm, stroke_mix);
let (mut c, a) = unpremultiply(pm);
if a <= 0.0 {
*p = [0; 4];
return;
}
let dir = if tones.get(x, y) > 0.52 { RIGHT } else { LEFT };
let n = bristle * streak(SALT_PASTEL + 2, q, dir, smear.len.max(1.0), row) as f32;
c = c.map(|v| (v + n).clamp(0.0, 1.0));
*p = premultiply(c, orig[3]);
});
}
if relief > 0.0 {
texturizer(px, w, h, space, texture, scaling, relief, light, invert);
}
}
/// 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.
+1 -1
View File
@@ -231,7 +231,7 @@ fn frame(q: Point, dir: Vec2) -> (f64, f64) {
/// How far along `dir` the stroke through `q` is shifted: strokes `len` long lie in rows `width`
/// apart across `dir`, each row starting at random, each stroke shifted by up to [`JITTER`] ×
/// `len`.
fn stroke_shift(salt: u64, q: Point, dir: Vec2, len: f64, width: f64) -> f64 {
pub(super) fn stroke_shift(salt: u64, q: Point, dir: Vec2, len: f64, width: f64) -> f64 {
if !(len > 0.0 && width > 0.0) {
return 0.0;
}
+24 -5
View File
@@ -1,5 +1,5 @@
//! Photoshop-style raster effects (Effect › Artistic › Cutout, Dry Brush, Film Grain, Neon Glow, Paint Daubs, Plastic Wrap and
//! Poster Edges, Smudge Stick, Sponge and Watercolor, Blur › Radial Blur and Smart Blur, Brush Strokes › Accented Edges, Angled Strokes,
//! Poster Edges, Rough Pastels, Smudge Stick, Sponge and Watercolor, 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 › Bas Relief, Chalk & Charcoal, Charcoal, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster,
@@ -44,7 +44,7 @@ pub use sketch::{HALFTONE_TYPES, HalftonePattern};
pub use texture::{GRAIN_TYPES, Grain, LIGHT_DIRECTIONS, Light, TEXTURES, TEXTURES_LOADED, Texture};
/// The Photoshop-style effect ids (all raster effects, see [`crate::is_raster`]).
pub const PIXEL_EFFECTS: [&str; 51] = [
pub const PIXEL_EFFECTS: [&str; 52] = [
"artistic.cutout",
"artistic.dryBrush",
"artistic.filmGrain",
@@ -53,6 +53,7 @@ pub const PIXEL_EFFECTS: [&str; 51] = [
"artistic.paletteKnife",
"artistic.plasticWrap",
"artistic.posterEdges",
"artistic.roughPastels",
"artistic.smudgeStick",
"artistic.sponge",
"artistic.underpainting",
@@ -143,6 +144,10 @@ pub enum PixelFx {
/// `texture` surface (`scaling`, `relief`, `light`, `invert`) blended in as strongly as
/// `texture_coverage` (0..40) says.
Underpainting { brush_size: f64, texture_coverage: f64, texture: Texture, scaling: f64, relief: f64, light: Light, invert: bool },
/// Artistic › Rough Pastels: diagonal pastel strokes `length` (0..40) long, bristle detail
/// (1..20), then a [`Texture`] surface scaled by `scaling` (0.5..2), in relief as strong as
/// `relief` (0..50) and lit from `light`; `invert` turns the surface's heights over.
RoughPastels { length: f64, detail: f64, texture: Texture, scaling: f64, relief: f64, light: Light, invert: bool },
/// 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 },
@@ -346,6 +351,15 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
light: Light::parse(text(p, "lightDirection", "top")),
invert: flag(p, "invert", false),
},
"artistic.roughPastels" => PixelFx::RoughPastels {
length: num(p, "strokeLength", 6.0).clamp(0.0, 40.0),
detail: num(p, "strokeDetail", 10.0).clamp(1.0, 20.0),
texture: Texture::parse(text(p, "texture", "canvas")),
scaling: num(p, "scaling", 100.0).clamp(50.0, 200.0) / 100.0,
relief: num(p, "relief", 4.0).clamp(0.0, 50.0),
light: Light::parse(text(p, "lightDirection", "top")),
invert: flag(p, "invert", false),
},
"artistic.smudgeStick" => PixelFx::SmudgeStick {
stroke: num(p, "strokeLength", 2.0).clamp(0.0, 10.0),
highlight: num(p, "highlightArea", 12.0).clamp(0.0, 20.0),
@@ -558,9 +572,10 @@ impl PixelFx {
PixelFx::AccentedEdges { .. } => 0.0,
// Strokes reach out of the object as far as they reach into it.
PixelFx::PaletteKnife { stroke_size, softness, .. } => artistic::palette_knife_reach(stroke_size, softness),
PixelFx::AngledStrokes { length, .. } | PixelFx::GraphicPen { length, .. } | PixelFx::InkOutlines { length, .. } => {
brushstrokes::stroke_reach(length)
}
PixelFx::AngledStrokes { length, .. }
| PixelFx::GraphicPen { length, .. }
| PixelFx::InkOutlines { length, .. }
| PixelFx::RoughPastels { length, .. } => brushstrokes::stroke_reach(length),
PixelFx::Crosshatch { length, .. } => brushstrokes::stroke_reach(length) + brushstrokes::rough(length),
PixelFx::DarkStrokes { .. } => brushstrokes::stroke_reach(brushstrokes::LIGHT_LENGTH),
PixelFx::Spatter { radius, .. } => brushstrokes::spatter_shift(radius),
@@ -622,6 +637,7 @@ impl PixelFx {
PixelFx::AngledStrokes { length, .. } | PixelFx::GraphicPen { length, .. } => {
Some(brushstrokes::stroke_reach(length).max(brushstrokes::TONE_REACH))
}
PixelFx::RoughPastels { length, .. } => Some(artistic::rough_pastels_reach(length)),
PixelFx::Crosshatch { length, .. } => {
Some((brushstrokes::stroke_reach(length) + brushstrokes::rough(length)).max(brushstrokes::TONE_REACH))
}
@@ -701,6 +717,9 @@ impl PixelFx {
PixelFx::Underpainting { brush_size, texture_coverage, ref texture, scaling, relief, light, invert } => {
artistic::underpainting(px, w, h, space, brush_size, texture_coverage, texture, scaling, relief, light, invert)
}
PixelFx::RoughPastels { length, detail, ref texture, scaling, relief, light, invert } => {
artistic::rough_pastels(px, w, h, space, length, detail, texture, scaling, relief, light, invert)
}
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)
+30
View File
@@ -1222,6 +1222,8 @@ fn texture_params_take_defaults_and_stay_in_range() {
("texture.grain", "grainType", GRAIN_TYPES.as_slice()),
("texture.texturizer", "texture", TEXTURES.as_slice()),
("texture.texturizer", "lightDirection", LIGHT_DIRECTIONS.as_slice()),
("artistic.roughPastels", "texture", TEXTURES.as_slice()),
("artistic.roughPastels", "lightDirection", LIGHT_DIRECTIONS.as_slice()),
] {
let doc = crate::effect_info(id).unwrap().params;
let parsed: Vec<PixelFx> = table.iter().map(|(_, v)| fx(id, json!({ key: v }))).collect();
@@ -2466,6 +2468,17 @@ fn artistic_params_take_defaults_and_stay_in_range() {
invert: true,
}
);
assert_eq!(
fx("artistic.roughPastels", json!({})),
PixelFx::RoughPastels { length: 6.0, detail: 10.0, texture: Texture::Canvas, scaling: 1.0, relief: 4.0, light: Light::Top, invert: false }
);
assert_eq!(
fx(
"artistic.roughPastels",
json!({"strokeLength": 0, "strokeDetail": 99, "texture": -3, "scaling": 10, "relief": 60, "lightDirection": "up", "invert": true})
),
PixelFx::RoughPastels { length: 0.0, detail: 20.0, texture: Texture::Canvas, scaling: 0.5, relief: 50.0, light: Light::Top, invert: true }
);
assert_eq!(fx("artistic.plasticWrap", json!({})), PixelFx::PlasticWrap { highlight: 20.0, detail: 12.0, smoothness: 15.0 });
assert_eq!(
fx("artistic.plasticWrap", json!({"highlightStrength": -1, "detail": 99, "smoothness": 0})),
@@ -2493,6 +2506,7 @@ fn artistic_params_take_defaults_and_stay_in_range() {
assert!(parsed.iter().skip(i + 1).all(|p| *p != parsed[i]), "{id} {v}");
}
}
assert!(fx("artistic.roughPastels", json!({})).outset(Rect::new(0.0, 0.0, 40.0, 30.0)) > 0.0);
}
#[test]
@@ -2520,6 +2534,7 @@ fn artistic_filters_leave_invalid_coordinate_spaces_alone() {
light: Light::Top,
invert: false,
},
PixelFx::RoughPastels { length: 6.0, detail: 10.0, texture: Texture::Canvas, scaling: 1.0, relief: 4.0, light: Light::Top, invert: false },
];
for f in &filters {
assert_ne!(run(f, &space(w, h)), src, "{f:?} changes the art in a valid space");
@@ -2622,6 +2637,20 @@ fn cutout_paints_a_few_flat_colours_and_keeps_coverage() {
}
}
#[test]
fn rough_pastels_keeps_coverage_and_longer_strokes_smear_more() {
let (w, h) = (48, 48);
let src = ramp(w, h);
let mut short = src.clone();
let mut long = src.clone();
fx("artistic.roughPastels", json!({"strokeLength": 8, "strokeDetail": 18, "relief": 12})).apply(&mut short, w, h, &space(w, h));
fx("artistic.roughPastels", json!({"strokeLength": 32, "strokeDetail": 18, "relief": 12})).apply(&mut long, w, h, &space(w, h));
assert!(short.as_chunks::<4>().0.iter().zip(src.as_chunks::<4>().0).all(|(o, s)| o[3] == s[3]));
let changed = |a: &Vec<u8>, b: &Vec<u8>| a.as_chunks::<4>().0.iter().zip(b.as_chunks::<4>().0).filter(|(o, s)| o != s).count();
assert!(changed(&short, &src) > w * h / 8, "short strokes repaint many pixels");
assert!(changed(&long, &short) > w * h / 16, "longer strokes smear differently");
}
#[test]
fn cutout_simplicity_smooths_the_pieces_edges() {
// A noisy grey: simpler edges join the speckles into fewer changes between neighbours.
@@ -2747,6 +2776,7 @@ fn artistic_filters_look_the_same_at_any_resolution() {
("artistic.neonGlow", json!({"glowSize": 12, "glowBrightness": 30})),
("artistic.posterEdges", json!({"edgeThickness": 6, "edgeIntensity": 6})),
("artistic.underpainting", json!({"brushSize": 12, "textureCoverage": 20, "relief": 10})),
("artistic.roughPastels", json!({"strokeLength": 12, "strokeDetail": 15, "texture": "burlap", "relief": 10})),
] {
let (mut a, mut b) = (coarse.clone(), fine.clone());
fx(id, p.clone()).apply(&mut a, w, h, &space(w, h));
+15 -1
View File
@@ -31,7 +31,7 @@ const BRUSH_STROKES: [(&str, &str); 8] = [
];
/// Effect › Artistic, at their defaults.
const ARTISTIC: [(&str, &str); 12] = [
const ARTISTIC: [(&str, &str); 13] = [
("artistic.cutout", "{}"),
("artistic.dryBrush", "{}"),
("artistic.filmGrain", "{}"),
@@ -42,6 +42,7 @@ const ARTISTIC: [(&str, &str); 12] = [
("artistic.plasticWrap", "{}"),
("artistic.posterEdges", "{}"),
("artistic.underpainting", "{}"),
("artistic.roughPastels", "{}"),
("artistic.smudgeStick", "{}"),
("artistic.sponge", "{}"),
("artistic.watercolor", "{}"),
@@ -227,6 +228,19 @@ fn artistic_settings_survive_a_save_and_open() {
json!({"highlightStrength": 18.0, "detail": 10.0, "smoothness": 8.0}),
[json!({"highlightStrength": 5.0}), json!({"highlightStrength": 0.0})],
),
(
"artistic.roughPastels",
json!({
"strokeLength": 8.0,
"strokeDetail": 12.0,
"texture": "sandstone",
"scaling": 100.0,
"relief": 8.0,
"lightDirection": "topLeft",
"invert": false
}),
[json!({"strokeLength": 20.0}), json!({"relief": 0.0})],
),
] {
let (mut s, g) = striped();
// Black and a grey beside the reds: Cutout puts the grey with black at 2 levels and apart
+4
View File
@@ -373,6 +373,10 @@ fn structured_junk() {
("effect.apply", json!({"effect": "artistic.dryBrush", "params": {"brushSize": -1e308, "brushDetail": "NaN", "texture": 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": "artistic.roughPastels", "params": {"strokeLength": 1e308, "strokeDetail": "NaN", "texture": -1e308, "scaling": "bad", "lightDirection": "nope"}}),
),
("effect.apply", json!({"effect": "sketch.chrome", "params": {"detail": -1e308, "smoothness": "NaN"}})),
("effect.apply", json!({"effect": "sketch.graphicPen", "params": {"strokeLength": -1e308, "lightDarkBalance": "NaN", "strokeDirection": 3}})),
("effect.apply", json!({"effect": "sketch.halftonePattern", "params": {"size": -1e308, "contrast": "NaN", "patternType": 3}})),
+2
View File
@@ -4568,7 +4568,9 @@ lighting Light Светлина
Paint Daubs Мазки с боя
Palette Knife Нож за палитра
Poster Edges… Плакатни ръбове…
Rough Pastels… Груби пастели…
Poster Edges Плакатни ръбове
Rough Pastels Груби пастели
Underpainting… Подбоя…
Underpainting Подбоя
Levels Нива
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5067,7 +5067,9 @@ imageTrace Outline Obrys
Paint Daubs Malířské tahy
Palette Knife Špachtle
Poster Edges… Plakátové okraje…
Rough Pastels… Hrubé pastely…
Poster Edges Plakátové okraje
Rough Pastels Hrubé pastely
Underpainting… Podmalba…
Underpainting Podmalba
Levels Úrovně
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5187,6 +5187,7 @@ swatch Bright Violet Kraftig violet
Neon Glow… Neonglød…
Palette Knife… Paletkniv…
Poster Edges… Plakatkanter…
Rough Pastels… Grove pasteller…
Cutout Udklip
Dry Brush Tør pensel
Neon Glow Neonglød
@@ -5194,6 +5195,7 @@ swatch Bright Violet Kraftig violet
Paint Daubs Malerklatter
Palette Knife Paletkniv
Poster Edges Plakatkanter
Rough Pastels Grove pasteller
Underpainting… Undermaling…
Underpainting Undermaling
Apply Data Set Anvend datasæt
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4654,7 +4654,9 @@ lighting Light Licht
Paint Daubs Tupfer
Palette Knife Palettenmesser
Poster Edges… Posterkanten…
Rough Pastels… Pastellkreide…
Poster Edges Posterkanten
Rough Pastels Pastellkreide
Underpainting… Untermalung…
Underpainting Untermalung
Levels Stufen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2277,7 +2277,9 @@ lighting Light Φως
Paint Daubs Πινελιές βαφής
Palette Knife Μαχαίρι παλέτας
Poster Edges… Άκρες αφίσας…
Rough Pastels… Χοντρά παστέλ…
Poster Edges Άκρες αφίσας
Rough Pastels Χοντρά παστέλ
Underpainting… Υποζωγραφία…
Underpainting Υποζωγραφία
Levels Επίπεδα
Can't render this file because it contains an unexpected character in line 2038 and column 19.
+2
View File
@@ -4655,7 +4655,9 @@ lighting Light Luz
Paint Daubs Pinceladas
Palette Knife Espátula
Poster Edges… Bordes de póster…
Rough Pastels… Pastel rudo…
Poster Edges Bordes de póster
Rough Pastels Pastel rudo
Underpainting… Subpintura…
Underpainting Subpintura
Levels Niveles
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4962,7 +4962,9 @@ imageTrace Outline Ääriviiva
Paint Daubs Maalitahrat
Palette Knife Palettiveitsi
Poster Edges… Julisteen reunat…
Rough Pastels… Karkeat pastellit…
Poster Edges Julisteen reunat
Rough Pastels Karkeat pastellit
Underpainting… Alapinta…
Underpainting Alapinta
Levels Tasot
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4665,7 +4665,9 @@ lighting Light Lumière
Paint Daubs Empâtements
Palette Knife Couteau à palette
Poster Edges… Bords d’affiche…
Rough Pastels… Pastels secs…
Poster Edges Bords d’affiche
Rough Pastels Pastels secs
Underpainting… Sous-peinture…
Underpainting Sous-peinture
Levels Niveaux
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1598,7 +1598,9 @@ file-filter Templates Sablonok
Paint Daubs Festékpaca
Palette Knife Palettakés
Poster Edges… Poszterszélek…
Rough Pastels… Durva pasztell…
Poster Edges Poszterszélek
Rough Pastels Durva pasztell
Underpainting… Alapozás…
Underpainting Alapozás
Levels Szintek
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5187,6 +5187,7 @@ swatch Bright Violet Violet cerah
Neon Glow… Pendar neon…
Palette Knife… Pisau palet…
Poster Edges… Tepi poster…
Rough Pastels… Pastel kasar…
Cutout Potongan
Dry Brush Kuas kering
Neon Glow Pendar neon
@@ -5194,6 +5195,7 @@ swatch Bright Violet Violet cerah
Paint Daubs Coretan cat
Palette Knife Pisau palet
Poster Edges Tepi poster
Rough Pastels Pastel kasar
Underpainting… Underpainting…
Underpainting Underpainting
Apply Data Set Terapkan set data
Can't render this file because it contains an unexpected character in line 3808 and column 19.
+2
View File
@@ -4697,7 +4697,9 @@ lighting Light Luce
Paint Daubs Pennellate
Palette Knife Spatola
Poster Edges… Bordi poster…
Rough Pastels… Pastelli grezzi…
Poster Edges Bordi poster
Rough Pastels Pastelli grezzi
Underpainting… Sottopittura…
Underpainting Sottopittura
Levels Livelli
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4736,7 +4736,9 @@ lighting Light ライト
Paint Daubs ペイントダブ
Palette Knife パレットナイフ
Poster Edges… ポスターエッジ…
Rough Pastels… ラフパステル…
Poster Edges ポスターエッジ
Rough Pastels ラフパステル
Underpainting… アンダーペインティング…
Underpainting アンダーペインティング
Levels レベル数
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5096,9 +5096,11 @@ imageTrace Outline კონტური
Persistent მუდმივი
Place from Library ბიბლიოთეკიდან განთავსება
Poster Edges პოსტერის კიდეები
Rough Pastels მკვრივი პასტელი
Underpainting… ქვეფენის მხატვრობა…
Underpainting ქვეფენის მხატვრობა
Poster Edges… პოსტერის კიდეები…
Rough Pastels… მკვრივი პასტელი…
Posterization პოსტერიზაცია
Previous Artboard წინა სამუშაო დაფა
Rearrange Artboards სამუშაო დაფების გადალაგება
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4660,7 +4660,9 @@ lighting Light 광원
Paint Daubs 페인트 다브
Palette Knife 팔레트 나이프
Poster Edges… 포스터 가장자리…
Rough Pastels… 거친 파스텔…
Poster Edges 포스터 가장자리
Rough Pastels 거친 파스텔
Underpainting… 언더페인팅…
Underpainting 언더페인팅
Levels 레벨 수
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5198,6 +5198,7 @@ swatch Bright Violet Felviolet
Neon Glow… Neongloed…
Palette Knife… Paletmes…
Poster Edges… Posterranden…
Rough Pastels… Ruwe pastels…
Cutout Knipsel
Dry Brush Droog penseel
Neon Glow Neongloed
@@ -5205,6 +5206,7 @@ swatch Bright Violet Felviolet
Paint Daubs Verfstipjes
Palette Knife Paletmes
Poster Edges Posterranden
Rough Pastels Ruwe pastels
Underpainting… Onderlaag…
Underpainting Onderlaag
Apply Data Set Gegevensset toepassen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4900,7 +4900,9 @@ imageTrace Outline Kontur
Paint Daubs Plamy farby
Palette Knife Szpachel
Poster Edges… Krawędzie plakatu…
Rough Pastels… Suche pastele…
Poster Edges Krawędzie plakatu
Rough Pastels Suche pastele
Underpainting… Podmalówka…
Underpainting Podmalówka
Levels Poziomy
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4050,7 +4050,9 @@ axis Both Ambos
Paint Daubs Manchas de tinta
Palette Knife Espátula
Poster Edges… Bordas de pôster…
Rough Pastels… Pastéis ásperos…
Poster Edges Bordas de pôster
Rough Pastels Pastéis ásperos
Underpainting… Subpintura…
Underpainting Subpintura
Levels Níveis
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -4693,7 +4693,9 @@ lighting Light Свет
Paint Daubs Мазки кисти
Palette Knife Нож для палитры
Poster Edges… Края плаката…
Rough Pastels… Грубые пастели…
Poster Edges Края плаката
Rough Pastels Грубые пастели
Underpainting… Подмалёвка…
Underpainting Подмалёвка
Levels Уровни
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4574,7 +4574,9 @@ lighting Light Ljus
Paint Daubs Färgklickar
Palette Knife Palettkniv
Poster Edges… Affischkanter…
Rough Pastels… Grova pasteller…
Poster Edges Affischkanter
Rough Pastels Grova pasteller
Underpainting… Undermålning…
Underpainting Undermålning
Levels Nivåer
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -4677,7 +4677,9 @@ lighting Light Світло
Paint Daubs Мазки фарби
Palette Knife Ніж палітри
Poster Edges… Плакатні краї…
Rough Pastels… Грубі пастелі…
Poster Edges Плакатні краї
Rough Pastels Грубі пастелі
Underpainting… Підмалювання…
Underpainting Підмалювання
Levels Рівні
Can't render this file because it contains an unexpected character in line 733 and column 40.
+2
View File
@@ -1279,7 +1279,9 @@
Paint Daubs Vệt sơn
Palette Knife Dao palette
Poster Edges… Viền áp phích…
Rough Pastels… Pastel thô…
Poster Edges Viền áp phích
Rough Pastels Pastel thô
Underpainting… Lớp sơn nền…
Underpainting Lớp sơn nền
Levels Số mức
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1279
1280
1281
1282
1283
1284
1285
1286
1287
+2
View File
@@ -4961,7 +4961,9 @@ lighting Light 光源
Paint Daubs 绘画涂抹
Palette Knife 调色刀
Poster Edges… 海报边缘…
Rough Pastels… 粗糙蜡笔…
Poster Edges 海报边缘
Rough Pastels 粗糙蜡笔
Underpainting… 底纹效果…
Underpainting 底纹效果
Levels 色阶数
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4767,7 +4767,9 @@ lighting Light 光源
Paint Daubs 繪畫塗抹
Palette Knife 調色刀
Poster Edges… 海報邊緣…
Rough Pastels… 粗糙粉彩…
Poster Edges 海報邊緣
Rough Pastels 粗糙粉彩
Underpainting… 底紋效果…
Underpainting 底紋效果
Levels 色階數
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -97,6 +97,7 @@ fn effect_menu_lists_the_artistic_filters_before_blur() {
"Palette Knife…",
"Plastic Wrap…",
"Poster Edges…",
"Rough Pastels…",
"Smudge Stick…",
"Sponge…",
"Underpainting…",
+1 -1
View File
@@ -60,7 +60,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; Anchor Point spine conversion/handle editing and selection priority (#2544), with #2425 Shift reshape preserved; 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 › Cutout, Film Grain, Neon Glow, Poster Edges, Smudge Stick, Sponge and Watercolor and Sketch › Halftone Pattern, Bas Relief, Charcoal, Photocopy and Stamp done (47/57 Photoshop filters); Revolve with automatic smooth vector shading, default-on circular caps and continuous rims, Extrude & Bevel, Inflate and Rotate with one to ten colored directional lights and four original lighting presets; SVG Filters, remaining raster effects and 3D materials 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, Dry Brush, Film Grain, Neon Glow, Paint Daubs, Palette Knife, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting and Watercolor and Sketch › Halftone Pattern, Bas Relief, Charcoal, Photocopy and Stamp done (51/57 Photoshop filters); Revolve with automatic smooth vector shading, default-on circular caps and continuous rims, Extrude & Bevel, Inflate and Rotate with one to ten colored directional lights and four original lighting presets; SVG Filters, remaining raster effects and 3D materials and shadows pending | 81–130 |
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); 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) (cached canvas reprojection preserves scale during resize, rotation and DPI changes; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) (cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; regression-tested; estimates unchanged) | 🟡 | 15–25 |
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (junction-returning pieces retain their positioned boundaries; a simpler contour walk with identical output; an edge's sub-pixel shift is the same from either side; notches read only their own piece; collapsed loops keep few segments; large images trace with a warning; Auto Color keeps rare distinct colours; quick presets trace at once and the Image Trace button opens the adjustment controls; Properties reopens them; tiny fitted contours retain islands and holes; Create Strokes: exact crop EDT, Guo–Hall thinning and maximal-disc graph simplification; 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`; closed outlines retain their interior after straightening; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients; live trace result bounds and selection highlights exclude the retained bitmap except in source-image views), 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 |
+1 -1
View File
@@ -79,7 +79,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de
| 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 (colored Inflate uses smooth membrane lighting; white-light Inflate retains its existing shading; Extrude & Bevel and Rotate retain planar shading; one to ten colored lights, shared neutral ambient, original lighting presets, painter visibility); materials, 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); Fractional Widths per type object regression-tested; scores and hours unchanged |
| Type advanced (CJK, spelling, Touch Type, Retype) | 4 | 50% | CJK vertical composition (mojikumi), variable font axes, tab leaders, spell check, Touch Type, Retype | 20–28 | estimated; No Break keeps marked characters on one line; upright fallback for fullwidth `!` / `?` regression-tested; score and hours unchanged |
| Raster effects (Effect Gallery) | 4 | 49% | 10 Photoshop-style filters (7 Artistic, 2 Sketch, 1 more), the Effect Gallery dialog, Load Texture | 8–14 | measured count 47/57; texture redraw distances optimized (#1257), pixels verified; depth estimated |
| Raster effects (Effect Gallery) | 4 | 49% | 6 Photoshop-style filters (Artistic › Colored Pencil and Fresco among them), the Effect Gallery dialog, Load Texture | 8–14 | measured count 51/57 (Artistic › Paint Daubs, Palette Knife, Rough Pastels and Underpainting added); texture redraw distances optimized (#1257), pixels verified; depth estimated |
| Symbols, blends, envelopes, Repeat, perspective | 5 | 87% | 30.7's Blend panel with ease in/out, dynamic symbols, symbol libraries, Start Global Edit, envelope Anti-Alias/Preserve Shape; Symbol Options › Registration, Export Type (Graphic / Movie Clip) and 9-slice guide settings (stored, not drawn) are done | 5–9 | estimated; interactive selected mirror axis implemented; bounding-box handles take priority over the axis line (focused regression and native checks passed; full CI blocked by an unrelated text test); #2544 spine Anchor Point conversion and picking regression-tested; scores and hours unchanged |
| Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`; simpler contour walk, identical output; an edge's sub-pixel shift is the same from either side; notches read only their own piece; collapsed loops keep few segments, so Paths is monotone on the code-drawn sweep) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them; Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views (scores/hours unchanged) |
| Symbols, blends, envelopes, Repeat, perspective | 5 | 87% | 30.7's Blend panel with ease in/out, dynamic symbols, symbol libraries, Start Global Edit, envelope Anti-Alias/Preserve Shape; Symbol Options › Registration and Export Type (Graphic / Movie Clip) are done | 5–9 | estimated |