Files
vectorcraft/crates/effects/src/params.rs
T
bflatastic f4c245286e Add-ons: Color Effects — live Vector Halftone effect
Effect › Color Effects › Vector Halftone… (addon.color.halftone, also in
Object › Plug-ins › Color Effects) draws its object as the halftone dots
Object › Vector Halftone makes, live: mono (one dot colour, or each dot in
the art's colour under it) or CMYK screens, the five dot shapes,
frequency, angle, invert and clip to the art's outline.

- The screens, dots and caps move to vectorcraft_effects::addons::halftone,
  shared by the command (its behaviour and tests unchanged) and the effect.
- The renderer samples the art (vectorcraft_render::addons::halftone) and
  keeps the dots by the object's content and what it reads from the
  document, so unchanged art isn't sampled again; past 100000 cells per
  screen the object draws as it is.
- Expand Appearance turns the object into its dots, and the SVG, PDF, EPS
  and AI exporters write them as paths.
- Its dialog reuses the command's fields, without Keep Original.

Deferred: stochastic and radial patterns, a dot-gain curve, per-screen
separation settings, type or symbols as dots.
2026-10-30 22:13:33 +07:00

151 lines
6.6 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.
use super::catalog::{catalog_index, plugin_info};
use super::{CROP_MARKS, Effect, Map, Node, PIXEL_EFFECTS, RASTERIZE, Value, is_adjustment, is_pathfinder, is_three_d, json, util};
/// Dialog defaults for `id` (`None` for unknown effects).
pub fn default_params(id: &str) -> Option<Value> {
match catalog_index().get(id) {
Some(e) => Some(e.defaults.clone()),
None => plugin_info(id).map(|e| e.defaults),
}
}
/// `params` layered over the defaults of `id` (unknown keys are kept), in their current form (see
/// [`upgrade_params`]). The 3D effects' earlier `perspective` is read as `perspectiveAngle`, and
/// Scribble's earlier `variation` is read as `overlapVariation` and `spacingVariation`.
pub fn merged_params(id: &str, params: &Value) -> Value {
let mut out = match catalog_index().get(id).map(|e| &e.defaults) {
Some(Value::Object(m)) => m.clone(),
Some(_) => Map::new(),
None => vectorcraft_plugins::effect::installed(id).map(|p| p.manifest().defaults()).unwrap_or_default(),
};
// Upgraded before the defaults are under it: a default angle is no angle given.
if let Value::Object(p) = upgrade_params(id, params) {
out.extend(p);
}
Value::Object(out)
}
/// `params` for effect `id` in their current form. The 3D effects took Perspective as a 0–100 %
/// strength, `perspective`, before the documented 0°–160° angle, `perspectiveAngle`, and the light
/// as `lightAzimuth` and `lightElevation` before the documented Rotation and Height
/// (`lightRotation`, `lightHeight`, `lightBehind`). Scribble took one `variation` before its
/// separate `overlapVariation` and `spacingVariation`. Earlier values (from a file or a script of
/// then) become the current ones that show exactly the same, unless those are given too, which win.
/// Scribble's angle is also stored above -180° and up to 180° (270° becomes -90°).
pub fn upgrade_params(id: &str, params: &Value) -> Value {
match params {
Value::Object(m) => {
let mut m = m.clone();
upgrade_perspective(id, &mut m);
upgrade_light(id, &mut m);
upgrade_scribble(id, &mut m);
Value::Object(m)
}
other => other.clone(),
}
}
fn upgrade_light(id: &str, m: &mut Map<String, Value>) {
if !is_three_d(id) {
return;
}
let (azimuth, elevation) = (m.remove("lightAzimuth"), m.remove("lightElevation"));
if azimuth.is_none() && elevation.is_none() || m.contains_key("lightRotation") || m.contains_key("lightHeight") {
return;
}
let degrees = |v: Option<Value>, default: f64| v.and_then(|v| v.as_f64()).unwrap_or(default);
let (rotation, height, behind) = vectorcraft_three_d::light_from_azimuth_elevation(degrees(azimuth, -45.0), degrees(elevation, 45.0));
m.insert("lightRotation".into(), json!(rotation));
m.insert("lightHeight".into(), json!(height));
m.insert("lightBehind".into(), json!(behind));
}
fn upgrade_perspective(id: &str, m: &mut Map<String, Value>) {
if !is_three_d(id) {
return;
}
let Some(strength) = m.remove("perspective") else { return };
if !m.contains_key("perspectiveAngle") {
let angle = vectorcraft_three_d::perspective_from_strength(strength.as_f64().unwrap_or(0.0) / 100.0);
m.insert("perspectiveAngle".into(), json!(angle));
}
}
fn upgrade_scribble(id: &str, m: &mut Map<String, Value>) {
if id != "stylize.scribble" {
return;
}
if let Some(variation) = m.remove("variation") {
for key in ["overlapVariation", "spacingVariation"] {
m.entry(key).or_insert_with(|| variation.clone());
}
}
if let Some(angle) = m.get("angle").and_then(Value::as_f64).filter(|a| a.is_finite() && (*a <= -180.0 || *a > 180.0)) {
let wrapped = angle.rem_euclid(360.0);
m.insert("angle".into(), json!(if wrapped > 180.0 { wrapped - 360.0 } else { wrapped }));
}
}
/// A new effect record with default parameters (overridden by `params`).
pub fn new_effect(id: &str, params: &Value) -> Option<Effect> {
if !catalog_index().contains_key(id) {
vectorcraft_plugins::effect::installed(id)?;
}
Some(Effect { id: id.to_string(), params: merged_params(id, params), visible: true })
}
/// Scale the distance parameters of `e` by `s` (Scale Strokes & Effects; see
/// [`EffectInfo::lengths`]). A distance left at its default is written out scaled; one the
/// effect doesn't have (Outline Stroke's weight following its stroke) stays absent.
pub fn scale_effect(e: &mut Effect, s: f64) {
let Some(info) = catalog_index().get(e.id.as_str()) else { return };
let merged = merged_params(&e.id, &e.params);
let relative = util::flag(&merged, "relative", false);
for k in info.lengths.keys(relative) {
let v = util::num(&merged, k, f64::NAN);
if v.is_finite() {
if !e.params.is_object() {
e.params = Value::Object(Map::new());
}
e.params[k] = json!(v * s);
}
}
}
/// Is `id` a raster (painted) effect?
pub fn is_raster(id: &str) -> bool {
matches!(id, "stylize.dropShadow" | "stylize.innerGlow" | "stylize.outerGlow" | "stylize.feather" | "blur.gaussian" | RASTERIZE)
|| PIXEL_EFFECTS.contains(&id)
}
/// Does `id` change geometry? (Crop Marks and Vector Halftone add art of their own instead,
/// [`crop_marks_art`] and [`crate::addons::halftone::halftone_art`]; colour adjustments recolour,
/// [`adjust`].) Plug-in effects do, installed or not (a missing plug-in
/// leaves the geometry as it is).
pub fn is_geometry(id: &str) -> bool {
!is_raster(id)
&& !is_pathfinder(id)
&& !is_adjustment(id)
&& id != CROP_MARKS
&& id != crate::addons::halftone::HALFTONE
&& !is_three_d(id)
&& (catalog_index().contains_key(id) || vectorcraft_plugins::effect::plugin_id(id).is_some())
}
/// Any visible geometry effect in the list?
pub fn has_geometry(effects: &[Effect]) -> bool {
effects.iter().any(|e| e.visible && is_geometry(&e.id))
}
/// Does `n` have a visible plug-in effect, its own or a fill's or stroke's? Such an effect draws
/// what the plug-in installed under its id draws ([`plugin_revision`]).
pub fn has_plugin_effect(n: &Node) -> bool {
let plugin = |e: &Effect| e.visible && vectorcraft_plugins::effect::plugin_id(&e.id).is_some();
n.appearance.effects.iter().any(plugin) || n.appearance.items.iter().any(|i| i.effects().iter().any(plugin))
}
/// The plug-in registry's revision, which changes whenever a plug-in is installed or removed.
pub fn plugin_revision() -> u64 {
vectorcraft_plugins::registry::revision()
}