Files
vectorcraft/crates/engine/src/cmd/halftone.rs
T
dotCooCoo b967408a09 Vector Halftone: a parameter of the wrong type is refused
object.vectorHalftone read frequency, angle, invert, clip and
keepOriginal with readers that fall back to the default for a value of
the wrong type, so {"frequency": "60"} drew a 20 lpi screen. shape and
mode given as a non-string also fell back to circle and mono. Each of
these is now an error and the document is left as it was. A null
value, color included, still stands for the default.
2026-11-11 15:39:28 -08:00

99 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Object → Vector Halftone: the selected art (vectors, type, images) as vector dots, lines or
//! shapes sized by its tone, as one undo step.
//!
//! The screens, dots and caps are Color Effects' Vector Halftone's
//! ([`vectorcraft_render::effects::addons::halftone`]), which draws the same dots live; the art is
//! sampled as [`vectorcraft_render::addons::halftone::sample`] draws it.
use serde_json::{Value, json};
use vectorcraft_color::Color;
use vectorcraft_doc::Node;
use vectorcraft_render::effects::addons::halftone::{DotShape, Halftone, art_outline, dot_nodes};
use super::edit::selected_roots;
use super::{CommandSpec, EngineError, Result, Session, bad, cmd, color_value, has_selection, number_or, switch_or, text_param};
pub fn specs() -> Vec<CommandSpec> {
vec![cmd!(
"object.vectorHalftone",
"Vector Halftone…",
["Object"],
None,
"{shape?: \"circle\"|\"ellipse\"|\"square\"|\"diamond\"|\"line\" (circle), frequency?: lines per inch 1–300 (20), angle?: deg clockwise (45; 0: level rows of dots; CMYK: the black screen's, cyan −30°, magenta +30°, yellow −45° from it), mode?: \"mono\"|\"cmyk\" (mono), color?: mono dot colour (\"#000000\"; #rrggbbaa and #rgba are refused), invert?: bool (dots for light tones), clip?: true (clip the dots to the art's outline), keepOriginal?: false} the selection as vector halftone dots sized by its tone, a group of one path per screen (CMYK screens multiply), as one undo step → {id, dots, warning? (an image in the art couldn't be decoded, as object.rasterize says)}",
has_selection,
halftone
)]
}
fn halftone(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.vectorHalftone";
let shape = match text_param(p, "shape", C)? {
Some(v) => DotShape::parse(v).ok_or_else(|| bad(C, format!("unknown shape `{v}` (circle|ellipse|square|diamond|line)")))?,
None => DotShape::Circle,
};
let frequency = number_or(p, "frequency", 20.0, C)?;
if !(1.0..=300.0).contains(&frequency) {
return Err(bad(C, "frequency must be between 1 and 300 lines per inch"));
}
let angle = number_or(p, "angle", 45.0, C)?;
if !angle.is_finite() {
return Err(bad(C, "invalid angle"));
}
let mono = match text_param(p, "mode", C)?.unwrap_or("mono") {
"mono" => true,
"cmyk" => false,
other => return Err(bad(C, format!("unknown mode `{other}` (mono|cmyk)"))),
};
let color = match p.get("color") {
None | Some(Value::Null) => Color::BLACK,
Some(v) => color_value(v).ok_or_else(|| bad(C, "invalid color"))?,
};
let (invert, clip, keep) = (switch_or(p, "invert", false, C)?, switch_or(p, "clip", true, C)?, switch_or(p, "keepOriginal", false, C)?);
let ht = Halftone { shape, frequency, angle, mono, color, from_art: false, invert, clip };
let roots = selected_roots(s)?;
let st = s.doc()?;
let b = st.doc.bounds_of(&roots, true).ok_or_else(|| bad(C, "the selection has no bounds"))?;
if b.width() <= 0.0 || b.height() <= 0.0 || !b.area().is_finite() {
return Err(bad(C, "the selection has no area"));
}
let (region, scale) = ht.plan(b).ok_or_else(|| bad(C, "too many dots for this size: lower the frequency"))?;
let nodes: Vec<Node> = roots
.iter()
.filter_map(|id| st.doc.node(*id).cloned())
.map(|mut n| {
n.visible = true;
n
})
.collect();
let outline = if clip { art_outline(&nodes) } else { None };
let failed = vectorcraft_render::decode_failures();
let samples = vectorcraft_render::addons::halftone::sample(&st.doc, nodes, region, scale, mono, invert);
let warning = super::rasterfx::undecoded_warning(failed);
let samples = samples.ok_or_else(|| EngineError::Other("the art could not be sampled".into()))?;
let (layers, dots) = ht.layers(&samples, b, outline.as_ref());
if layers.is_empty() {
return Err(EngineError::Other("the art is too light for any dots".into()));
}
let top = *roots.last().ok_or_else(|| bad(C, "nothing selected"))?;
let id = s.edit("Vector Halftone", |d, sel| {
let children = dot_nodes(layers, mono, &mut || d.alloc_id());
let gid = d.alloc_id();
let mut g = Node::group(gid, children);
g.name = Some("Vector Halftone".into());
if keep {
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
d.insert(par, idx + 1, g)?;
} else {
d.wrap(&roots, |_| g)?;
}
sel.set([gid]);
Ok(gid)
})?;
let mut out = json!({ "id": id.0, "dots": dots });
if let Some(w) = warning {
out["warning"] = json!(w);
}
Ok(out)
}