mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
On main, Glowing Edges… is listed in the Effect menu's vector Stylize submenu, after Drop Shadow, Inner Glow, Outer Glow and Feather. The reference app lists it in the Stylize submenu of its Photoshop effects, below the vector effects. The Effect menu now has a Stylize submenu under Raster Effects, between Sharpen and Texture, and Glowing Edges is listed there. The menu sends the effects at the Stylize path that PIXEL_EFFECTS names to that submenu, and the vector Stylize submenu keeps its six effects. The effect's id, its catalog entry, its effect.list entry and Apply Last Effect do not change.
245 lines
12 KiB
Rust
245 lines
12 KiB
Rust
//! Neutral wording in menus and panels: what users read names VectorCraft's own features.
|
||
|
||
use egui::epaint::Shape;
|
||
use serde_json::json;
|
||
use vectorcraft_engine::Session;
|
||
|
||
use crate::VectorcraftApp;
|
||
use crate::menus::{self, Item};
|
||
|
||
fn app() -> VectorcraftApp {
|
||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||
app
|
||
}
|
||
|
||
/// Every string painted by `f` in a headless frame (app fonts installed), one per line. The second
|
||
/// of two frames: windows size themselves invisibly in their first.
|
||
pub(crate) fn painted_text(app: &mut VectorcraftApp, mut f: impl FnMut(&mut VectorcraftApp, &mut egui::Ui)) -> String {
|
||
let ctx = egui::Context::default();
|
||
crate::theme::install_fonts(&ctx);
|
||
ctx.run_ui(egui::RawInput::default(), |ui| f(app, ui)).textures_delta.clear();
|
||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| f(app, ui));
|
||
out.textures_delta.clear();
|
||
shapes_text(&out)
|
||
}
|
||
|
||
/// Every string a frame painted, one per line.
|
||
pub(crate) fn shapes_text(out: &egui::FullOutput) -> String {
|
||
fn collect(s: &Shape, out: &mut String) {
|
||
match s {
|
||
Shape::Text(t) => {
|
||
out.push_str(t.galley.text());
|
||
out.push('\n');
|
||
}
|
||
Shape::Vec(v) => v.iter().for_each(|s| collect(s, out)),
|
||
_ => {}
|
||
}
|
||
}
|
||
let mut text = String::new();
|
||
for c in &out.shapes {
|
||
collect(&c.shape, &mut text);
|
||
}
|
||
text
|
||
}
|
||
|
||
#[test]
|
||
fn effect_menu_has_a_vector_effects_section() {
|
||
let tree = menus::menu_tree();
|
||
let (_, effect) = tree.iter().find(|(t, _)| *t == "Effect").expect("Effect menu");
|
||
assert!(effect.iter().any(|i| matches!(i, Item::Header("Vector Effects"))));
|
||
}
|
||
|
||
#[test]
|
||
fn effect_menu_lists_film_grain_in_artistic_before_blur() {
|
||
let app = app();
|
||
let entries = menus::menu_entries(&app);
|
||
let artistic: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Artistic"]).collect();
|
||
assert_eq!(artistic.len(), 1);
|
||
assert_eq!(artistic[0].label, "Film Grain…");
|
||
assert_eq!(artistic[0].command.as_deref(), Some("effect.dialog"));
|
||
let at = |name: &str| entries.iter().position(|e| e.path == ["Effect", name]).unwrap();
|
||
assert!(at("Artistic") < at("Blur"));
|
||
}
|
||
|
||
#[test]
|
||
fn effect_menu_lists_the_pixelate_filters_between_blur_and_sharpen() {
|
||
let app = app();
|
||
let entries = menus::menu_entries(&app);
|
||
let pixelate: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Pixelate"]).collect();
|
||
let labels: Vec<&str> = pixelate.iter().map(|e| e.label.as_str()).collect();
|
||
assert_eq!(labels, ["Color Halftone…", "Crystallize…", "Mezzotint…", "Pointillize…"]);
|
||
assert!(pixelate.iter().all(|e| e.command.as_deref() == Some("effect.dialog")));
|
||
let raster: Vec<&str> =
|
||
entries.iter().filter_map(|e| e.path.get(1).filter(|_| e.path.len() == 2 && e.path[0] == "Effect")).map(String::as_str).collect();
|
||
let at = |name: &str| raster.iter().position(|s| *s == name).unwrap();
|
||
assert!(at("Blur") < at("Pixelate") && at("Pixelate") < at("Sharpen"));
|
||
}
|
||
|
||
/// Effect › Video, after Sharpen: De-Interlace opens its dialog, NTSC Colors (no options) applies.
|
||
#[test]
|
||
fn effect_menu_lists_the_video_filters_last() {
|
||
let app = app();
|
||
let entries = menus::menu_entries(&app);
|
||
let video: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Video"]).collect();
|
||
let items: Vec<(&str, Option<&str>)> = video.iter().map(|e| (e.label.as_str(), e.command.as_deref())).collect();
|
||
assert_eq!(items, [("De-Interlace…", Some("effect.dialog")), ("NTSC Colors", Some("effect.apply"))]);
|
||
let raster: Vec<&str> =
|
||
entries.iter().filter_map(|e| e.path.get(1).filter(|_| e.path.len() == 2 && e.path[0] == "Effect")).map(String::as_str).collect();
|
||
let at = |name: &str| raster.iter().position(|s| *s == name).unwrap();
|
||
assert!(at("Sharpen") < at("Video"));
|
||
}
|
||
|
||
/// Effect › Distort, between Blur and Pixelate: each filter opens its dialog.
|
||
#[test]
|
||
fn effect_menu_lists_the_distort_filters_between_blur_and_pixelate() {
|
||
let app = app();
|
||
let entries = menus::menu_entries(&app);
|
||
let distort: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Distort"]).collect();
|
||
let labels: Vec<&str> = distort.iter().map(|e| e.label.as_str()).collect();
|
||
assert_eq!(labels, ["Diffuse Glow…", "Glass…", "Ocean Ripple…"]);
|
||
assert!(distort.iter().all(|e| e.command.as_deref() == Some("effect.dialog")));
|
||
let raster: Vec<&str> =
|
||
entries.iter().filter_map(|e| e.path.get(1).filter(|_| e.path.len() == 2 && e.path[0] == "Effect")).map(String::as_str).collect();
|
||
let at = |name: &str| raster.iter().position(|s| *s == name).unwrap();
|
||
assert!(at("Blur") < at("Distort") && at("Distort") < at("Pixelate"));
|
||
// The vector Distort & Transform stays its own submenu.
|
||
assert!(raster.contains(&"Distort & Transform"));
|
||
}
|
||
|
||
/// Effect › Brush Strokes, between Blur and Distort: each filter opens its dialog.
|
||
#[test]
|
||
fn effect_menu_lists_the_brush_strokes_filters_between_blur_and_distort() {
|
||
let app = app();
|
||
let entries = menus::menu_entries(&app);
|
||
let strokes: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Brush Strokes"]).collect();
|
||
let labels: Vec<&str> = strokes.iter().map(|e| e.label.as_str()).collect();
|
||
assert_eq!(
|
||
labels,
|
||
["Accented Edges…", "Angled Strokes…", "Crosshatch…", "Dark Strokes…", "Ink Outlines…", "Spatter…", "Sprayed Strokes…", "Sumi-e…"]
|
||
);
|
||
assert!(strokes.iter().all(|e| e.command.as_deref() == Some("effect.dialog")));
|
||
let raster: Vec<&str> =
|
||
entries.iter().filter_map(|e| e.path.get(1).filter(|_| e.path.len() == 2 && e.path[0] == "Effect")).map(String::as_str).collect();
|
||
let at = |name: &str| raster.iter().position(|s| *s == name).unwrap();
|
||
assert!(at("Blur") < at("Brush Strokes") && at("Brush Strokes") < at("Distort"));
|
||
}
|
||
|
||
/// Effect › Texture, between Sharpen and Video: each filter opens its dialog.
|
||
#[test]
|
||
fn effect_menu_lists_the_texture_filters_between_sharpen_and_video() {
|
||
let app = app();
|
||
let entries = menus::menu_entries(&app);
|
||
let texture: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Texture"]).collect();
|
||
let labels: Vec<&str> = texture.iter().map(|e| e.label.as_str()).collect();
|
||
assert_eq!(labels, ["Craquelure…", "Grain…", "Mosaic Tiles…", "Patchwork…", "Stained Glass…", "Texturizer…"]);
|
||
assert!(texture.iter().all(|e| e.command.as_deref() == Some("effect.dialog")));
|
||
let raster: Vec<&str> =
|
||
entries.iter().filter_map(|e| e.path.get(1).filter(|_| e.path.len() == 2 && e.path[0] == "Effect")).map(String::as_str).collect();
|
||
let at = |name: &str| raster.iter().position(|s| *s == name).unwrap();
|
||
assert!(at("Sharpen") < at("Texture") && at("Texture") < at("Video"));
|
||
}
|
||
|
||
/// Effect › Stylize under Raster Effects, between Sharpen and Texture: Glowing Edges opens its
|
||
/// dialog.
|
||
#[test]
|
||
fn effect_menu_lists_glowing_edges_in_the_raster_stylize_between_sharpen_and_texture() {
|
||
let app = app();
|
||
let entries = menus::menu_entries(&app);
|
||
let at = |label: &str| entries.iter().position(|e| e.path[0] == "Effect" && e.label == label).unwrap();
|
||
let glowing = &entries[at("Glowing Edges…")];
|
||
assert_eq!(glowing.path, ["Effect", "Stylize"]);
|
||
assert_eq!(glowing.command.as_deref(), Some("effect.dialog"));
|
||
assert_eq!(glowing.params, json!({"effect": "stylize.glowingEdges"}));
|
||
assert!(at("Unsharp Mask…") < at("Glowing Edges…") && at("Glowing Edges…") < at("Craquelure…"));
|
||
}
|
||
|
||
/// The vector effects' Stylize keeps its six effects and leaves Glowing Edges out.
|
||
#[test]
|
||
fn effect_menu_keeps_glowing_edges_out_of_the_vector_stylize() {
|
||
let tree = menus::menu_tree();
|
||
let (_, effect) = tree.iter().find(|(t, _)| *t == "Effect").expect("Effect menu");
|
||
let raster = effect.iter().position(|i| matches!(i, Item::Header("Raster Effects"))).expect("Raster Effects");
|
||
let stylize: Vec<(usize, Vec<&str>)> = effect
|
||
.iter()
|
||
.enumerate()
|
||
.filter_map(|(i, it)| match it {
|
||
Item::Sub("Stylize", items) => Some((i, items.iter().filter_map(|c| if let Item::Cmd(l, ..) = c { Some(*l) } else { None }).collect())),
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
assert_eq!(stylize.len(), 2);
|
||
assert!(stylize[0].0 < raster && raster < stylize[1].0);
|
||
assert_eq!(stylize[0].1, ["Round Corners…", "Scribble…", "Drop Shadow…", "Inner Glow…", "Outer Glow…", "Feather…"]);
|
||
assert_eq!(stylize[1].1, ["Glowing Edges…"]);
|
||
}
|
||
|
||
#[test]
|
||
fn library_submenus_are_disabled_placeholders() {
|
||
let app = app();
|
||
let entries = menus::menu_entries(&app);
|
||
for lib in ["Brush Libraries", "Symbol Libraries"] {
|
||
let items: Vec<_> = entries.iter().filter(|e| e.path == ["Window", lib]).collect();
|
||
let labels: Vec<&str> = items.iter().map(|e| e.label.as_str()).collect();
|
||
assert_eq!(labels, ["Built-in Libraries", "User Defined", "Other Library…"], "{lib}");
|
||
assert!(items.iter().all(|e| !e.enabled && e.command.is_none()), "{lib}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn stroke_profiles_have_descriptive_names() {
|
||
let labels: Vec<&str> = vectorcraft_doc::WidthProfile::PRESETS.iter().map(|p| p.label).collect();
|
||
assert_eq!(labels, ["Uniform", "Lens", "Taper Start", "Taper End", "Pinch", "Teardrop", "Wave"]);
|
||
}
|
||
|
||
#[test]
|
||
fn swatches_menu_offers_save_swatch_library() {
|
||
let mut app = app();
|
||
let text = painted_text(&mut app, crate::panels::swatches::menu);
|
||
assert!(text.contains("Save Swatch Library…"), "{text}");
|
||
assert!(!text.contains("ASE"), "{text}");
|
||
}
|
||
|
||
#[test]
|
||
fn edit_menu_lists_pdf_presets() {
|
||
let app = app();
|
||
// Implemented (M4.44): enabled, and it opens Edit → PDF Presets.
|
||
assert!(
|
||
menus::menu_entries(&app)
|
||
.iter()
|
||
.any(|e| e.path == ["Edit"] && e.label == "PDF Presets…" && e.enabled && e.command.as_deref() == Some("ui.pdfPresetsDialog"))
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn clipboard_preferences_name_the_legacy_format_neutrally() {
|
||
let label = |key: &str| vectorcraft_engine::cmd::prefscmds::PREF_SPECS.iter().find(|p| p.key == key).unwrap().label;
|
||
assert_eq!(label("copyAicb"), "Legacy vector clipboard (no transparency)");
|
||
assert_eq!(label("aicbMode"), "Legacy vector clipboard");
|
||
}
|
||
|
||
#[test]
|
||
fn shortcut_set_old_name_is_accepted() {
|
||
use crate::shortcut_editor::{PRESETS, import_json};
|
||
let (old, _) = crate::shortcut_editor::LEGACY_PRESETS[0];
|
||
assert_eq!(PRESETS, ["VectorCraft Defaults", "Classic Defaults"]);
|
||
// UI preferences saved by an earlier version load (and save again) under the current name.
|
||
let ui: crate::state::UiState = serde_json::from_value(json!({ "shortcut_set": old })).unwrap();
|
||
assert_eq!(ui.shortcut_set, "Classic Defaults");
|
||
let saved = serde_json::to_value(&ui).unwrap();
|
||
assert_eq!(saved["shortcut_set"], "Classic Defaults");
|
||
let back: crate::state::UiState = serde_json::from_value(saved).unwrap();
|
||
assert_eq!(back.shortcut_set, "Classic Defaults");
|
||
let default: crate::state::UiState = serde_json::from_value(json!({})).unwrap();
|
||
assert_eq!(default.shortcut_set, PRESETS[0]);
|
||
// The command and imported sets take the old name too.
|
||
let mut app = app();
|
||
app.run("shortcuts.preset", json!({ "name": old })).unwrap();
|
||
assert_eq!(app.ui.shortcut_set, "Classic Defaults");
|
||
app.run("shortcuts.preset", json!({ "name": "VectorCraft Defaults" })).unwrap();
|
||
assert_eq!(app.ui.shortcut_set, "VectorCraft Defaults");
|
||
assert!(app.run("shortcuts.preset", json!({ "name": "Nope" })).is_err());
|
||
let (set, _) = import_json(&json!({ "set": old, "overrides": {} })).unwrap();
|
||
assert_eq!(set, "Classic Defaults");
|
||
}
|