diff --git a/ROADMAP.md b/ROADMAP.md index c57a7ab64..d50f4f57b 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 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 | diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 2094fba21..d5aea1d79 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -406,6 +406,21 @@ pub fn effect_catalog() -> Vec { "{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…", diff --git a/crates/effects/src/pixel/artistic.rs b/crates/effects/src/pixel/artistic.rs index b2484bd7c..f14671a12 100644 --- a/crates/effects/src/pixel/artistic.rs +++ b/crates/effects/src/pixel/artistic.rs @@ -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: //! and //! . @@ -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. diff --git a/crates/effects/src/pixel/brushstrokes.rs b/crates/effects/src/pixel/brushstrokes.rs index d496f294e..100d4e249 100644 --- a/crates/effects/src/pixel/brushstrokes.rs +++ b/crates/effects/src/pixel/brushstrokes.rs @@ -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; } diff --git a/crates/effects/src/pixel/mod.rs b/crates/effects/src/pixel/mod.rs index 6c465355f..d3734ad75 100644 --- a/crates/effects/src/pixel/mod.rs +++ b/crates/effects/src/pixel/mod.rs @@ -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 { 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) diff --git a/crates/effects/src/pixel/tests.rs b/crates/effects/src/pixel/tests.rs index 2f47a97a6..edce4b431 100644 --- a/crates/effects/src/pixel/tests.rs +++ b/crates/effects/src/pixel/tests.rs @@ -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 = 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, b: &Vec| 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)); diff --git a/crates/engine/src/tests_rasterfilters.rs b/crates/engine/src/tests_rasterfilters.rs index af903bdc5..ca709de50 100644 --- a/crates/engine/src/tests_rasterfilters.rs +++ b/crates/engine/src/tests_rasterfilters.rs @@ -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 diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index f5789301a..c502fd663 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -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}})), diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 1e10132e6..c7dbcf346 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4568,7 +4568,9 @@ lighting Light Светлина Paint Daubs Мазки с боя Palette Knife Нож за палитра Poster Edges… Плакатни ръбове… + Rough Pastels… Груби пастели… Poster Edges Плакатни ръбове + Rough Pastels Груби пастели Underpainting… Подбоя… Underpainting Подбоя Levels Нива diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index c1c3e6bf2..69b7b087d 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -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ě diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 5bdf1921e..f4fc9d500 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 51cbf7860..f2954f330 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 0545b135b..b530c8469 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2277,7 +2277,9 @@ lighting Light Φως Paint Daubs Πινελιές βαφής Palette Knife Μαχαίρι παλέτας Poster Edges… Άκρες αφίσας… + Rough Pastels… Χοντρά παστέλ… Poster Edges Άκρες αφίσας + Rough Pastels Χοντρά παστέλ Underpainting… Υποζωγραφία… Underpainting Υποζωγραφία Levels Επίπεδα diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 69614c9d5..877b5826e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 6a3f34163..365452e4c 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index b3d49bade..4c4fbe06b 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index e9e820d0e..63455d978 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 79f41ccf5..b3a3051bc 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 5de6d61df..f52676205 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 6618e1e15..b24e36167 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4736,7 +4736,9 @@ lighting Light ライト Paint Daubs ペイントダブ Palette Knife パレットナイフ Poster Edges… ポスターエッジ… + Rough Pastels… ラフパステル… Poster Edges ポスターエッジ + Rough Pastels ラフパステル Underpainting… アンダーペインティング… Underpainting アンダーペインティング Levels レベル数 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 7fa70cb04..c1e30279f 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -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 სამუშაო დაფების გადალაგება diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 199544432..02935f068 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4660,7 +4660,9 @@ lighting Light 광원 Paint Daubs 페인트 다브 Palette Knife 팔레트 나이프 Poster Edges… 포스터 가장자리… + Rough Pastels… 거친 파스텔… Poster Edges 포스터 가장자리 + Rough Pastels 거친 파스텔 Underpainting… 언더페인팅… Underpainting 언더페인팅 Levels 레벨 수 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index df85856cf..805597607 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 8618138db..e14037421 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index b89fa7608..aba95ccdf 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 1d1939fcd..71728573e 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4693,7 +4693,9 @@ lighting Light Свет Paint Daubs Мазки кисти Palette Knife Нож для палитры Poster Edges… Края плаката… + Rough Pastels… Грубые пастели… Poster Edges Края плаката + Rough Pastels Грубые пастели Underpainting… Подмалёвка… Underpainting Подмалёвка Levels Уровни diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 4bbef3924..fc43028ae 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index b950aca2a..9ab75b00c 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4677,7 +4677,9 @@ lighting Light Світло Paint Daubs Мазки фарби Palette Knife Ніж палітри Poster Edges… Плакатні краї… + Rough Pastels… Грубі пастелі… Poster Edges Плакатні краї + Rough Pastels Грубі пастелі Underpainting… Підмалювання… Underpainting Підмалювання Levels Рівні diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 54e8d3c65..b508807cf 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 851c5b727..a6f520b2b 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4961,7 +4961,9 @@ lighting Light 光源 Paint Daubs 绘画涂抹 Palette Knife 调色刀 Poster Edges… 海报边缘… + Rough Pastels… 粗糙蜡笔… Poster Edges 海报边缘 + Rough Pastels 粗糙蜡笔 Underpainting… 底纹效果… Underpainting 底纹效果 Levels 色阶数 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index fde67267e..549615b26 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -4767,7 +4767,9 @@ lighting Light 光源 Paint Daubs 繪畫塗抹 Palette Knife 調色刀 Poster Edges… 海報邊緣… + Rough Pastels… 粗糙粉彩… Poster Edges 海報邊緣 + Rough Pastels 粗糙粉彩 Underpainting… 底紋效果… Underpainting 底紋效果 Levels 色階數 diff --git a/crates/ui-egui/src/tests_labels.rs b/crates/ui-egui/src/tests_labels.rs index 67dfe48a3..eb9da8a14 100644 --- a/crates/ui-egui/src/tests_labels.rs +++ b/crates/ui-egui/src/tests_labels.rs @@ -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…", diff --git a/docs/roadmap.md b/docs/roadmap.md index 4186bd527..a7f838d66 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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 ``/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 | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 5038ccfb0..f5eba9185 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -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 |