Files
vectorcraft/crates/engine/src/tests_halftone.rs
T
bflatasticandClaude Opus 5.5 285d94a3e6 Fix #122: Object > Vector Halftone
The issue asked for a vector halftone that turns art or an image into
vector dots, lines or shapes by tone, with angle, frequency, shape, mono
or CMYK separations and clipping to the object. Nothing like it existed.

object.vectorHalftone renders the selection offscreen, samples its tone
once per cell of a grid turned by the screen angle and places a shape
whose area follows the tone (circle, ellipse, square, diamond or line).
Mono screens follow lightness in one colour; CMYK screens follow the ink
amounts the CMYK output separates, one screen per ink at the classic
angles, multiplied over each other. Dots are clipped to the art's
outline as real geometry (only the dots crossing it go through a
boolean). It is a command rather than a live effect: the live pipeline
can't sample rendered tone when exporting. Its dialog previews live and
OK keeps it as one undo step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:42:26 +07:00

54 lines
2.4 KiB
Rust

//! Object → Vector Halftone.
use serde_json::json;
use super::*;
use crate::tests_adjust::{fill_hex, node, red_rect, session};
#[test]
fn halftone_turns_art_into_dots_by_tone() {
let mut s = session();
let id = red_rect(&mut s);
s.execute("paint.setFill", &json!({"color": "#000000"})).unwrap();
let undo = s.doc().unwrap().history.undo.len();
let r = s.execute("object.vectorHalftone", &json!({"frequency": 18})).unwrap();
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
assert!(s.doc().unwrap().doc.node(id).is_none(), "the original is replaced");
let g = node(&s, NodeId(r["id"].as_u64().unwrap()));
let dots = r["dots"].as_u64().unwrap();
// 60 pt at 18 lpi (4 pt cells) turned 45°: about 225 cells, all black.
assert!((180..=320).contains(&dots), "{dots}");
let ch = g.children().unwrap();
assert_eq!(ch.len(), 1);
let (pd, b) = (ch[0].path_data().unwrap(), ch[0].geometric_bounds().unwrap());
assert!(pd.subpaths.len() as u64 >= dots / 2);
// Clipped to the art.
assert!(b.x0 >= 19.9 && b.y0 >= 19.9 && b.x1 <= 80.1 && b.y1 <= 80.1, "{b:?}");
}
#[test]
fn halftone_options() {
let mut s = session();
let id = red_rect(&mut s);
// Red art in CMYK: magenta and yellow screens, no cyan or black.
let r = s.execute("object.vectorHalftone", &json!({"mode": "cmyk", "shape": "square", "keepOriginal": true})).unwrap();
assert!(s.doc().unwrap().doc.node(id).is_some(), "the original is kept");
let g = node(&s, NodeId(r["id"].as_u64().unwrap()));
let inks: Vec<String> = g.children().unwrap().iter().map(|c| fill_hex(c)).collect();
assert_eq!(inks.len(), 2, "{inks:?}");
assert!(g.children().unwrap().iter().all(|c| c.blend == vectorcraft_color::BlendMode::Multiply));
// Light art gets no dots; inverted, it does.
let mut s = session();
let id = red_rect(&mut s);
s.execute("paint.setFill", &json!({"color": "#ffffff"})).unwrap();
assert!(s.execute("object.vectorHalftone", &json!({})).is_err());
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
assert!(s.execute("object.vectorHalftone", &json!({"invert": true, "shape": "line"})).is_ok());
// Bad input is refused.
for bad in [json!({"frequency": 0}), json!({"shape": "star"}), json!({"mode": "rgb"}), json!({"color": "nope"})] {
let mut s = session();
red_rect(&mut s);
assert!(s.execute("object.vectorHalftone", &bad).is_err(), "{bad}");
}
}