G2: Effect › Sketch › Note Paper raster filter (#2954)

This commit is contained in:
nrq6993-a11y
2026-10-20 08:28:51 +07:00
committed by bflatastic
parent 64699f5212
commit 301783c480
7 changed files with 148 additions and 14 deletions
+9 -1
View File
@@ -155,6 +155,7 @@ fn lengths_of(id: &str) -> Lengths {
"texture.stainedGlass" => always(&["cellSize"]),
"sketch.graphicPen" => always(&["strokeLength"]),
"sketch.halftonePattern" => always(&["size"]),
"sketch.notePaper" => always(&["relief"]),
RASTERIZE => always(&["addAround"]),
_ => Lengths::default(),
}
@@ -535,9 +536,16 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
"sketch.halftonePattern",
"Halftone Pattern…",
SKETCH,
"{size: pt 1..12 (1; how far apart the screen's dots, rings or lines are), contrast: 0..50 (5; 50 draws pure black and white), patternType: \"circle\"|\"dot\"|\"line\" (\"dot\"; rings round the object's centre, dots on a square grid, horizontal lines)} redraws the object in black ink on white paper through a halftone screen whose ink covers as much as the colour is dark, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.",
"{size: pt 1..12 (1; how far apart the screen's dots, rings or lines are), contrast: 0..50 (5; 50 draws pure black and white), patternType: \"circle\"|\"dot\"|\"line\" (\"dot\"; rings round the object's centre, dots on a square grid, horizontal lines)} redraws the object in black ink on white paper through a halftone screen whose ink covers as much as the colour is dark, keeping its coverage. Original visual approximation from the public Sketch effects reference; slider ranges follow the documented control names, numeric defaults are chosen for VectorCraft and are not verified against proprietary defaults (the public help pages give no numbers).",
json!({"size": 1.0, "contrast": 5.0, "patternType": "dot"}),
),
r(
"sketch.notePaper",
"Note Paper…",
SKETCH,
"{imageBalance: 0..50 (25; lower tones darken the emboss, higher lighten it), graininess: 0..10 (0; fibrous paper texture), relief: 0..50 (4; emboss strength in points)} embosses the object's tones on warm paper, keeping its coverage. Original visual approximation from the public Sketch effects reference; slider ranges follow the documented control names, numeric defaults are chosen for VectorCraft and are not verified against proprietary defaults (the public help pages give no numbers).",
json!({"imageBalance": 25.0, "graininess": 0.0, "relief": 4.0}),
),
r(
"sketch.photocopy",
"Photocopy…",
+17 -3
View File
@@ -2,7 +2,7 @@
//! Poster Edges, 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 › Chalk & Charcoal, Graphic Pen, Halftone Pattern, Plaster and
//! Sharpen › Unsharp Mask, Sketch › Chalk & Charcoal, Graphic Pen, Halftone Pattern, Note Paper, Plaster and
//! Reticulation, Stylize ›
//! Glowing Edges, Texture
//! › Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass and Texturizer, and Video ›
@@ -43,7 +43,7 @@ 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; 45] = [
pub const PIXEL_EFFECTS: [&str; 46] = [
"artistic.cutout",
"artistic.filmGrain",
"artistic.neonGlow",
@@ -76,6 +76,7 @@ pub const PIXEL_EFFECTS: [&str; 45] = [
"sketch.chrome",
"sketch.graphicPen",
"sketch.halftonePattern",
"sketch.notePaper",
"sketch.photocopy",
"sketch.plaster",
"sketch.reticulation",
@@ -180,6 +181,8 @@ pub enum PixelFx {
/// Sketch › Halftone Pattern: black ink on white paper through a `kind` screen of cells `size`
/// (1..12) apart, its edges as hard as `contrast` (0..50) says.
HalftonePattern { size: f64, contrast: f64, kind: HalftonePattern },
/// Sketch › Note Paper: embossed tones on fibrous paper (`balance`, `graininess`, `relief`).
NotePaper { balance: f64, graininess: f64, relief: f64 },
/// Sketch › Bas Relief: low relief lit from `light`, keeping the finer features as `detail`
/// (1..15) says and rounded by `smoothness` (1..15).
BasRelief { detail: f64, smoothness: f64, light: Light },
@@ -404,6 +407,11 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
contrast: num(p, "contrast", 5.0).clamp(0.0, 50.0),
kind: HalftonePattern::parse(text(p, "patternType", "dot")),
},
"sketch.notePaper" => PixelFx::NotePaper {
balance: num(p, "imageBalance", 25.0).clamp(0.0, 50.0),
graininess: num(p, "graininess", 0.0).clamp(0.0, 10.0),
relief: num(p, "relief", 4.0).clamp(0.0, 50.0),
},
"sketch.basRelief" => PixelFx::BasRelief {
detail: num(p, "detail", 13.0).clamp(1.0, 15.0),
smoothness: num(p, "smoothness", 3.0).clamp(1.0, 15.0),
@@ -512,6 +520,7 @@ impl PixelFx {
| PixelFx::ChalkCharcoal { .. }
| PixelFx::Chrome { .. }
| PixelFx::HalftonePattern { .. }
| PixelFx::NotePaper { .. }
| PixelFx::Plaster { .. }
| PixelFx::Reticulation { .. } => 0.0,
PixelFx::BasRelief { .. } | PixelFx::Charcoal { .. } | PixelFx::Photocopy { .. } | PixelFx::Stamp { .. } => 0.0,
@@ -575,7 +584,11 @@ impl PixelFx {
PixelFx::Crystallize { cell } => Some(1.5 * cell),
PixelFx::ChalkCharcoal { .. } => Some(sketch::CHALK_CHARCOAL_REACH),
PixelFx::Chrome { smoothness, .. } => Some(sketch::chrome_reach(smoothness)),
PixelFx::FilmGrain { .. } | PixelFx::Mezzotint { .. } | PixelFx::HalftonePattern { .. } | PixelFx::Reticulation { .. } => Some(0.0),
PixelFx::FilmGrain { .. }
| PixelFx::Mezzotint { .. }
| PixelFx::HalftonePattern { .. }
| PixelFx::NotePaper { .. }
| PixelFx::Reticulation { .. } => Some(0.0),
PixelFx::Plaster { smoothness, .. } => Some(sketch::plaster_reach(smoothness)),
// Cutout's palette comes from the whole object.
PixelFx::Cutout { .. } => None,
@@ -639,6 +652,7 @@ impl PixelFx {
PixelFx::Chrome { detail, smoothness } => sketch::chrome(px, w, h, space, detail, smoothness),
PixelFx::GraphicPen { length, balance, direction } => brushstrokes::graphic_pen(px, w, h, space, length, balance, direction),
PixelFx::HalftonePattern { size, contrast, kind } => sketch::halftone_pattern(px, w, h, space, size, contrast, kind),
PixelFx::NotePaper { balance, graininess, relief } => sketch::note_paper(px, w, h, space, balance, graininess, relief),
PixelFx::BasRelief { detail, smoothness, light } => sketch_ink::bas_relief(px, w, h, space, detail, smoothness, light),
PixelFx::Charcoal { thickness, detail, balance } => sketch_ink::charcoal(px, w, h, space, thickness, detail, balance),
PixelFx::Photocopy { detail, darkness } => sketch_ink::photocopy(px, w, h, space, detail, darkness),
+60 -1
View File
@@ -1,4 +1,4 @@
//! Effect › Sketch (Chalk & Charcoal, Chrome, Halftone Pattern, Plaster and Reticulation; Graphic Pen lives in
//! Effect › Sketch (Chalk & Charcoal, Chrome, Halftone Pattern, Note Paper, Plaster and Reticulation; Graphic Pen lives in
//! [`super::brushstrokes`]). Original filters built from public behaviour documentation:
//! <https://helpx.adobe.com/illustrator/using/summary-effects.html> (Sketch effects redraw in
//! black and white) and the public Photoshop Elements Artistic filters guide. Visual
@@ -100,6 +100,65 @@ pub(super) fn halftone_pattern(px: &mut [[u8; 4]], w: usize, h: usize, space: &P
});
}
/// Note Paper: the object embossed on fibrous paper. `balance` (0..50) shifts tones toward
/// highlights or shadows around the middle; `graininess` (0..10) adds paper fibre; `relief`
/// (0..50, points) is how strongly the tones stand out in the light. Coverage is kept.
pub(super) fn note_paper(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, balance: f64, graininess: f64, relief: f64) {
if !(balance.is_finite() && graininess.is_finite() && relief.is_finite()) {
return;
}
let Some(place) = Place::new(space, w, h) else { return };
let height: Vec<f32> = px
.iter()
.map(|p| {
if p[3] == 0 {
0.0
} else {
let mut l = f64::from(luma(straight(*p)));
l += (balance - 25.0) / 125.0;
l.clamp(0.0, 1.0) as f32
}
})
.collect();
let sample = |x: i32, y: i32| -> f32 {
if x < 0 || y < 0 || x >= w as i32 || y >= h as i32 {
return 0.0;
}
height.get(y as usize * w + x as usize).copied().unwrap_or(0.0)
};
let emboss = relief / 50.0;
let grain = graininess / 10.0;
let paper = [0.95_f32, 0.93, 0.88];
let light = (-0.6_f64, -0.6, 0.8);
let ln = (light.0 * light.0 + light.1 * light.1 + light.2 * light.2).sqrt().max(1e-9);
place.each(px, |x, y, p| {
if p[3] == 0 {
return;
}
let xi = x as i32;
let yi = y as i32;
let h0 = height.get(y * w + x).copied().unwrap_or(0.0);
let dx = (sample(xi + 1, yi) - sample(xi - 1, yi)) as f64;
let dy = (sample(xi, yi + 1) - sample(xi, yi - 1)) as f64;
let dz = emboss * 0.12;
let nx = -dx;
let ny = -dy;
let nz = dz;
let nn = (nx * nx + ny * ny + nz * nz).sqrt().max(1e-9);
let shade = ((nx * light.0 + ny * light.1 + nz * light.2) / (nn * ln) * 0.5 + 0.5).clamp(0.0, 1.0);
let tone = (0.35 + 0.65 * f64::from(h0)) * shade;
let q = place.at(x, y);
let fibre = if grain > 0.0 {
value_noise(0x4e4f_5450, q.x * 0.35, q.y * 0.35) * grain * 0.18
} else {
0.0
};
let v = (tone + fibre).clamp(0.0, 1.0) as f32;
let rgb = paper.map(|c| c * v);
*p = premultiply(rgb, f32::from(p[3]) / 255.0);
});
}
/// Chalk & Charcoal: light areas get chalky highlights and dark areas charcoal strokes and grain;
/// `chalk` and `charcoal` (0..20) set how strongly, `pressure` (0..5) the stroke weight. Black
/// chalk and charcoal on white paper, coverage kept.
+38
View File
@@ -2106,6 +2106,44 @@ fn halftone_pattern_params_take_defaults_and_stay_in_range() {
assert_eq!(f.reach(), Some(0.0));
}
#[test]
fn note_paper_params_take_defaults_and_stay_in_range() {
assert_eq!(fx("sketch.notePaper", json!({})), PixelFx::NotePaper { balance: 25.0, graininess: 0.0, relief: 4.0 });
assert_eq!(
fx("sketch.notePaper", json!({"imageBalance": -1, "graininess": 20, "relief": 100})),
PixelFx::NotePaper { balance: 0.0, graininess: 10.0, relief: 50.0 }
);
let doc = crate::effect_info("sketch.notePaper").unwrap().params;
assert!(doc.contains("imageBalance") && doc.contains("graininess") && doc.contains("relief"), "{doc}");
let f = fx("sketch.notePaper", json!({"relief": 12}));
assert_eq!(f.outset(Rect::new(0.0, 0.0, 100.0, 50.0)), 0.0);
assert_eq!(f.reach(), Some(0.0));
}
/// Note Paper embosses a horizontal gradient and graininess adds fibre.
#[test]
fn note_paper_embosses_a_gradient_and_grain_changes_pixels() {
let (w, h) = (32, 32);
let src = image(w, h, |x, _| grey((x * 255 / (w - 1)) as u8));
let flat = {
let mut out = src.clone();
fx("sketch.notePaper", json!({"relief": 0, "graininess": 0})).apply(&mut out, w, h, &space(w, h));
out
};
let embossed = {
let mut out = src.clone();
fx("sketch.notePaper", json!({"relief": 50, "graininess": 0})).apply(&mut out, w, h, &space(w, h));
out
};
assert_ne!(flat, embossed, "relief changes the emboss");
let grainy = {
let mut out = src.clone();
fx("sketch.notePaper", json!({"relief": 0, "graininess": 10})).apply(&mut out, w, h, &space(w, h));
out
};
assert_ne!(flat, grainy, "graininess adds fibre");
}
/// Line's rows repeat every Size points: Size pixels at 72 ppi, twice as many at 144 ppi.
#[test]
fn halftone_line_period_is_the_size_at_any_resolution() {
+2 -1
View File
@@ -52,13 +52,14 @@ const PIXELATE: [(&str, &str); 4] =
[("pixelate.colorHalftone", "{}"), ("pixelate.crystallize", "{}"), ("pixelate.mezzotint", "{}"), ("pixelate.pointillize", "{}")];
/// Effect › Sketch, at their defaults.
const SKETCH: [(&str, &str); 10] = [
const SKETCH: [(&str, &str); 11] = [
("sketch.basRelief", "{}"),
("sketch.chalkCharcoal", "{}"),
("sketch.charcoal", "{}"),
("sketch.chrome", "{}"),
("sketch.graphicPen", "{}"),
("sketch.halftonePattern", "{}"),
("sketch.notePaper", "{}"),
("sketch.photocopy", "{}"),
("sketch.plaster", "{}"),
("sketch.reticulation", "{}"),
+8 -5
View File
@@ -187,6 +187,7 @@ fn effect_menu_lists_the_sketch_filters_between_sharpen_and_stylize() {
"Chrome…",
"Graphic Pen…",
"Halftone Pattern…",
"Note Paper…",
"Photocopy…",
"Plaster…",
"Reticulation…",
@@ -200,14 +201,16 @@ fn effect_menu_lists_the_sketch_filters_between_sharpen_and_stylize() {
assert_eq!(sketch[3].params, json!({"effect": "sketch.chrome"}));
assert_eq!(sketch[4].params, json!({"effect": "sketch.graphicPen"}));
assert_eq!(sketch[5].params, json!({"effect": "sketch.halftonePattern"}));
assert_eq!(sketch[6].params, json!({"effect": "sketch.photocopy"}));
assert_eq!(sketch[7].params, json!({"effect": "sketch.plaster"}));
assert_eq!(sketch[8].params, json!({"effect": "sketch.reticulation"}));
assert_eq!(sketch[9].params, json!({"effect": "sketch.stamp"}));
assert_eq!(sketch[6].params, json!({"effect": "sketch.notePaper"}));
assert_eq!(sketch[7].params, json!({"effect": "sketch.photocopy"}));
assert_eq!(sketch[8].params, json!({"effect": "sketch.plaster"}));
assert_eq!(sketch[9].params, json!({"effect": "sketch.reticulation"}));
assert_eq!(sketch[10].params, json!({"effect": "sketch.stamp"}));
let at = |label: &str| entries.iter().position(|e| e.path[0] == "Effect" && e.label == label).unwrap();
assert!(at("Unsharp Mask…") < at("Chalk & Charcoal…") && at("Chalk & Charcoal…") < at("Chrome…"));
assert!(at("Chrome…") < at("Graphic Pen…") && at("Graphic Pen…") < at("Halftone Pattern…"));
assert!(at("Halftone Pattern…") < at("Plaster…") && at("Plaster…") < at("Reticulation…"));
assert!(at("Halftone Pattern…") < at("Note Paper…") && at("Note Paper…") < at("Plaster…"));
assert!(at("Plaster…") < at("Reticulation…"));
assert!(at("Reticulation…") < at("Glowing Edges…"));
}
+14 -3
View File
@@ -1,6 +1,6 @@
# Effects parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor; Sketch › Bas Relief, Charcoal, Photocopy and Stamp, #2533; Photoshop filters 45/57, 12 remaining (8–14 h); G2: Sketch › Chrome; Sketch › Chalk & Charcoal, Graphic Pen and Reticulation and Artistic › Plastic Wrap, #2972, #2967, #2976, #3034: 37/57 Photoshop effects; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Zig Zag's Smooth points make a continuous wave, #2343; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 45/57, 12 remaining (8–14 h); Round Corners also rounds corners where a curve meets a line or another curve, with Live Corners' cut; straight corners unchanged; score unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; wide shadows and glows at high zoom, #1706; stacked Transform effects, #1705; 3D on nonzero compound paths, #1724; pattern tiles redraw, #1723; scores unchanged; Roughen's Smooth turn-backs, #1643; Scribble per region, #1648; score unchanged; Warp Arc bends in the object's proportions, #1578; Zig Zag ridges evenly spaced, #1558; effect parameters checked, #1592; score unchanged; raster effects after a pattern, radial or freeform gradient fill export at their place and size, #1536; score unchanged; 3D and Materials on outlined type and other shapes of many small curves, #1481; Twist 0° to 360°, #1524; twisted walls keep their faces, #1525; score unchanged; 3D and Materials: Inflate takes Twist, Taper and Cap; score unchanged; 3D and Materials: Perspective as the documented 0°–160° angle; 3D and Materials: the documented light settings) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (G2: Effect › Sketch › Note Paper; Halftone Pattern and Note Paper defaults documented as unverified VectorCraft choices; G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor; Sketch › Bas Relief, Charcoal, Photocopy and Stamp, #2533; Photoshop filters 46/57, 11 remaining (8–14 h); G2: Sketch › Chrome; Sketch › Chalk & Charcoal, Graphic Pen and Reticulation and Artistic › Plastic Wrap, #2972, #2967, #2976, #3034: 37/57 Photoshop effects; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Zig Zag's Smooth points make a continuous wave, #2343; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 46/57, 11 remaining (8–14 h); Round Corners also rounds corners where a curve meets a line or another curve, with Live Corners' cut; straight corners unchanged; score unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; wide shadows and glows at high zoom, #1706; stacked Transform effects, #1705; 3D on nonzero compound paths, #1724; pattern tiles redraw, #1723; scores unchanged; Roughen's Smooth turn-backs, #1643; Scribble per region, #1648; score unchanged; Warp Arc bends in the object's proportions, #1578; Zig Zag ridges evenly spaced, #1558; effect parameters checked, #1592; score unchanged; raster effects after a pattern, radial or freeform gradient fill export at their place and size, #1536; score unchanged; 3D and Materials on outlined type and other shapes of many small curves, #1481; Twist 0° to 360°, #1524; twisted walls keep their faces, #1525; score unchanged; 3D and Materials: Inflate takes Twist, Taper and Cap; score unchanged; 3D and Materials: Perspective as the documented 0°–160° angle; 3D and Materials: the documented light settings) · **Target:** Adobe Illustrator 2026 (30.x)
Every entry of Illustrator's Effect menu, by its public feature name, against VectorCraft's effects catalogue
(`effect_catalog()` in `crates/effects/src/lib.rs`) and the Effect menu built from it
@@ -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 | 46 | 92% | 6–10 |
| Photoshop effects (raster, Effect Gallery) | 57 | 45 | 79% | 8–14 |
| Photoshop effects (raster, Effect Gallery) | 57 | 46 | 81% | 8–14 |
| 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 | — | — |
@@ -44,7 +44,7 @@ its resolution, PDF export warns once per object with the requested and the lowe
| Distort | Diffuse Glow, Glass, Ocean Ripple | 3/3 | — |
| Pixelate | Color Halftone, Crystallize, Mezzotint, Pointillize | 4/4 | — |
| Sharpen | Unsharp Mask | 1/1 | — |
| Sketch | Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Conté Crayon, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster, Reticulation, Stamp, Torn Edges, Water Paper | 10/14 (approximate Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Graphic Pen, Halftone Pattern, Photocopy, Plaster, Reticulation, Stamp; Chalk & Charcoal and Graphic Pen defaults unverified) | Conté Crayon, Note Paper, Torn Edges, Water Paper |
| Sketch | Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Conté Crayon, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster, Reticulation, Stamp, Torn Edges, Water Paper | 11/14 (approximate Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster, Reticulation, Stamp; Chalk & Charcoal, Graphic Pen and Note Paper defaults unverified) | Conté Crayon, Torn Edges, Water Paper |
| Stylize | Glowing Edges | 1/1 | — |
| Texture | Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass, Texturizer | 6/6 | Texturizer's Load Texture |
| Video | De-Interlace, NTSC Colors | 2/2 | — |
@@ -149,6 +149,16 @@ VectorCraft, not asserted as measured proprietary defaults. Tests: `pixel::tests
`tests_rasterfilters`, `command_sweep`, `dialogs::tests::raster_filter_dialogs_preview_and_apply` and
`tests_labels::effect_menu_lists_the_sketch_filters_*`.
Effect › Sketch › Halftone Pattern and Note Paper (`sketch.halftonePattern`, `sketch.notePaper`) are
original black-and-white filters guided by [Illustrator's public Sketch effects summary](https://helpx.adobe.com/illustrator/using/summary-effects.html)
and [effects overview](https://helpx.adobe.com/illustrator/using/effects.html) (Halftone Size in points at any Document Raster Effects resolution).
The public help pages name each control but publish no numeric defaults or slider limits; VectorCraft's ranges
follow those control names and the emboss/halftone behaviour described there, while dialog defaults are chosen
for VectorCraft and are not verified against proprietary defaults. Evidence: `crates/effects/src/pixel/sketch.rs`,
`halftone_*` and `note_paper_*` in `crates/effects/src/pixel/tests.rs`, `SKETCH` and
`halftone_pattern_settings_survive_a_save_and_open` in `crates/engine/src/tests_rasterfilters.rs`, and
`tests_labels::effect_menu_lists_the_sketch_filters_between_sharpen_and_stylize`.
## 3D and Materials
| Item | Ours |
@@ -168,6 +178,7 @@ angular cut caps, exact visibility, 3D Classic, Turntable, 3D export) is 25–45
| Date | Change | Summary |
|---|---|---|
| 2026-10-20 | minor | G2: Effect › Sketch › Note Paper (Image Balance, Graininess, Relief; embossed tones on fibrous paper); Halftone Pattern and Note Paper parameter ranges follow public control names, numeric defaults are unverified VectorCraft choices (public help gives no numbers); measured Photoshop filters 46/57, Sketch 11/14. |
| 2026-10-19 | minor | Sketch › Chalk & Charcoal, Graphic Pen and Reticulation and Artistic › Plastic Wrap landed (#2972, #2967, #2976, #3034): 36/57 Photoshop effects (estimated; defaults unverified) |
| 2026-10-16 | minor | Zig Zag with Smooth points draws a continuous wave: every peak and valley is a smooth point with handles along the path, a third of the way to the next ridge, so each span follows a half cosine through the path instead of a straight diagonal with rounded tips, also at the ends of open paths and for ridges much taller than they are apart (#2343); score unchanged (estimated) |
| 2026-10-16 | minor | G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor (live, with their documented options; original approximations); measured Photoshop filters 36/57 with Cutout, Neon Glow and Poster Edges, 21 remaining (19–31 h). |