mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
With the Type tool editing, a press with Ctrl (Cmd on the Mac) lends the Selection tool for the drag. Its drag joined the open typing session, so its preview started from the document before the typing: the type moved or scaled and lost what had just been typed. The typing is now kept as its own undo step before the lent tool takes the press, as Undo already does, and typing goes on after the drag.
508 lines
19 KiB
Rust
508 lines
19 KiB
Rust
//! The command registry. Ids follow Illustrator's menu structure.
|
||
|
||
pub(crate) mod appearance;
|
||
mod assets;
|
||
mod attributes;
|
||
mod brushsym;
|
||
mod buildcmds;
|
||
pub mod clipboard;
|
||
mod colorcmds;
|
||
pub mod colormgmt;
|
||
pub mod colortheme;
|
||
mod create;
|
||
mod css;
|
||
mod cut;
|
||
pub(crate) mod distortcmds;
|
||
mod docinfo;
|
||
mod docmenu;
|
||
pub(crate) mod docsetup;
|
||
mod draw2;
|
||
mod edit;
|
||
mod effectcmd;
|
||
pub mod expand;
|
||
pub(crate) mod fileinfo;
|
||
pub mod fileio;
|
||
pub mod findfiles;
|
||
pub mod flatten;
|
||
pub mod fontfiles;
|
||
mod fonts;
|
||
pub(crate) mod freeform;
|
||
pub(crate) mod gradient;
|
||
pub(crate) mod graph;
|
||
mod halftone;
|
||
pub mod help;
|
||
pub(crate) mod inline;
|
||
mod layer;
|
||
mod layerpanel;
|
||
pub mod library;
|
||
pub mod linkfiles;
|
||
pub mod links;
|
||
mod live;
|
||
pub(crate) mod maskedit;
|
||
pub(crate) mod menucmds;
|
||
pub(crate) mod newart;
|
||
pub mod newdoc;
|
||
mod object;
|
||
mod opacitymask;
|
||
mod overprint;
|
||
mod package;
|
||
mod paint;
|
||
mod panelcmds;
|
||
pub mod params;
|
||
mod path;
|
||
mod pathops;
|
||
mod patterncmds;
|
||
pub mod pdfcmds;
|
||
pub mod perspgrid;
|
||
pub(crate) mod place;
|
||
pub mod plugin;
|
||
pub mod prefscmds;
|
||
pub mod print;
|
||
pub(crate) mod printadvanced;
|
||
pub mod printpresets;
|
||
pub mod printtiling;
|
||
pub mod rasterfx;
|
||
mod recolor;
|
||
pub mod recovery;
|
||
pub mod scripts;
|
||
mod select;
|
||
pub(crate) mod shaper;
|
||
pub(crate) mod slices;
|
||
mod stroke;
|
||
mod style;
|
||
pub mod stylelib;
|
||
mod swatch;
|
||
pub mod swatchlib;
|
||
pub(crate) mod tabs;
|
||
mod textedit;
|
||
pub mod textstyles;
|
||
pub(crate) mod textwrap;
|
||
pub(crate) mod threads;
|
||
pub(crate) mod typecmd;
|
||
mod typemenu;
|
||
mod variablecmds;
|
||
pub(crate) mod views;
|
||
pub mod wand;
|
||
pub mod webexport;
|
||
mod xform;
|
||
|
||
use serde::Serialize;
|
||
use serde_json::Value;
|
||
use vectorcraft_color::Color;
|
||
use vectorcraft_doc::NodeId;
|
||
use vectorcraft_geom::{Affine, Point};
|
||
|
||
use crate::{EngineError, Result, Session};
|
||
|
||
pub type Run = fn(&mut Session, &Value) -> Result<Value>;
|
||
pub type Enabled = fn(&Session) -> std::result::Result<(), String>;
|
||
|
||
/// Metadata + implementation for one command.
|
||
pub struct CommandSpec {
|
||
pub id: &'static str,
|
||
pub label: &'static str,
|
||
/// Menu placement, e.g. `["Object", "Arrange"]`. Empty = not in menus.
|
||
pub menu: &'static [&'static str],
|
||
/// Default shortcut (`Cmd+Shift+]`), mapped per platform by the UI.
|
||
pub shortcut: Option<&'static str>,
|
||
/// Human/agent-readable parameter description.
|
||
pub params: &'static str,
|
||
pub enabled: Enabled,
|
||
pub run: Run,
|
||
/// Record in the journal (false for queries and selection-only helpers).
|
||
pub journal: bool,
|
||
}
|
||
|
||
/// Serializable command metadata.
|
||
#[derive(Clone, Debug, Serialize)]
|
||
pub struct CommandInfo {
|
||
pub id: &'static str,
|
||
pub label: &'static str,
|
||
pub menu: Vec<&'static str>,
|
||
pub shortcut: Option<&'static str>,
|
||
pub params: &'static str,
|
||
pub enabled: bool,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub disabled_reason: Option<String>,
|
||
}
|
||
|
||
impl CommandSpec {
|
||
pub fn info(&self, s: &Session) -> CommandInfo {
|
||
let e = (self.enabled)(s);
|
||
CommandInfo {
|
||
id: self.id,
|
||
label: self.label,
|
||
menu: self.menu.to_vec(),
|
||
shortcut: self.shortcut,
|
||
params: self.params,
|
||
enabled: e.is_ok(),
|
||
disabled_reason: e.err(),
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------- enablement predicates ----------
|
||
|
||
pub fn always(_: &Session) -> std::result::Result<(), String> {
|
||
Ok(())
|
||
}
|
||
pub fn has_doc(s: &Session) -> std::result::Result<(), String> {
|
||
s.active().map(|_| ()).ok_or_else(|| "no document open".into())
|
||
}
|
||
pub fn has_selection(s: &Session) -> std::result::Result<(), String> {
|
||
let st = s.active().ok_or("no document open")?;
|
||
if st.selection.is_empty() { Err("nothing selected".into()) } else { Ok(()) }
|
||
}
|
||
/// Objects are selected, or the Artboard tool is chosen (Copy and Cut take its artboard).
|
||
pub fn has_selection_or_artboard_tool(s: &Session) -> std::result::Result<(), String> {
|
||
if s.tool_id() == "artboard" { has_doc(s) } else { has_selection(s) }
|
||
}
|
||
/// Objects or ruler guides are selected (what Delete and the arrow keys act on).
|
||
pub fn has_selection_or_guides(s: &Session) -> std::result::Result<(), String> {
|
||
let st = s.active().ok_or("no document open")?;
|
||
if st.selection.has_objects_or_guides() { Ok(()) } else { Err("nothing selected".into()) }
|
||
}
|
||
pub fn has_anchors(s: &Session) -> std::result::Result<(), String> {
|
||
let st = s.active().ok_or("no document open")?;
|
||
if st.selection.anchors.values().all(|a| a.is_empty()) { Err("no anchor points selected".into()) } else { Ok(()) }
|
||
}
|
||
pub fn has_multi(s: &Session) -> std::result::Result<(), String> {
|
||
let st = s.active().ok_or("no document open")?;
|
||
if st.selection.len() < 2 { Err("select at least two objects".into()) } else { Ok(()) }
|
||
}
|
||
pub fn can_undo(s: &Session) -> std::result::Result<(), String> {
|
||
s.active().filter(|d| !d.history.undo.is_empty()).map(|_| ()).ok_or_else(|| "nothing to undo".into())
|
||
}
|
||
pub fn can_redo(s: &Session) -> std::result::Result<(), String> {
|
||
s.active().filter(|d| !d.history.redo.is_empty()).map(|_| ()).ok_or_else(|| "nothing to redo".into())
|
||
}
|
||
pub fn has_clipboard(s: &Session) -> std::result::Result<(), String> {
|
||
has_doc(s)?;
|
||
if s.clipboard.is_empty() { Err("clipboard is empty".into()) } else { Ok(()) }
|
||
}
|
||
|
||
macro_rules! cmd {
|
||
($id:literal, $label:literal, [$($m:literal),*], $sc:expr, $params:literal, $en:expr, $run:expr) => {
|
||
$crate::cmd::CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: $sc, params: $params, enabled: $en, run: $run, journal: true }
|
||
};
|
||
(query $id:literal, $label:literal, [$($m:literal),*], $sc:expr, $params:literal, $en:expr, $run:expr) => {
|
||
$crate::cmd::CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: $sc, params: $params, enabled: $en, run: $run, journal: false }
|
||
};
|
||
}
|
||
pub(crate) use cmd;
|
||
|
||
pub fn command_specs() -> &'static [CommandSpec] {
|
||
static SPECS: std::sync::OnceLock<Vec<CommandSpec>> = std::sync::OnceLock::new();
|
||
SPECS.get_or_init(|| {
|
||
let mut v = Vec::new();
|
||
v.extend(edit::specs());
|
||
v.extend(clipboard::specs());
|
||
v.extend(recolor::specs());
|
||
v.extend(fileio::specs());
|
||
v.extend(create::specs());
|
||
v.extend(object::specs());
|
||
v.extend(path::specs());
|
||
v.extend(select::specs());
|
||
v.extend(wand::specs());
|
||
v.extend(paint::specs());
|
||
// Split out of `paint` (M3.8); kept next to it so lists in registry order (the command
|
||
// palette, `engine.commands`) show them where they always were.
|
||
v.extend(stroke::specs());
|
||
v.extend(appearance::specs());
|
||
v.extend(style::specs());
|
||
v.extend(swatch::specs());
|
||
v.extend(gradient::specs());
|
||
v.extend(opacitymask::specs());
|
||
v.extend(layer::specs());
|
||
v.extend(layerpanel::specs());
|
||
v.extend(draw2::specs());
|
||
v.extend(xform::specs());
|
||
v.extend(effectcmd::specs());
|
||
v.extend(pathops::specs());
|
||
v.extend(typecmd::specs());
|
||
v.extend(menucmds::specs());
|
||
v.extend(live::specs());
|
||
v.extend(colorcmds::specs());
|
||
v.extend(colormgmt::specs());
|
||
v.extend(typemenu::specs());
|
||
v.extend(textedit::specs());
|
||
v.extend(inline::specs());
|
||
v.extend(textstyles::specs());
|
||
v.extend(fonts::specs());
|
||
v.extend(fontfiles::specs());
|
||
v.extend(help::specs());
|
||
v.extend(threads::specs());
|
||
v.extend(textwrap::specs());
|
||
v.extend(graph::specs());
|
||
v.extend(rasterfx::specs());
|
||
v.extend(views::specs());
|
||
v.extend(maskedit::specs());
|
||
v.extend(tabs::specs());
|
||
v.extend(docmenu::specs());
|
||
v.extend(variablecmds::specs());
|
||
v.extend(docinfo::specs());
|
||
v.extend(panelcmds::specs());
|
||
v.extend(buildcmds::specs());
|
||
v.extend(shaper::specs());
|
||
v.extend(brushsym::specs());
|
||
v.extend(patterncmds::specs());
|
||
v.extend(prefscmds::specs());
|
||
v.extend(distortcmds::specs());
|
||
v.extend(paint::proxy_specs());
|
||
v.extend(overprint::specs());
|
||
v.extend(swatchlib::specs());
|
||
v.extend(freeform::specs());
|
||
v.extend(flatten::specs());
|
||
v.extend(stylelib::specs());
|
||
v.extend(library::specs());
|
||
v.extend(expand::specs());
|
||
v.extend(attributes::specs());
|
||
v.extend(newart::specs());
|
||
v.extend(colortheme::specs());
|
||
v.extend(fileio::pdf::specs());
|
||
v.extend(docsetup::specs());
|
||
v.extend(newdoc::specs());
|
||
v.extend(place::specs());
|
||
v.extend(links::specs());
|
||
v.extend(linkfiles::specs());
|
||
v.extend(pdfcmds::specs());
|
||
v.extend(fileio::dxf::specs());
|
||
v.extend(fileio::eps::specs());
|
||
v.extend(package::specs());
|
||
v.extend(slices::specs());
|
||
v.extend(print::specs());
|
||
v.extend(recovery::specs());
|
||
v.extend(fileio::dxfimport::specs());
|
||
v.extend(webexport::specs());
|
||
v.extend(assets::specs());
|
||
v.extend(css::specs());
|
||
v.extend(printpresets::specs());
|
||
v.extend(printtiling::specs());
|
||
v.extend(plugin::specs());
|
||
v.extend(scripts::specs());
|
||
v.extend(cut::specs());
|
||
v.extend(halftone::specs());
|
||
v.extend(perspgrid::specs());
|
||
v
|
||
})
|
||
}
|
||
|
||
pub fn find_command(id: &str) -> Option<&'static CommandSpec> {
|
||
command_specs().iter().find(|c| c.id == id)
|
||
}
|
||
|
||
// ---------- param helpers ----------
|
||
|
||
pub(crate) fn bad(cmd: &str, msg: impl Into<String>) -> EngineError {
|
||
EngineError::BadParams { cmd: cmd.into(), msg: msg.into() }
|
||
}
|
||
pub(crate) fn f64_or(p: &Value, key: &str, default: f64) -> f64 {
|
||
p.get(key).and_then(Value::as_f64).unwrap_or(default)
|
||
}
|
||
pub(crate) fn f64_req(p: &Value, key: &str, cmd: &str) -> Result<f64> {
|
||
p.get(key).and_then(Value::as_f64).ok_or_else(|| bad(cmd, format!("missing number `{key}`")))
|
||
}
|
||
pub(crate) fn bool_or(p: &Value, key: &str, default: bool) -> bool {
|
||
p.get(key).and_then(Value::as_bool).unwrap_or(default)
|
||
}
|
||
pub(crate) fn str_param<'a>(p: &'a Value, key: &str) -> Option<&'a str> {
|
||
p.get(key).and_then(Value::as_str)
|
||
}
|
||
pub(crate) fn id_param(p: &Value, key: &str) -> Option<NodeId> {
|
||
p.get(key).and_then(Value::as_u64).map(NodeId)
|
||
}
|
||
pub(crate) fn ids_param(p: &Value, key: &str) -> Option<Vec<NodeId>> {
|
||
p.get(key).and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(NodeId).collect())
|
||
}
|
||
/// Validate all explicit targets before a command changes the document or selection.
|
||
pub(crate) fn checked_ids_param(s: &Session, p: &Value, key: &str, command: &str) -> Result<Vec<NodeId>> {
|
||
let values =
|
||
p.get(key).and_then(Value::as_array).ok_or_else(|| bad(command, format!("{key} must be an array of non-negative integer object ids")))?;
|
||
values.iter().map(|value| checked_id(s, value, command)).collect()
|
||
}
|
||
|
||
/// The objects a command acts on: `ids` when given (every one checked), else the selected objects.
|
||
pub(crate) fn checked_ids_or_selection(s: &Session, p: &Value, command: &str) -> Result<Vec<NodeId>> {
|
||
if p.get("ids").is_some() { checked_ids_param(s, p, "ids", command) } else { edit::selected_roots(s) }
|
||
}
|
||
|
||
pub(crate) fn checked_id(s: &Session, value: &Value, command: &str) -> Result<NodeId> {
|
||
let id = value.as_u64().map(NodeId).ok_or_else(|| bad(command, format!("invalid object id {value}: expected a non-negative integer")))?;
|
||
if s.doc()?.doc.node(id).is_none() {
|
||
return Err(bad(command, format!("no such object id {value}")));
|
||
}
|
||
Ok(id)
|
||
}
|
||
|
||
/// `v` as exactly `N` finite numbers (`[x, y]`, `[x, y, width, height]`…): `None` when it isn't an
|
||
/// array of that length or an entry isn't a number, so nothing malformed is left out or padded.
|
||
pub(crate) fn finite_numbers<const N: usize>(v: &Value) -> Option<[f64; N]> {
|
||
let a = v.as_array().filter(|a| a.len() == N)?;
|
||
let mut out = [0.0; N];
|
||
for (o, n) in out.iter_mut().zip(a) {
|
||
*o = n.as_f64().filter(|n| n.is_finite())?;
|
||
}
|
||
Some(out)
|
||
}
|
||
|
||
/// `v` as a list of point indices: `None` unless it is an array of non-negative integers, so no
|
||
/// malformed index is dropped or wrapped round.
|
||
pub(crate) fn point_indices(v: &Value) -> Option<Vec<usize>> {
|
||
v.as_array()?.iter().map(|i| usize::try_from(i.as_u64()?).ok()).collect()
|
||
}
|
||
|
||
pub(crate) fn point_param(p: &Value, key: &str) -> Option<Point> {
|
||
let a = p.get(key)?.as_array()?;
|
||
Some(Point::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?))
|
||
}
|
||
/// `[a, b, c, d, e, f]` affine coefficients.
|
||
pub fn matrix_param(p: &Value, key: &str) -> Option<Affine> {
|
||
let a = p.get(key)?.as_array()?;
|
||
if a.len() != 6 {
|
||
return None;
|
||
}
|
||
let mut c = [0.0; 6];
|
||
for (i, v) in a.iter().enumerate() {
|
||
c[i] = v.as_f64()?;
|
||
}
|
||
Some(Affine::new(c))
|
||
}
|
||
/// A colour from `"#rrggbb"`, a colour key (`"cmyk 0 100 100 0"`, `"lab 55 60 40"`), `[r,g,b]` (0..1),
|
||
/// `{"c":..,"m":..,"y":..,"k":..}` (percent when any component is above 1, else 0..1), `{"gray":..}` or `{"l":..,"a":..,"b":..}` (CIE Lab: L 0..100, a and b about
|
||
/// −128..127).
|
||
pub fn color_value(v: &Value) -> Option<Color> {
|
||
match v {
|
||
// A hex, or a colour key ("cmyk 0 100 100 0", as Recolor Artwork lists colours).
|
||
Value::String(s) => Color::from_hex(s).or_else(|| vectorcraft_color::recolor::ColorKey::parse(s).map(|k| k.color())),
|
||
Value::Array(a) if a.len() >= 3 => Some(Color::rgb(a[0].as_f64()? as f32, a[1].as_f64()? as f32, a[2].as_f64()? as f32)),
|
||
Value::Object(o) => {
|
||
if let (Some(c), Some(m), Some(y), Some(k)) = (o.get("c"), o.get("m"), o.get("y"), o.get("k")) {
|
||
// One scale for the whole colour: percent when any component is above 1, else 0..1.
|
||
// Deciding per component read {c: 18, k: 0.79} as K 79 %.
|
||
let v = [c.as_f64()?, m.as_f64()?, y.as_f64()?, k.as_f64()?];
|
||
let scale = if v.iter().any(|x| *x > 1.0) { 100.0 } else { 1.0 };
|
||
let f = |x: f64| (x / scale) as f32;
|
||
return Some(Color::cmyk(f(v[0]), f(v[1]), f(v[2]), f(v[3])));
|
||
}
|
||
if let Some(g) = o.get("gray") {
|
||
let g = g.as_f64()?;
|
||
return Some(Color::gray(if g > 1.0 { g / 100.0 } else { g } as f32));
|
||
}
|
||
if let (Some(l), Some(a), Some(b), None) = (o.get("l"), o.get("a"), o.get("b"), o.get("model")) {
|
||
return Some(Color::lab(l.as_f64()? as f32, a.as_f64()? as f32, b.as_f64()? as f32));
|
||
}
|
||
serde_json::from_value(v.clone()).ok()
|
||
}
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// Objects a command targets: explicit `ids` param or the selection.
|
||
pub(crate) fn targets(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
|
||
// Objects given are used as given: a value that isn't an object id fails rather than the
|
||
// command acting on the selection instead (#785).
|
||
let object_id = |v: &Value| v.as_u64().map(NodeId).ok_or_else(|| EngineError::Other(format!("object ids are non-negative integers, not {v}")));
|
||
match p.get("ids").filter(|v| !v.is_null()) {
|
||
Some(Value::Array(a)) => return a.iter().map(object_id).collect(),
|
||
Some(v) => return Err(EngineError::Other(format!("`ids` must be an array of object ids, not {v}"))),
|
||
None => {}
|
||
}
|
||
match p.get("id").filter(|v| !v.is_null()) {
|
||
Some(v @ Value::Number(_)) => Ok(vec![object_id(v)?]),
|
||
// A step reference a batch couldn't resolve (other strings are other ids, such as an
|
||
// effect's).
|
||
Some(Value::String(r)) if r.starts_with('$') => {
|
||
Err(EngineError::Other(format!("`id` is the step reference {r:?}, which only `run` and `command_batch` resolve")))
|
||
}
|
||
_ => Ok(s.doc()?.selection.objects.clone()),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn ok() -> Result<Value> {
|
||
Ok(Value::Null)
|
||
}
|
||
|
||
/// Leaves whose appearance a paint command changes: groups and layers expand to their contents,
|
||
/// and compound paths own their children's appearance.
|
||
pub(crate) fn leaf_targets(s: &Session, ids: &[NodeId]) -> Result<Vec<NodeId>> {
|
||
use vectorcraft_doc::NodeKind;
|
||
let d = &s.doc()?.doc;
|
||
let mut out = vec![];
|
||
for id in ids {
|
||
let Some(n) = d.node(*id) else { continue };
|
||
match &n.kind {
|
||
NodeKind::Group { .. } | NodeKind::Layer { .. } => n.walk(&mut |c| {
|
||
if !c.is_container() || matches!(c.kind, NodeKind::Compound { .. }) {
|
||
out.push(c.id)
|
||
}
|
||
}),
|
||
_ => out.push(*id),
|
||
}
|
||
}
|
||
let comp: Vec<NodeId> = out.iter().filter(|id| matches!(d.node(**id).map(|n| &n.kind), Some(NodeKind::Compound { .. }))).copied().collect();
|
||
out.retain(|id| !comp.iter().any(|c| d.parent_of(*id) == Some(*c)));
|
||
Ok(out)
|
||
}
|
||
|
||
/// [`leaf_targets`] of a command's [`targets`] (`ids`, `id` or the selection).
|
||
pub(crate) fn paint_targets(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
|
||
leaf_targets(s, &targets(s, p)?)
|
||
}
|
||
|
||
/// `base` if `taken` doesn't claim it, else the first free "base 2", "base 3", …
|
||
pub(crate) fn unique_name(base: &str, taken: impl Fn(&str) -> bool) -> String {
|
||
if !taken(base) {
|
||
return base.to_string();
|
||
}
|
||
(2..).map(|i| format!("{base} {i}")).find(|n| !taken(n)).unwrap_or_else(|| base.to_string())
|
||
}
|
||
|
||
pub(crate) use edit::{settle_interaction, typing_in_progress};
|
||
pub(crate) use vectorcraft_doc::range::parse_range;
|
||
|
||
/// A date param in Unix seconds that defaults to the clock: `None` when absent (now, see
|
||
/// [`clock_date`]), `Some(None)` for null (no date), else `Some(Some(seconds))`.
|
||
pub(crate) fn date_param(p: &Value, key: &str, cmd: &str) -> Result<Option<Option<i64>>> {
|
||
match p.get(key) {
|
||
None => Ok(None),
|
||
Some(Value::Null) => Ok(Some(None)),
|
||
Some(v) => v.as_i64().map(|t| Some(Some(t))).ok_or_else(|| bad(cmd, format!("{key} must be Unix seconds (an integer) or null"))),
|
||
}
|
||
}
|
||
|
||
/// The date a command stamps: `given` (see [`date_param`]), else now (none on the web, which has
|
||
/// no clock). It joins the running command's journal entry as `key`, so a replay of the journal
|
||
/// gives the same date whenever it runs (an action leaves it out: [`crate::Session::journal_for_action`]).
|
||
pub(crate) fn clock_date(s: &mut Session, key: &str, given: Option<Option<i64>>) -> Option<i64> {
|
||
let t = given.unwrap_or_else(vectorcraft_doc::metadata::now_unix);
|
||
s.note_journal(key, serde_json::json!(t));
|
||
t
|
||
}
|
||
|
||
/// The Transparency panel's state ([`Session::transparency_info`]).
|
||
pub use opacitymask::TransparencyInfo;
|
||
|
||
/// Eyedropper Options (`Prefs::eyedropper`).
|
||
pub use xform::{AppearanceAttrs, EyedropperAttrs, EyedropperOptions, FillAttrs, StrokeAttrs};
|
||
|
||
/// The Attributes panel's state ([`Session::attributes_info`]).
|
||
pub use overprint::AttributesInfo;
|
||
|
||
/// Object → Flatten Transparency settings (`object.flattenTransparency`).
|
||
pub use flatten::FlattenOptions;
|
||
|
||
/// A built-in or saved flattener preset ([`crate::Prefs::flattener_presets`]).
|
||
pub use flatten::FlattenerPreset;
|
||
|
||
/// The shapes a group selected alone gives the Pathfinder panel.
|
||
pub use pathops::shape_count;
|
||
|
||
/// Command ids kept only so older scripts keep working: `(alias, the command it duplicates)`.
|
||
/// They run as before but the command palette leaves them out (it lists the command once).
|
||
pub const ALIASES: &[(&str, &str)] = &[("object.convertDocumentColorMode", "file.documentColorMode")];
|
||
|
||
/// Is `id` an [`ALIASES`] entry?
|
||
pub fn is_alias(id: &str) -> bool {
|
||
ALIASES.iter().any(|(a, _)| *a == id)
|
||
}
|