diff --git a/ROADMAP.md b/ROADMAP.md index ec3f6e9c76..0270db445e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -133,6 +133,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap - 2026-11-14: Nest Shapes in the Cutting Tools add-on (Object › Plug-ins › Cutting Tools): the selected objects move close together onto an artboard, a sheet of a set size or a roll of a set width, packed by their bounding boxes with a margin inside the edges and a spacing between them, optionally turning objects 90° to fit; objects that do not fit go in a column beside the sheet; a dialog with Preview, one undo step (`addon.cutting.nest`). A VectorCraft addition: headline numbers unchanged. - 2026-11-05: Release Type on a Path joins the Type Tools add-on (Object › Plug-ins › Type Tools): each selected type on a path splits into point type with the same text and attributes, on one straight line starting where its first character sat, and the plain path it was set on, with its exact shape and place in the stacking order and no paint; or keeps only the path or only the text, optionally leaving the original. A dialog with Preview, one undo step (`addon.type.releasePath`, #5532). A VectorCraft addition: headline numbers unchanged. - 2026-11-04: New Document: the units chosen in Preset Details now belong to the chosen document, as the pane's options are listed for the selected preset; the chosen card shows them, every other Recent and Saved card keeps the units its document or preset was made in, and the built-in tabs still follow the chosen units, so switching between inches and millimetres no longer restyles every recent template. Scores and estimates unchanged. +- 2026-11-14: Packaging Tools add-on (Object › Plug-ins › Packaging Tools): fold line roles for dielines (Cut, Crease, Partial Cut, Perforation), each carried by the swatch on a path's top stroke through a per-document role table that native files keep; PDF prints a role line on its swatch's separation when that swatch is a spot color, as the default role swatches are, and SVG, DXF and HP-GL keep the stroke's color. Make Cut Line, Make Crease Line, Make Partial Cut Line and Make Perforation Line give the selected paths a role, overprinting by default, and make a missing role swatch; the Fold Lines panel counts each role in the selection or the document, and its Role Swatches… picks another swatch for a role (`addon.packaging.setRole`, `addon.packaging.roles`, `addon.packaging.setRoleSwatch`). A VectorCraft addition: headline numbers unchanged. - 2026-11-02: Mouse wheel over a closed font-family field in Character, Properties or Control changes selected text through the family list using saved font filters (excluding search text), one undo step per notch, stopping at the ends (#5253). Scores and estimates unchanged. - 2026-10-31: Pattern libraries: Window › Swatch Libraries › Patterns and the Swatches panel's library menu open Geometric Patterns, a built-in library of eleven pattern swatches (dots, stripes, checks, a grid and shapes); clicking one adds its pattern and swatch to the document, once, and applies it to the active fill or stroke (`swatch.library.add`). - 2026-11-11: Pattern libraries: Patterns › Grit Textures, a second built-in pattern library, holds eight seamless black textures (Fine Grain, Coarse Grit, Sand, Speckle, Stipple, Scratches, Sparse Flecks and Dense Flecks) on tiles of 18 to 36 pt, each drawn the same way every time; marks that reach past a tile edge continue on the next tile, so the tiles join without a seam (`swatch.library.add`). Scores and estimates unchanged. diff --git a/crates/engine/src/cmd/addons.rs b/crates/engine/src/cmd/addons.rs index 66c67651a3..8c310878fd 100644 --- a/crates/engine/src/cmd/addons.rs +++ b/crates/engine/src/cmd/addons.rs @@ -8,6 +8,7 @@ mod contour; mod cutting; mod document; mod image; +mod packaging; mod pathtools; mod pen; mod select; @@ -15,6 +16,7 @@ mod shapes; mod typetools; pub(crate) use image::{DecodedImages, refresh_color_masks}; +pub(crate) use packaging::{role_swatch_edited, role_swatches_merged}; use super::CommandSpec; @@ -27,6 +29,7 @@ pub fn specs() -> Vec { v.extend(cutting::specs()); v.extend(document::specs()); v.extend(image::specs()); + v.extend(packaging::specs()); v.extend(pathtools::specs()); v.extend(pen::specs()); v.extend(select::specs()); diff --git a/crates/engine/src/cmd/addons/packaging.rs b/crates/engine/src/cmd/addons/packaging.rs new file mode 100644 index 0000000000..7e262c3869 --- /dev/null +++ b/crates/engine/src/cmd/addons/packaging.rs @@ -0,0 +1,18 @@ +//! Packaging Tools (`addon.packaging.*`), a built-in add-on for the dielines of boxes and other +//! folded packaging. A path is a fold line when its top stroke is painted with the swatch of a line +//! role: Cut, Crease, Partial Cut or Perforation ([`roles`]). The document's role table maps each +//! role to its swatch, so every format that keeps the stroke's swatch keeps the role. Its submenu +//! (Object › Plug-ins › Packaging Tools) and its Fold Lines panel are the UI's. + +mod roles; + +pub(crate) use roles::{role_swatch_edited, role_swatches_merged}; + +use super::super::CommandSpec; + +pub fn specs() -> Vec { + roles::specs() +} + +#[cfg(test)] +mod tests; diff --git a/crates/engine/src/cmd/addons/packaging/roles.rs b/crates/engine/src/cmd/addons/packaging/roles.rs new file mode 100644 index 0000000000..010384c6c2 --- /dev/null +++ b/crates/engine/src/cmd/addons/packaging/roles.rs @@ -0,0 +1,454 @@ +//! Fold line roles (`addon.packaging.setRole`, `addon.packaging.roles`, +//! `addon.packaging.setRoleSwatch`). A path, compound path or live shape has a role when its top +//! stroke is painted with the swatch the document's role table gives that role, at any tint. The +//! table lives in [`Document::unknown`] under [`ROLES_KEY`], `{cut, crease, partialCut, +//! perforation}` each naming a swatch; a role it leaves out, or names with something other than a +//! string, takes its default swatch, named after the role. + +use serde_json::{Map, Value, json}; +use vectorcraft_color::swatch::REGISTRATION; +use vectorcraft_color::{Color, Paint, Swatch}; +use vectorcraft_doc::{Document, Node, NodeId, NodeKind}; + +use super::super::super::overprint::edit_counted; +use super::super::super::{CommandSpec, bad, checked_ids_or_selection, cmd, has_doc, has_selection, switch_or, text_param}; +use crate::{Result, Session}; + +/// Document unknown key: the role table, `{role id: swatch name}`. +pub(crate) const ROLES_KEY: &str = "packagingRoles"; + +const SET_ROLE: &str = "addon.packaging.setRole"; +const ROLES: &str = "addon.packaging.roles"; +const SET_ROLE_SWATCH: &str = "addon.packaging.setRoleSwatch"; + +/// What a fold line does when the blank is cut and folded. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Role { + /// Separates the material. + Cut, + /// A fold. + Crease, + /// A fold cut part way through the board. + PartialCut, + /// A fold along a line of cuts. + Perforation, +} + +impl Role { + pub(crate) const ALL: [Role; 4] = [Role::Cut, Role::Crease, Role::PartialCut, Role::Perforation]; + + /// The role's id in params and in the role table. + pub(crate) fn id(self) -> &'static str { + match self { + Role::Cut => "cut", + Role::Crease => "crease", + Role::PartialCut => "partialCut", + Role::Perforation => "perforation", + } + } + + /// The role's name, which is also its default swatch's name. + pub(crate) fn name(self) -> &'static str { + match self { + Role::Cut => "Cut", + Role::Crease => "Crease", + Role::PartialCut => "Partial Cut", + Role::Perforation => "Perforation", + } + } + + pub(crate) fn from_id(id: &str) -> Option { + Role::ALL.into_iter().find(|r| r.id() == id) + } + + /// The color a missing role swatch is made with (VectorCraft's own values): Cut 100% magenta, + /// Crease 100% cyan, Partial Cut 100% yellow, Perforation 50% magenta and 50% cyan. + fn color(self) -> Color { + match self { + Role::Cut => Color::cmyk(0.0, 1.0, 0.0, 0.0), + Role::Crease => Color::cmyk(1.0, 0.0, 0.0, 0.0), + Role::PartialCut => Color::cmyk(0.0, 0.0, 1.0, 0.0), + Role::Perforation => Color::cmyk(0.5, 0.5, 0.0, 0.0), + } + } + + fn index(self) -> usize { + Role::ALL.iter().position(|r| *r == self).unwrap_or(0) + } +} + +/// The swatch each role is carried by, in [`Role::ALL`] order. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct RoleTable([String; 4]); + +impl RoleTable { + /// The document's role table, with each missing or damaged entry at its default. When a damaged + /// table names one swatch for two roles, the later role takes its default name instead, or + /// "Crease 2" and so on when an earlier role has that. + pub(crate) fn of(d: &Document) -> RoleTable { + let stored = d.unknown.get(ROLES_KEY).and_then(Value::as_object); + let mut names: Vec = Vec::with_capacity(Role::ALL.len()); + for r in Role::ALL { + let name = match stored.and_then(|m| m.get(r.id())).and_then(Value::as_str) { + Some(name) if !name.is_empty() && !names.iter().any(|n| n == name) => name.to_string(), + _ => { + let base = r.name(); + std::iter::once(base.to_string()) + .chain((2..).map(|i| format!("{base} {i}"))) + .find(|n| !names.contains(n)) + .unwrap_or_else(|| base.to_string()) + } + }; + names.push(name); + } + RoleTable(Role::ALL.map(|r| names.get(r.index()).cloned().unwrap_or_else(|| r.name().to_string()))) + } + + /// The swatch of `role`. + pub(crate) fn swatch(&self, role: Role) -> &str { + self.0.get(role.index()).map_or("", String::as_str) + } + + /// The role swatch `name` carries. + pub(crate) fn role_of_swatch(&self, name: &str) -> Option { + Role::ALL.into_iter().find(|r| self.swatch(*r) == name) + } + + /// The role of object `n`: the role of the swatch its top stroke is painted with. + pub(crate) fn role_of(&self, n: &Node) -> Option { + if !can_carry(n) { + return None; + } + match &n.appearance.stroke()?.paint { + Paint::Solid { swatch: Some(name), .. } => self.role_of_swatch(name), + _ => None, + } + } + + fn to_value(&self) -> Value { + Value::Object(Role::ALL.into_iter().map(|r| (r.id().to_string(), json!(self.swatch(r)))).collect::>()) + } +} + +/// Swatch `from` of `d` is now named `to`, and `global` says whether it is still a global color. +/// A role it carried keeps it under the new name, as the strokes linked to it do; when it stopped +/// being global, which unlinks those strokes, the role moves to a new name that `setRole` makes a +/// swatch for. Another role that named `to` without such a swatch moves to a new name too, so no +/// two roles share a swatch. +pub(crate) fn role_swatch_edited(d: &mut Document, from: &str, to: &str, global: bool) { + let before = RoleTable::of(d); + let mut table = before.clone(); + for r in Role::ALL { + let name = table.swatch(r).to_string(); + let moved = if name == from && global { + Some(to.to_string()) + } else if name == from || (name == to && from != to) { + Some(free_role_name(d, &table, r)) + } else { + None + }; + if let (Some(new), Some(slot)) = (moved, table.0.get_mut(r.index())) { + *slot = new; + } + } + if table != before { + d.unknown.insert(ROLES_KEY.to_string(), table.to_value()); + } +} + +/// Swatches `merged` of `d` were merged into swatch `kept`, and `linked` says whether their strokes +/// now link to it (it is a global color). When `kept` carries no role, it takes the role of the +/// first merged swatch that carried one, so the lines of that role keep it; a merged swatch's other +/// role keeps its name, which the next `setRole` makes a swatch for. +pub(crate) fn role_swatches_merged(d: &mut Document, merged: &[String], kept: &str, linked: bool) { + let mut table = RoleTable::of(d); + if !linked || table.role_of_swatch(kept).is_some() { + return; + } + let Some(role) = merged.iter().find_map(|m| table.role_of_swatch(m)) else { return }; + if let Some(slot) = table.0.get_mut(role.index()) { + *slot = kept.to_string(); + } + d.unknown.insert(ROLES_KEY.to_string(), table.to_value()); +} + +/// A name for `role`'s swatch that no swatch, color group or other role of `table` uses: the +/// role's name, else "Cut 2", "Cut 3"… The document holds finitely many names, so the search +/// always ends at a free one. +fn free_role_name(d: &Document, table: &RoleTable, role: Role) -> String { + let free = |n: &str| !d.swatch_name_taken(n) && Role::ALL.into_iter().all(|r| r == role || table.swatch(r) != n); + let base = role.name(); + if free(base) { + return base.to_string(); + } + (2usize..).map(|i| format!("{base} {i}")).find(|n| free(n)).unwrap_or_else(|| base.to_string()) +} + +/// Whether `n` can carry a role: a path (a live shape too) that is neither a clipping path nor a +/// guide, or a compound path. +fn can_carry(n: &Node) -> bool { + match &n.kind { + NodeKind::Path { clipping, guide, .. } => !clipping && !guide, + NodeKind::Compound { .. } => true, + _ => false, + } +} + +/// The objects under the outermost objects `roots` that can carry a role, through groups and clip +/// groups, each once, and +/// how many of `roots` and their members can't (text, images, symbol instances and other objects). +/// With `editable`, objects that are locked or hidden, are in a locked or hidden group or layer, or +/// are on a locked artboard are left out and counted as skipped. +fn role_targets(d: &Document, roots: &[NodeId], editable: bool) -> (Vec, usize) { + fn walk(n: &Node, out: &mut Vec, skipped: &mut usize) { + match &n.kind { + NodeKind::Group { children, .. } => { + for c in children { + // A group's clipping path draws no stroke: it is skipped. + if matches!(c.kind, NodeKind::Path { clipping: true, .. }) { + *skipped += 1; + } else { + walk(c, out, skipped); + } + } + } + _ if can_carry(n) => out.push(n.id), + _ => *skipped += 1, + } + } + let (mut out, mut skipped) = (vec![], 0); + // A path of a compound path is painted with the compound's appearance: the compound takes the + // role. `roots` come from `checked_ids_or_selection`, which keeps only outermost objects, so a + // group and its member are never both roots. + let mut seen = std::collections::HashSet::new(); + let compound_of = |id: NodeId| d.parent_of(id).filter(|p| matches!(d.node(*p).map(|n| &n.kind), Some(NodeKind::Compound { .. }))); + for n in roots.iter().map(|id| compound_of(*id).unwrap_or(*id)).filter(|id| seen.insert(*id)).filter_map(|id| d.node(id)) { + walk(n, &mut out, &mut skipped); + } + if editable { + // Named directly or not, a locked or hidden object, or one in a locked or hidden group or + // layer or on a locked artboard, stays as it is. + let ok = d.editable(); + let before = out.len(); + out.retain(|id| ok(*id)); + skipped += before - out.len(); + } + (out, skipped) +} + +/// Every object of the document that can carry a role, through layers and groups. +fn document_targets(d: &Document) -> Vec { + fn walk(n: &Node, out: &mut Vec) { + match &n.kind { + NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } => children.iter().for_each(|c| walk(c, out)), + _ if can_carry(n) => out.push(n.id), + _ => {} + } + } + let mut out = vec![]; + d.layers.iter().for_each(|l| walk(l, &mut out)); + out +} + +pub fn specs() -> Vec { + vec![ + cmd!( + "addon.packaging.setRole", + "Make Fold Line", + ["Object", "Plug-ins", "Packaging Tools"], + None, + "{role: cut|crease|partialCut|perforation|none, ids?: [id…] (default: the selection), overprint?: bool (true)} give each path, compound path and live shape among the objects (through groups and clip groups; objects that are locked or hidden, in a locked or hidden group or layer, or on a locked artboard are left out and counted as skipped, even when named in ids) a fold line role: its top stroke is painted with the role's swatch (addon.packaging.roles) at 100% and shown, a path without a stroke gets a 1 pt stroke, and the stroke overprints, or with overprint: false does not. A missing role swatch is made as a global spot swatch (Cut 100% magenta, Crease 100% cyan, Partial Cut 100% yellow, Perforation 50% magenta and 50% cyan, in the document's color mode; named \"Cut 2\" and so on, in the role table too, when a color group or a swatch that can't carry a role, such as a process color that is not global, has the name). none takes the role away: a role stroke keeps the color it shows, no longer linked to the swatch, and stops overprinting. Text, images, symbol instances and other objects are skipped and counted. One undo step, none when nothing changes → {ids (the objects that can carry a role), changed, skipped}", + has_selection, + set_role + ), + cmd!( + query "addon.packaging.roles", + "Fold Lines", + [], + None, + "{ids?: [id…] (default: the selection)} the fold line roles → {roles: [{role: cut|crease|partialCut|perforation, name, swatch (the swatch that carries it), exists (the document has the swatch), count}], scope: ids|selection|document, selected (objects given or selected)}; count is how many of the paths, compound paths and live shapes among the objects (through groups, locked and hidden members included, each object once) have the role, or among every object of the document while nothing is selected and ids is left out", + has_doc, + roles + ), + cmd!( + "addon.packaging.setRoleSwatch", + "Role Swatches…", + ["Object", "Plug-ins", "Packaging Tools"], + None, + "{role: cut|crease|partialCut|perforation, swatch: a global or spot color swatch's name} make the swatch carry the role, in the document's role table, as one undo step (none when it already does). Objects stroked with the role's old swatch keep their color and no longer have the role. Refused: a missing swatch, a gradient or pattern swatch, a process color that is not global, a tint swatch, [None], [Registration], and a swatch another role has → {role, swatch, changed}", + has_doc, + set_role_swatch + ), + ] +} + +/// The role `p` gives: `Some(None)` for `none` when `none` is allowed. +fn role_param(p: &Value, cmd: &str, none: bool) -> Result> { + let id = text_param(p, "role", cmd)?.ok_or_else(|| bad(cmd, "missing `role`"))?; + match Role::from_id(id) { + Some(r) => Ok(Some(r)), + None if none && id == "none" => Ok(None), + None if none => Err(bad(cmd, format!("unknown role `{id}`: cut, crease, partialCut, perforation or none"))), + None => Err(bad(cmd, format!("unknown role `{id}`: cut, crease, partialCut or perforation"))), + } +} + +/// Why swatch `name` of `d` can't carry a role, if it can't. +fn unfit_swatch(d: &Document, name: &str) -> Option { + if name == REGISTRATION || name == "[None]" { + return Some(format!("`{name}` can't carry a role")); + } + let Some(sw) = d.swatch(name) else { return Some(format!("no swatch `{name}`")) }; + match &sw.paint { + Paint::Solid { swatch: Some(_), .. } => Some(format!("`{name}` is a tint swatch: pick its base or another global or spot color")), + Paint::Solid { .. } if sw.global => None, + Paint::Solid { .. } => Some(format!("`{name}` is a process color: pick a global or spot color")), + _ => Some(format!("`{name}` isn't a color swatch")), + } +} + +/// `role`'s swatch in `d`, made as a global spot swatch in the document's color mode when `d` +/// lacks it → its paint at 100%. When a color group or a swatch that can't carry a role (a process +/// color that is not global, a tint, a gradient) has the swatch's name, the swatch is made under a +/// free name, which the role table then gives the role. +fn role_paint(d: &mut Document, role: Role) -> Result { + let mut table = RoleTable::of(d); + let current = table.swatch(role); + let taken = match d.swatch(current) { + Some(_) => unfit_swatch(d, current).is_some(), + None => d.swatch_name_taken(current), + }; + if taken { + let free = free_role_name(d, &table, role); + if let Some(slot) = table.0.get_mut(role.index()) { + *slot = free; + } + d.unknown.insert(ROLES_KEY.to_string(), table.to_value()); + } + let name = table.swatch(role); + if d.swatch(name).is_none() { + let model = d.color_mode.model(); + let color = role.color(); + let color = if color.model() == model { + color + } else { + let cms = vectorcraft_color::cms::active(); + cms.convert(&color, model, cms.settings().intent) + }; + d.swatches.push(Swatch { name: name.to_string(), paint: Paint::solid(color), global: true, spot: true }); + } + if let Some(why) = unfit_swatch(d, name) { + return Err(bad(SET_ROLE, format!("{why} (the {} role's swatch; addon.packaging.setRoleSwatch picks another)", role.name()))); + } + d.tint_paint(name, 1.0).ok_or_else(|| bad(SET_ROLE, format!("`{name}` isn't a global or spot color"))) +} + +/// Gives object `id` of `d` `role`'s `paint` (`None`: takes its role away) → whether it changed. +fn give_role(d: &mut Document, table: &RoleTable, id: NodeId, paint: Option<&Paint>, overprint: bool) -> bool { + let Some(n) = d.node_mut(id) else { return false }; + let had = table.role_of(n); + let mut a = (*n.appearance).clone(); + match paint { + Some(paint) => { + a.set_stroke(paint.clone()); + if let Some(st) = a.stroke_mut() { + if st.width <= 0.0 { + st.width = 1.0; + } + // A hidden stroke would carry the role without drawing or printing the line. + st.visible = true; + st.overprint = overprint; + } + } + None => { + if had.is_none() { + return false; + } + if let Some(st) = a.stroke_mut() { + if let Paint::Solid { color, .. } = st.paint { + st.paint = Paint::solid(color); + } + st.overprint = false; + } + } + } + if a == *n.appearance { + return false; + } + n.appearance = vectorcraft_doc::Shared::new(a); + true +} + +fn set_role(s: &mut Session, p: &Value) -> Result { + let role = role_param(p, SET_ROLE, true)?; + let overprint = switch_or(p, "overprint", true, SET_ROLE)?; + let roots = checked_ids_or_selection(s, p, SET_ROLE)?; + let (ids, skipped) = role_targets(&s.doc()?.doc, &roots, true); + if ids.is_empty() { + return Err(bad(SET_ROLE, "select a path that is neither locked nor hidden: text, images and symbol instances can't have a fold line role")); + } + let label = role.map_or("Remove Fold Line Role", |_| "Make Fold Line"); + let mut changed = 0; + edit_counted(s, label, |d| { + let before = (d.swatches.len(), d.unknown.get(ROLES_KEY).cloned()); + let paint = role.map(|r| role_paint(d, r)).transpose()?; + // A role swatch made again (its strokes may still name it) is a change of its own. + let made = (d.swatches.len(), d.unknown.get(ROLES_KEY).cloned()) != before; + let table = RoleTable::of(d); + changed = ids.iter().filter(|id| give_role(d, &table, **id, paint.as_ref(), overprint)).count(); + Ok(changed + usize::from(made)) + })?; + Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::>(), "changed": changed, "skipped": skipped })) +} + +fn roles(s: &mut Session, p: &Value) -> Result { + let given = p.get("ids").is_some_and(|v| !v.is_null()); + let roots = checked_ids_or_selection(s, p, ROLES)?; + let d = &s.doc()?.doc; + let (scope, ids) = match (given, roots.is_empty()) { + (true, _) => ("ids", role_targets(d, &roots, false).0), + (false, false) => ("selection", role_targets(d, &roots, false).0), + (false, true) => ("document", document_targets(d)), + }; + let table = RoleTable::of(d); + let mut counts = [0usize; 4]; + for r in ids.iter().filter_map(|id| d.node(*id)).filter_map(|n| table.role_of(n)) { + if let Some(c) = counts.get_mut(r.index()) { + *c += 1; + } + } + let rows: Vec = Role::ALL + .into_iter() + .zip(counts) + .map(|(r, count)| { + let swatch = table.swatch(r); + json!({ "role": r.id(), "name": r.name(), "swatch": swatch, "exists": d.swatch(swatch).is_some(), "count": count }) + }) + .collect(); + Ok(json!({ "roles": rows, "scope": scope, "selected": roots.len() })) +} + +fn set_role_swatch(s: &mut Session, p: &Value) -> Result { + let Some(role) = role_param(p, SET_ROLE_SWATCH, false)? else { return Err(bad(SET_ROLE_SWATCH, "missing `role`")) }; + let name = text_param(p, "swatch", SET_ROLE_SWATCH)?.ok_or_else(|| bad(SET_ROLE_SWATCH, "missing `swatch`"))?; + let d = &s.doc()?.doc; + if let Some(why) = unfit_swatch(d, name) { + return Err(bad(SET_ROLE_SWATCH, why)); + } + let mut table = RoleTable::of(d); + if let Some(other) = Role::ALL.into_iter().find(|r| *r != role && table.swatch(*r) == name) { + return Err(bad(SET_ROLE_SWATCH, format!("`{name}` already carries the {} role", other.name()))); + } + if let Some(slot) = table.0.get_mut(role.index()) { + *slot = name.to_string(); + } + let value = table.to_value(); + let changed = RoleTable::of(d) != table; + if changed { + s.edit("Role Swatches", |d, _| { + d.unknown.insert(ROLES_KEY.to_string(), value); + Ok(()) + })?; + } + Ok(json!({ "role": role.id(), "swatch": name, "changed": changed })) +} diff --git a/crates/engine/src/cmd/addons/packaging/tests.rs b/crates/engine/src/cmd/addons/packaging/tests.rs new file mode 100644 index 0000000000..07a8bd3a30 --- /dev/null +++ b/crates/engine/src/cmd/addons/packaging/tests.rs @@ -0,0 +1,540 @@ +//! Tests for Packaging Tools: fold line roles, their swatches and the role table. + +use std::collections::BTreeSet; + +use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{AppearanceItem, Node, NodeKind}; + +use crate::*; + +fn session(mode: &str) -> Session { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 600, "height": 400, "colorMode": mode})).unwrap(); + s +} + +fn id_of(v: &Value) -> NodeId { + NodeId(v["id"].as_u64().unwrap()) +} + +fn rect(s: &mut Session, x: f64) -> NodeId { + id_of(&s.execute("shape.rectangle", &json!({"x": x, "y": 20, "width": 40, "height": 40})).unwrap()) +} + +fn node(s: &Session, id: NodeId) -> Node { + s.doc().unwrap().doc.node(id).cloned().unwrap() +} + +fn steps(s: &Session) -> usize { + s.doc().unwrap().history.undo.len() +} + +fn set_role(s: &mut Session, p: Value) -> Value { + s.execute("addon.packaging.setRole", &p).unwrap() +} + +fn err(s: &mut Session, cmd: &str, p: Value) -> String { + s.execute(cmd, &p).unwrap_err().to_string() +} + +fn b64(v: &Value) -> Vec { + vectorcraft_format::base64_decode(v["dataBase64"].as_str().expect("dataBase64")).unwrap() +} + +/// The `roles` query's counts, in role order. +fn counts(s: &mut Session, p: Value) -> Vec { + let r = s.execute("addon.packaging.roles", &p).unwrap(); + r["roles"].as_array().unwrap().iter().map(|r| r["count"].as_u64().unwrap()).collect() +} + +#[test] +fn set_role_makes_the_spot_swatch_and_links_the_top_stroke() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + assert!(s.doc().unwrap().doc.swatch("Cut").is_none()); + let before = steps(&s); + let r = set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + assert_eq!((r["changed"].as_u64(), r["skipped"].as_u64()), (Some(1), Some(0))); + assert_eq!(steps(&s), before + 1, "one undo step"); + let d = &s.doc().unwrap().doc; + let sw = d.swatch("Cut").unwrap(); + assert!(sw.global && sw.spot, "{sw:?}"); + assert_eq!(sw.paint, Paint::solid(Color::cmyk(0.0, 1.0, 0.0, 0.0)), "100% magenta in a CMYK document"); + let st = node(&s, a).appearance.stroke().cloned().unwrap(); + assert!(matches!(&st.paint, Paint::Solid { swatch: Some(n), tint, .. } if n == "Cut" && *tint == 1.0), "{:?}", st.paint); + assert!(st.overprint, "role strokes overprint by default"); + // Again: nothing changes, and no undo step is recorded. + let r = set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + assert_eq!(r["changed"].as_u64(), Some(0)); + assert_eq!(steps(&s), before + 1); + // Undo takes the swatch away with the role. + s.execute("edit.undo", &json!({})).unwrap(); + assert!(s.doc().unwrap().doc.swatch("Cut").is_none()); +} + +#[test] +fn each_role_gets_its_own_swatch_color_in_the_document_s_mode() { + let mut s = session("rgb"); + for (i, role) in ["cut", "crease", "partialCut", "perforation"].into_iter().enumerate() { + let id = rect(&mut s, 20.0 + 60.0 * i as f64); + set_role(&mut s, json!({"role": role, "ids": [id.0]})); + } + let d = &s.doc().unwrap().doc; + let colors: Vec = ["Cut", "Crease", "Partial Cut", "Perforation"].iter().map(|n| d.swatch(n).unwrap().paint.color().unwrap()).collect(); + assert!(colors.iter().all(|c| matches!(c, Color::Rgb { .. })), "an RGB document's swatches are RGB: {colors:?}"); + assert_eq!(colors.iter().map(|c| c.to_hex()).collect::>().len(), 4, "four colors: {colors:?}"); +} + +#[test] +fn a_path_without_a_stroke_gets_one_and_overprint_can_be_left_off() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + s.edit("strip", |d, _| { + let n = d.node_mut(a).unwrap(); + n.appearance.items.retain(AppearanceItem::is_fill); + Ok(()) + }) + .unwrap(); + assert!(node(&s, a).appearance.stroke().is_none()); + set_role(&mut s, json!({"role": "crease", "ids": [a.0], "overprint": false})); + let st = node(&s, a).appearance.stroke().cloned().unwrap(); + assert_eq!(st.width, 1.0, "a 1 pt stroke"); + assert!(!st.overprint); + assert!(matches!(&st.paint, Paint::Solid { swatch: Some(n), .. } if n == "Crease")); + assert!(err(&mut s, "addon.packaging.setRole", json!({"role": "cut", "ids": [a.0], "overprint": "yes"})).contains("overprint")); +} + +#[test] +fn none_takes_the_role_away_and_keeps_the_color() { + let mut s = session("cmyk"); + let (a, b) = (rect(&mut s, 20.0), rect(&mut s, 100.0)); + set_role(&mut s, json!({"role": "perforation", "ids": [a.0]})); + let shown = node(&s, a).appearance.stroke().unwrap().paint.color().unwrap(); + let before = steps(&s); + let r = set_role(&mut s, json!({"role": "none", "ids": [a.0, b.0]})); + assert_eq!(r["changed"].as_u64(), Some(1), "the plain rectangle has no role to take away"); + assert_eq!(steps(&s), before + 1); + let st = node(&s, a).appearance.stroke().cloned().unwrap(); + assert_eq!(st.paint, Paint::solid(shown)); + assert!(!st.overprint); + assert_eq!(counts(&mut s, json!({"ids": [a.0, b.0]})), [0, 0, 0, 0]); + // Nothing left with a role: no undo step. + set_role(&mut s, json!({"role": "none", "ids": [a.0, b.0]})); + assert_eq!(steps(&s), before + 1); +} + +#[test] +fn bad_roles_and_targets_are_refused() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + assert!(err(&mut s, "addon.packaging.setRole", json!({"role": "fold", "ids": [a.0]})).contains("unknown role")); + assert!(err(&mut s, "addon.packaging.setRole", json!({"ids": [a.0]})).contains("role")); + assert!(err(&mut s, "addon.packaging.setRole", json!({"role": 3, "ids": [a.0]})).contains("role")); + s.execute("select.none", &json!({})).unwrap(); + assert!(s.execute("addon.packaging.setRole", &json!({"role": "cut"})).is_err(), "nothing selected"); + let t = id_of(&s.execute("text.create", &json!({"x": 20, "y": 200, "text": "Lid", "size": 20})).unwrap()); + let before = steps(&s); + assert!(err(&mut s, "addon.packaging.setRole", json!({"role": "cut", "ids": [t.0]})).contains("select a path")); + assert_eq!(steps(&s), before); + assert!(s.doc().unwrap().doc.swatch("Cut").is_none(), "a refused run makes no swatch"); + // Text beside a path is skipped and counted. + let r = set_role(&mut s, json!({"role": "cut", "ids": [a.0, t.0]})); + assert_eq!((r["changed"].as_u64(), r["skipped"].as_u64()), (Some(1), Some(1))); +} + +#[test] +fn groups_are_walked_and_their_locked_and_hidden_members_left_out() { + let mut s = session("cmyk"); + let (a, b, c) = (rect(&mut s, 20.0), rect(&mut s, 100.0), rect(&mut s, 180.0)); + s.execute("select.set", &json!({"ids": [a.0, b.0, c.0]})).unwrap(); + let g = id_of(&s.execute("object.group", &json!({})).unwrap()); + s.edit("lock", |d, _| { + d.node_mut(b).unwrap().locked = true; + d.node_mut(c).unwrap().visible = false; + Ok(()) + }) + .unwrap(); + let r = set_role(&mut s, json!({"role": "crease", "ids": [g.0]})); + assert_eq!(r["ids"], json!([a.0])); + assert_eq!(r["skipped"].as_u64(), Some(2), "the locked and the hidden member"); + assert_eq!(counts(&mut s, json!({"ids": [g.0]})), [0, 1, 0, 0]); + // Named directly, a locked or hidden path is left out too. + let r = set_role(&mut s, json!({"role": "cut", "ids": [a.0, b.0, c.0]})); + assert_eq!((r["ids"].clone(), r["skipped"].as_u64()), (json!([a.0]), Some(2))); + let before = steps(&s); + assert!(err(&mut s, "addon.packaging.setRole", json!({"role": "cut", "ids": [b.0]})).contains("locked")); + assert_eq!(steps(&s), before); + assert_eq!(counts(&mut s, json!({"ids": [b.0, c.0]})), [0, 0, 0, 0]); +} + +#[test] +fn a_clip_group_s_clipping_path_is_skipped_and_counted() { + let mut s = session("cmyk"); + let (art, mask) = (rect(&mut s, 20.0), rect(&mut s, 30.0)); + s.execute("select.set", &json!({"ids": [art.0, mask.0]})).unwrap(); + let g = id_of(&s.execute("object.clippingMask.make", &json!({})).unwrap()); + let r = set_role(&mut s, json!({"role": "cut", "ids": [g.0]})); + assert_eq!((r["ids"].clone(), r["skipped"].as_u64()), (json!([art.0]), Some(1)), "the clipping path is left out and counted"); +} + +#[test] +fn a_group_and_its_member_count_once_and_locked_lines_count_in_every_scope() { + let mut s = session("cmyk"); + let (a, b) = (rect(&mut s, 20.0), rect(&mut s, 100.0)); + set_role(&mut s, json!({"role": "cut", "ids": [b.0]})); + s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap(); + let g = id_of(&s.execute("object.group", &json!({})).unwrap()); + let r = set_role(&mut s, json!({"role": "crease", "ids": [g.0, a.0]})); + assert_eq!(r["ids"], json!([a.0, b.0]), "each path once"); + assert_eq!(counts(&mut s, json!({"ids": [g.0, a.0]})), [0, 2, 0, 0]); + set_role(&mut s, json!({"role": "cut", "ids": [b.0]})); + s.edit("lock", |d, _| { + d.node_mut(b).unwrap().locked = true; + Ok(()) + }) + .unwrap(); + // A locked line keeps its role and is counted, whether its group is selected or not. + assert_eq!(counts(&mut s, json!({"ids": [g.0]})), [1, 1, 0, 0]); + s.execute("select.none", &json!({})).unwrap(); + assert_eq!(counts(&mut s, json!({})), [1, 1, 0, 0]); +} + +#[test] +fn a_process_swatch_named_like_a_role_swatch_keeps_its_name() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + s.execute("swatch.new", &json!({"name": "Cut", "color": "#ff0000"})).unwrap(); + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + let d = &s.doc().unwrap().doc; + assert_eq!(d.unknown.get("packagingRoles").unwrap()["cut"], json!("Cut 2")); + assert!(d.swatch("Cut 2").is_some_and(|w| w.spot), "the role swatch is made beside it"); + assert!(d.swatch("Cut").is_some_and(|w| !w.global), "the process swatch is left as it was"); + assert_eq!(counts(&mut s, json!({"ids": [a.0]})), [1, 0, 0, 0]); +} + +#[test] +fn the_free_role_swatch_name_is_searched_past_ten_thousand() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + let taken = std::iter::once("Cut".to_string()).chain((2..10_000).map(|i| format!("Cut {i}"))).chain(["Cut 10004".to_string()]); + let process = |name: String| vectorcraft_color::Swatch { name, paint: Paint::solid(Color::rgb(1.0, 0.0, 0.0)), global: false, spot: false }; + Arc::make_mut(&mut s.doc_mut().unwrap().doc).swatches.extend(taken.map(process)); + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + let d = &s.doc().unwrap().doc; + assert_eq!(d.unknown.get("packagingRoles").unwrap()["cut"], json!("Cut 10000")); + assert!(d.swatch("Cut 10000").is_some_and(|w| w.spot), "the role swatch is made under the first free name"); + assert!(d.swatch("Cut 10004").is_some_and(|w| !w.global), "a taken name is left as it was"); +} + +#[test] +fn a_merged_role_swatch_hands_its_role_to_the_kept_swatch() { + let mut s = session("cmyk"); + let (a, b) = (rect(&mut s, 20.0), rect(&mut s, 100.0)); + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + set_role(&mut s, json!({"role": "crease", "ids": [b.0]})); + s.execute("swatch.new", &json!({"name": "Die", "color": {"model": "cmyk", "c": 0, "m": 0.8, "y": 0.2, "k": 0}, "spot": true})).unwrap(); + s.execute("swatch.merge", &json!({"names": ["Die", "Cut"]})).unwrap(); + assert_eq!(s.doc().unwrap().doc.unknown.get("packagingRoles").unwrap()["cut"], json!("Die")); + assert_eq!(counts(&mut s, json!({"ids": [a.0]})), [1, 0, 0, 0], "the cut line is still a cut line"); + // A kept swatch that already carries a role keeps it; the merged role's lines take it. + s.execute("swatch.merge", &json!({"names": ["Crease", "Die"]})).unwrap(); + let table = s.doc().unwrap().doc.unknown.get("packagingRoles").cloned().unwrap(); + assert_eq!((&table["cut"], &table["crease"]), (&json!("Die"), &json!("Crease")), "{table}"); + assert_eq!(counts(&mut s, json!({"ids": [a.0, b.0]})), [0, 2, 0, 0]); + // The next Make Cut Line makes the Cut role's swatch again. + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + assert!(s.doc().unwrap().doc.swatch("Die").is_some_and(|w| w.spot)); + assert_eq!(counts(&mut s, json!({"ids": [a.0, b.0]})), [1, 1, 0, 0]); +} + +#[test] +fn a_global_process_swatch_can_carry_a_role() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + s.execute("swatch.new", &json!({"name": "Die", "color": {"model": "cmyk", "c": 0, "m": 0.8, "y": 0.2, "k": 0}, "global": true})).unwrap(); + s.execute("addon.packaging.setRoleSwatch", &json!({"role": "partialCut", "swatch": "Die"})).unwrap(); + set_role(&mut s, json!({"role": "partialCut", "ids": [a.0]})); + assert!(matches!(&node(&s, a).appearance.stroke().unwrap().paint, Paint::Solid { swatch: Some(n), .. } if n == "Die")); + assert_eq!(counts(&mut s, json!({"ids": [a.0]})), [0, 0, 1, 0]); +} + +#[test] +fn roles_counts_the_selection_or_the_whole_document() { + let mut s = session("cmyk"); + let (a, b, c) = (rect(&mut s, 20.0), rect(&mut s, 100.0), rect(&mut s, 180.0)); + set_role(&mut s, json!({"role": "cut", "ids": [a.0, b.0]})); + set_role(&mut s, json!({"role": "partialCut", "ids": [c.0]})); + s.execute("select.set", &json!({"ids": [a.0, c.0]})).unwrap(); + let r = s.execute("addon.packaging.roles", &json!({})).unwrap(); + assert_eq!((r["scope"].as_str(), r["selected"].as_u64()), (Some("selection"), Some(2))); + assert_eq!(counts(&mut s, json!({})), [1, 0, 1, 0]); + s.execute("select.none", &json!({})).unwrap(); + let r = s.execute("addon.packaging.roles", &json!({})).unwrap(); + assert_eq!((r["scope"].as_str(), r["selected"].as_u64()), (Some("document"), Some(0))); + assert_eq!(counts(&mut s, json!({})), [2, 0, 1, 0]); + let rows = r["roles"].as_array().unwrap(); + assert_eq!(rows[0], json!({"role": "cut", "name": "Cut", "swatch": "Cut", "exists": true, "count": 2})); + assert_eq!(rows[1]["exists"], json!(false), "no Crease swatch yet"); +} + +#[test] +fn role_swatches_maps_a_role_to_another_swatch() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + s.execute("swatch.new", &json!({"name": "Die", "color": {"model": "cmyk", "c": 0, "m": 0.8, "y": 0.2, "k": 0}, "spot": true})).unwrap(); + let before = steps(&s); + let r = s.execute("addon.packaging.setRoleSwatch", &json!({"role": "cut", "swatch": "Die"})).unwrap(); + assert_eq!(r["changed"], json!(true)); + assert_eq!(steps(&s), before + 1); + assert_eq!(s.doc().unwrap().doc.unknown.get("packagingRoles").unwrap()["cut"], json!("Die")); + // The old swatch's objects keep their color and lose the role. + assert_eq!(counts(&mut s, json!({"ids": [a.0]})), [0, 0, 0, 0]); + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + assert!(matches!(&node(&s, a).appearance.stroke().unwrap().paint, Paint::Solid { swatch: Some(n), .. } if n == "Die")); + // The same mapping again changes nothing. + let steps_now = steps(&s); + let r = s.execute("addon.packaging.setRoleSwatch", &json!({"role": "cut", "swatch": "Die"})).unwrap(); + assert_eq!((r["changed"].clone(), steps(&s)), (json!(false), steps_now)); +} + +#[test] +fn a_renamed_role_swatch_keeps_its_role() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + set_role(&mut s, json!({"role": "crease", "ids": [a.0]})); + s.execute("swatch.edit", &json!({"name": "Crease", "newName": "Score"})).unwrap(); + assert_eq!(s.doc().unwrap().doc.unknown.get("packagingRoles").unwrap()["crease"], json!("Score")); + assert_eq!(counts(&mut s, json!({"ids": [a.0]})), [0, 1, 0, 0]); + // A swatch that carries no role leaves the table alone. + s.execute("swatch.new", &json!({"name": "Ink", "color": {"model": "cmyk", "c": 0, "m": 1, "y": 1, "k": 0}, "spot": true})).unwrap(); + s.execute("swatch.edit", &json!({"name": "Ink", "newName": "Red Ink"})).unwrap(); + let table = s.doc().unwrap().doc.unknown.get("packagingRoles").cloned().unwrap(); + assert_eq!(table, json!({"cut": "Cut", "crease": "Score", "partialCut": "Partial Cut", "perforation": "Perforation"})); + // Undo puts the old name back in the table with the swatch. + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("edit.undo", &json!({})).unwrap(); + assert!(s.doc().unwrap().doc.unknown.get("packagingRoles").is_none()); + assert_eq!(counts(&mut s, json!({"ids": [a.0]})), [0, 1, 0, 0]); +} + +#[test] +fn a_role_swatch_renamed_to_another_role_s_name_leaves_each_role_its_own_swatch() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + // No Crease swatch exists yet, so the name is free for the Cut swatch. + s.execute("swatch.edit", &json!({"name": "Cut", "newName": "Crease"})).unwrap(); + let table = s.doc().unwrap().doc.unknown.get("packagingRoles").cloned().unwrap(); + assert_eq!((&table["cut"], &table["crease"]), (&json!("Crease"), &json!("Crease 2")), "{table}"); + assert_eq!(counts(&mut s, json!({"ids": [a.0]})), [1, 0, 0, 0], "the line is still a cut line"); +} + +#[test] +fn a_role_swatch_that_stops_being_global_hands_its_role_to_a_new_swatch() { + let mut s = session("cmyk"); + let (a, b) = (rect(&mut s, 20.0), rect(&mut s, 100.0)); + set_role(&mut s, json!({"role": "perforation", "ids": [a.0]})); + s.execute("swatch.edit", &json!({"name": "Perforation", "spot": false, "global": false})).unwrap(); + assert_eq!(s.doc().unwrap().doc.unknown.get("packagingRoles").unwrap()["perforation"], json!("Perforation 2")); + assert_eq!(counts(&mut s, json!({"ids": [a.0]})), [0, 0, 0, 0], "the unlinked stroke has no role"); + set_role(&mut s, json!({"role": "perforation", "ids": [b.0]})); + assert!(s.doc().unwrap().doc.swatch("Perforation 2").is_some_and(|w| w.spot)); + assert_eq!(counts(&mut s, json!({"ids": [b.0]})), [0, 0, 0, 1]); +} + +#[test] +fn a_color_group_named_like_a_role_swatch_keeps_its_name() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + s.execute("swatch.newGroup", &json!({"name": "Cut", "colors": ["#ff0000"]})).unwrap(); + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + let d = &s.doc().unwrap().doc; + assert_eq!(d.unknown.get("packagingRoles").unwrap()["cut"], json!("Cut 2")); + assert!(d.swatch("Cut 2").is_some_and(|w| w.spot) && d.swatch("Cut").is_none()); + assert_eq!(counts(&mut s, json!({"ids": [a.0]})), [1, 0, 0, 0]); +} + +#[test] +fn set_role_makes_a_deleted_role_swatch_again_for_strokes_that_still_name_it() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + s.execute("swatch.delete", &json!({"name": "Cut", "unlink": false})).unwrap(); + assert!(s.doc().unwrap().doc.swatch("Cut").is_none()); + let before = steps(&s); + let r = set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + assert_eq!(r["changed"].as_u64(), Some(0), "the stroke already names the swatch"); + assert!(s.doc().unwrap().doc.swatch("Cut").is_some_and(|w| w.spot), "the swatch is back"); + assert_eq!(steps(&s), before + 1, "as one undo step"); +} + +#[test] +fn a_hidden_stroke_is_shown_when_it_takes_a_role() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + s.edit("hide", |d, _| { + if let Some(AppearanceItem::Stroke(st)) = d.node_mut(a).unwrap().appearance.items.iter_mut().find(|i| !i.is_fill()) { + st.visible = false; + } + Ok(()) + }) + .unwrap(); + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + assert!(node(&s, a).appearance.stroke().unwrap().visible); +} + +#[test] +fn role_swatches_refuses_swatches_that_cannot_carry_a_role() { + let mut s = session("cmyk"); + let ramp = json!({"type": "linear", "stops": [{"offset": 0, "color": "#000000"}, {"offset": 1, "color": "#ffffff"}]}); + s.execute("swatch.new", &json!({"name": "Ramp", "gradient": ramp})).unwrap(); + s.execute("swatch.new", &json!({"name": "Plain", "color": "#336699"})).unwrap(); + s.execute("swatch.new", &json!({"name": "Ink", "color": {"model": "cmyk", "c": 0, "m": 1, "y": 1, "k": 0}, "spot": true})).unwrap(); + s.execute("swatch.new", &json!({"name": "Ink Tint", "swatch": "Ink", "tint": 0.5})).unwrap(); + s.execute("swatch.new", &json!({"name": "Crease", "color": {"model": "cmyk", "c": 1, "m": 0, "y": 0, "k": 0}, "spot": true})).unwrap(); + let before = steps(&s); + for (swatch, says) in [ + ("Nope", "no swatch"), + ("Ramp", "isn't a color swatch"), + ("Plain", "process color"), + ("Ink Tint", "tint swatch"), + ("[None]", "can't carry a role"), + ("[Registration]", "can't carry a role"), + ("Crease", "already carries the Crease role"), + ] { + let e = err(&mut s, "addon.packaging.setRoleSwatch", json!({"role": "cut", "swatch": swatch})); + assert!(e.contains(says), "{swatch}: {e}"); + } + assert!(err(&mut s, "addon.packaging.setRoleSwatch", json!({"role": "none", "swatch": "Ink"})).contains("unknown role")); + assert!(err(&mut s, "addon.packaging.setRoleSwatch", json!({"role": "cut"})).contains("swatch")); + assert_eq!(steps(&s), before, "no refusal records an undo step"); +} + +#[test] +fn a_damaged_role_table_reads_as_the_defaults() { + let mut s = session("cmyk"); + s.edit("damage", |d, _| { + d.unknown.insert("packagingRoles".into(), json!({"cut": 5, "crease": "", "partialCut": null, "extra": "x"})); + Ok(()) + }) + .unwrap(); + let r = s.execute("addon.packaging.roles", &json!({})).unwrap(); + let swatches: Vec<&str> = r["roles"].as_array().unwrap().iter().map(|r| r["swatch"].as_str().unwrap()).collect(); + assert_eq!(swatches, ["Cut", "Crease", "Partial Cut", "Perforation"]); + s.edit("not an object", |d, _| { + d.unknown.insert("packagingRoles".into(), json!("Cut")); + Ok(()) + }) + .unwrap(); + let a = rect(&mut s, 20.0); + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + assert_eq!(counts(&mut s, json!({"ids": [a.0]})), [1, 0, 0, 0]); + // One swatch named for two roles: the later role takes its own default. + s.edit("duplicate", |d, _| { + d.unknown.insert("packagingRoles".into(), json!({"cut": "Crease", "crease": "Crease", "partialCut": "Cut"})); + Ok(()) + }) + .unwrap(); + let r = s.execute("addon.packaging.roles", &json!({})).unwrap(); + let swatches: Vec<&str> = r["roles"].as_array().unwrap().iter().map(|r| r["swatch"].as_str().unwrap()).collect(); + assert_eq!(swatches, ["Crease", "Crease 2", "Cut", "Perforation"]); + let b = rect(&mut s, 100.0); + set_role(&mut s, json!({"role": "crease", "ids": [b.0]})); + assert_eq!(counts(&mut s, json!({"ids": [b.0]})), [0, 1, 0, 0], "the crease line is a crease line"); +} + +#[test] +fn native_save_and_open_keep_the_role_table_and_the_roles() { + let mut s = session("cmyk"); + let a = rect(&mut s, 20.0); + s.execute("swatch.new", &json!({"name": "Die", "color": {"model": "cmyk", "c": 0, "m": 0.8, "y": 0.2, "k": 0}, "spot": true})).unwrap(); + s.execute("addon.packaging.setRoleSwatch", &json!({"role": "cut", "swatch": "Die"})).unwrap(); + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + let saved = s.execute("document.export", &json!({"format": "vectorcraft"})).unwrap(); + s.execute("document.open", &json!({"name": "box.vectorcraft", "dataBase64": saved["dataBase64"]})).unwrap(); + assert_eq!(s.doc().unwrap().doc.unknown.get("packagingRoles").unwrap()["cut"], json!("Die")); + s.execute("select.none", &json!({})).unwrap(); + assert_eq!(counts(&mut s, json!({})), [1, 0, 0, 0]); +} + +/// A document with one rectangle of each of Cut and Crease. +fn two_roles() -> Session { + let mut s = session("cmyk"); + let (a, b) = (rect(&mut s, 20.0), rect(&mut s, 100.0)); + set_role(&mut s, json!({"role": "cut", "ids": [a.0]})); + set_role(&mut s, json!({"role": "crease", "ids": [b.0]})); + s +} + +fn export(s: &mut Session, format: &str) -> String { + String::from_utf8_lossy(&b64(&s.execute("document.export", &json!({"format": format})).unwrap())).into_owned() +} + +#[test] +fn pdf_prints_each_role_on_its_swatch_s_separation_and_overprints_it() { + let mut s = two_roles(); + let c = rect(&mut s, 180.0); + set_role(&mut s, json!({"role": "partialCut", "ids": [c.0]})); + let bytes = vectorcraft_pdf::export( + &s.doc().unwrap().doc, + &vectorcraft_pdf::PdfOptions { created: Some(0), ..vectorcraft_pdf::PdfOptions::uncompressed() }, + ) + .unwrap(); + let pdf = String::from_utf8_lossy(&bytes); + assert!(pdf.contains("/Separation/Cut"), "the Cut line prints on the Cut plate"); + assert!(pdf.contains("/Separation/Crease")); + assert!(pdf.contains("/Separation/Partial#20Cut"), "a name with a space"); + assert!(pdf.contains("/OP true"), "the role strokes overprint"); + // Without overprint, nothing in the file overprints. + let mut plain = session("cmyk"); + let (a, b) = (rect(&mut plain, 20.0), rect(&mut plain, 100.0)); + set_role(&mut plain, json!({"role": "cut", "ids": [a.0], "overprint": false})); + set_role(&mut plain, json!({"role": "crease", "ids": [b.0], "overprint": false})); + let bytes = vectorcraft_pdf::export( + &plain.doc().unwrap().doc, + &vectorcraft_pdf::PdfOptions { created: Some(0), ..vectorcraft_pdf::PdfOptions::uncompressed() }, + ) + .unwrap(); + assert!(!String::from_utf8_lossy(&bytes).contains("/OP true")); + assert!(!export(&mut s, "pdf").is_empty()); +} + +#[test] +fn svg_dxf_and_hpgl_keep_the_default_role_swatches_apart() { + let mut s = two_roles(); + let svg = export(&mut s, "svg"); + let strokes: BTreeSet<&str> = svg.split("stroke=\"").skip(1).filter_map(|t| t.split('"').next()).filter(|c| c.starts_with('#')).collect(); + let strokes = if strokes.is_empty() { + svg.split("stroke:").skip(1).filter_map(|t| t.split([';', '"', '}']).next()).filter(|c| c.starts_with('#')).collect() + } else { + strokes + }; + assert_eq!(strokes.len(), 2, "two stroke colors: {svg}"); + let dxf = export(&mut s, "dxf"); + let lines: Vec<&str> = dxf.lines().map(str::trim).collect(); + // The rectangles' white fills come out as true colors too. + let colors: BTreeSet<&str> = lines.windows(2).filter(|w| w[0] == "420" && w[1] != "16777215").map(|w| w[1]).collect(); + assert_eq!(colors.len(), 2, "two stroke true colors: {colors:?}"); + let plot = s.execute("document.export", &json!({"format": "hpgl", "hpglVersion": "hpgl2"})).unwrap(); + let plot = String::from_utf8_lossy(&b64(&plot)).into_owned(); + assert!(plot.contains("SP1") && plot.contains("SP2"), "a pen per role swatch color: {plot}"); +} + +#[test] +fn compound_paths_carry_a_role() { + let mut s = session("cmyk"); + let (a, b) = (rect(&mut s, 20.0), rect(&mut s, 30.0)); + s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap(); + let c = id_of(&s.execute("object.compoundPath.make", &json!({})).unwrap()); + assert!(matches!(node(&s, c).kind, NodeKind::Compound { .. })); + let r = set_role(&mut s, json!({"role": "cut", "ids": [c.0]})); + assert_eq!(r["ids"], json!([c.0])); + assert_eq!(counts(&mut s, json!({"ids": [c.0]})), [1, 0, 0, 0]); + // A member named by id gives its compound the role. + let r = set_role(&mut s, json!({"role": "crease", "ids": [a.0, b.0]})); + assert_eq!((r["ids"].clone(), r["changed"].as_u64()), (json!([c.0]), Some(1))); + assert_eq!(counts(&mut s, json!({"ids": [c.0]})), [0, 1, 0, 0]); +} diff --git a/crates/engine/src/cmd/plugin_call/reviewed.rs b/crates/engine/src/cmd/plugin_call/reviewed.rs index 67d0fec2a3..f445237d83 100644 --- a/crates/engine/src/cmd/plugin_call/reviewed.rs +++ b/crates/engine/src/cmd/plugin_call/reviewed.rs @@ -40,6 +40,9 @@ pub(super) const ALLOWED: &[&str] = &[ "addon.image.colorMask", "addon.image.removeBackground", "addon.image.removeColorMask", + "addon.packaging.roles", + "addon.packaging.setRole", + "addon.packaging.setRoleSwatch", "addon.pathtools.addPoints", "addon.pathtools.close", "addon.pathtools.corner", diff --git a/crates/engine/src/cmd/swatch.rs b/crates/engine/src/cmd/swatch.rs index 8868143ef3..a42cc134ed 100644 --- a/crates/engine/src/cmd/swatch.rs +++ b/crates/engine/src/cmd/swatch.rs @@ -48,7 +48,7 @@ pub fn specs() -> Vec { "Swatch Options", ["Window", "Swatches"], None, - "{name, newName?, color? (e.g. {l, a, b} for a Lab colour; #rrggbbaa and #rgba are refused), keepModel?: bool (as paint.setFill; the swatch's own color and a color given with mode keep their model), mode?: \"gray\"|\"rgb\"|\"hsb\"|\"lab\"|\"cmyk\"|\"web\" (convert the colour), global?, spot? (spot colours are always global), paint?: paint.setFill params ({color}, {gradient} or {swatch}) or {pattern: name} replacing the swatch's colour; gradient or pattern with one of the same kind (stored as swatch.new stores it: a gradient keeps its aspect with or without start/end and holds no swatch link; a pattern is untransformed)} edit a swatch in any colour group, as one undo step. Fills, strokes, text, gradient stops and tint swatches linked to a global swatch take its new colour (at their own tint) and name; turning Global off unlinks them (they keep their colour). Colour, mode and spot apply to solid colours only; a tint swatch only takes a new name (edit its base) → {name, relinked: paints changed}", + "{name, newName?, color? (e.g. {l, a, b} for a Lab colour; #rrggbbaa and #rgba are refused), keepModel?: bool (as paint.setFill; the swatch's own color and a color given with mode keep their model), mode?: \"gray\"|\"rgb\"|\"hsb\"|\"lab\"|\"cmyk\"|\"web\" (convert the colour), global?, spot? (spot colours are always global), paint?: paint.setFill params ({color}, {gradient} or {swatch}) or {pattern: name} replacing the swatch's colour; gradient or pattern with one of the same kind (stored as swatch.new stores it: a gradient keeps its aspect with or without start/end and holds no swatch link; a pattern is untransformed)} edit a swatch in any colour group, as one undo step. Fills, strokes, text, gradient stops and tint swatches linked to a global swatch take its new colour (at their own tint) and name; turning Global off unlinks them (they keep their colour). A fold line role the swatch carries (addon.packaging.roles) stays with it under its new name, and moves to a new swatch name when Global is turned off. Colour, mode and spot apply to solid colours only; a tint swatch only takes a new name (edit its base) → {name, relinked: paints changed}", has_doc, swatch_edit ), @@ -93,7 +93,7 @@ pub fn specs() -> Vec { "Merge Swatches", ["Window", "Swatches"], None, - "{names: [two or more solid-colour swatches], name?: the merged swatch's name (one swatch kept)} keep the first (its name and colour) and delete the others; paints linked to them take its colour and link to it (or unlink when it isn't global), and gradient stops linked to them drop their process plates, as one undo step → {name, merged: [deleted names], relinked}", + "{names: [two or more solid-colour swatches], name?: the merged swatch's name (one swatch kept)} keep the first (its name and colour) and delete the others; paints linked to them take its colour and link to it (or unlink when it isn't global), and gradient stops linked to them drop their process plates, as one undo step. When the kept swatch is global and carries no fold line role (addon.packaging.roles), it takes the role of the first merged swatch that carried one → {name, merged: [deleted names], relinked}", has_doc, swatch_merge ), @@ -937,6 +937,8 @@ fn swatch_edit(s: &mut Session, p: &Value) -> Result { } } let relinked = map_swatch_links(d, &mut |c, l, t| relink.apply(c, l, t)); + // A fold line role carried by the swatch follows its new name, as the strokes do. + super::addons::role_swatch_edited(d, &name, &to, global); super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad(C, e))?; Ok(relinked) })?; @@ -1139,6 +1141,8 @@ fn swatch_merge(s: &mut Session, p: &Value) -> Result { take_swatches(d, merged, |_| true); map_all_paints(d, &mut unplate); let relinked = map_swatch_links(d, &mut relink); + // The lines of a merged role swatch link to the kept swatch, which takes that role. + super::addons::role_swatches_merged(d, merged, keep, link.is_some()); super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad(C, e))?; Ok(relinked) })?; diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index fce3742d11..30d0dc96cf 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -993,6 +993,13 @@ fn structured_junk() { ("addon.document.symbolsFromLayers", json!({"layers": "x", "replace": 3})), ("addon.document.symbolsFromLayers", json!({"layers": [1e308, -1, null, u64::MAX], "replace": true})), ("addon.document.symbolsFromLayers", json!({"layers": [], "replace": true})), + // Packaging Tools add-on. + ("addon.packaging.setRole", json!({"role": "cut", "ids": [u64::MAX], "overprint": "no"})), + ("addon.packaging.setRole", json!({"role": ["crease"], "ids": "all"})), + ("addon.packaging.setRole", json!({"role": "none", "overprint": null})), + ("addon.packaging.roles", json!({"ids": [-1, 1e300]})), + ("addon.packaging.setRoleSwatch", json!({"role": "perforation", "swatch": "[Registration]"})), + ("addon.packaging.setRoleSwatch", json!({"role": "cut", "swatch": 5})), // Smart Pen add-on. ("addon.pen.append", json!({"id": 2, "x": 1e308, "y": 0, "in": [1e308, -1e308]})), ("addon.pen.append", json!({"id": 2, "subpath": u64::MAX, "x": 0, "y": 0, "segment": "curved"})), diff --git a/crates/ui-egui/src/addons.rs b/crates/ui-egui/src/addons.rs index 522f82bd2b..b63c32af59 100644 --- a/crates/ui-egui/src/addons.rs +++ b/crates/ui-egui/src/addons.rs @@ -9,6 +9,7 @@ pub mod cutting; pub mod document; pub mod enclosing_lasso; pub mod image; +pub mod packaging; pub mod pathtools; pub mod pen; pub mod select; @@ -30,6 +31,7 @@ pub fn plugin_menu() -> Vec { Item::Sub("Document Tools", document::menu()), Item::Sub("Enclosing Lasso", enclosing_lasso::menu()), Item::Sub("Image Tools", image::menu()), + Item::Sub("Packaging Tools", packaging::menu()), Item::Sub("Path Precision Tools", pathtools::menu()), Item::Sub("Selection Tools", select::menu()), Item::Sub("Smart Pen", pen::menu()), diff --git a/crates/ui-egui/src/addons/packaging.rs b/crates/ui-egui/src/addons/packaging.rs new file mode 100644 index 0000000000..1c70bef5d0 --- /dev/null +++ b/crates/ui-egui/src/addons/packaging.rs @@ -0,0 +1,134 @@ +//! Packaging Tools' submenu (Object › Plug-ins › Packaging Tools) and its Fold Lines panel. The +//! panel lists the fold line roles, each with its swatch's chip and how many of the selected objects +//! have it (of the whole document while nothing is selected); clicking a role gives the selected +//! paths that role (`addon.packaging.setRole`). Its menu's Role Swatches… picks the swatch that +//! carries a role (`addon.packaging.setRoleSwatch`). + +use egui::{Align, Layout, Sense, Ui, vec2}; +use serde_json::{Value, json}; + +use crate::VectorcraftApp; +use crate::menus::Item; +use crate::panels::{empty_state, pstate, set_pstate}; +use crate::widgets::{self, dim_label}; + +/// The panel's id (`window.panel {panel: "foldLines"}`). +pub const ID: &str = "foldLines"; +/// The command that gives objects a role. +pub const SET_ROLE: &str = "addon.packaging.setRole"; +/// The command that picks a role's swatch. +pub const SET_ROLE_SWATCH: &str = "addon.packaging.setRoleSwatch"; +/// The roles by name, as `addon.packaging.setRole` and Role Swatches… take them. +pub const ROLES: &[(&str, &str)] = + &[("Cut Line", "cut"), ("Crease Line", "crease"), ("Partial Cut Line", "partialCut"), ("Perforation Line", "perforation")]; + +/// Where the panel keeps the last `addon.packaging.roles` answer (panel state). +const STATE: &str = "fold-lines"; + +/// Object › Plug-ins › Packaging Tools. +pub fn menu() -> Vec { + let make = |label, role: &str| Item::Cmd(label, SET_ROLE, json!({ "role": role })); + vec![ + Item::Cmd("Fold Lines", "window.panel", json!({ "panel": ID })), + Item::Sep, + make("Make Cut Line", "cut"), + make("Make Crease Line", "crease"), + make("Make Partial Cut Line", "partialCut"), + make("Make Perforation Line", "perforation"), + ] +} + +/// One role row: (role id, swatch name, the swatch's paint if the document has it, count). +type Row = (String, String, Option, u64); + +/// The panel's rows for one document state. +#[derive(Clone, Default)] +struct Rows { + /// What they were read for: the document, its revision and its selection. + key: Option<(u64, u64, Vec)>, + rows: Vec, +} + +/// The rows for the active document as it is now, read again only when it changed. +fn rows(app: &mut VectorcraftApp, ctx: &egui::Context) -> Vec { + let Some(st) = app.session.active() else { return vec![] }; + let key = (st.uid, st.revision, st.selection.objects.iter().map(|id| id.0).collect::>()); + let mut kept: Rows = pstate(ctx, STATE); + if kept.key.as_ref() == Some(&key) { + return kept.rows; + } + let answer = app.session.execute("addon.packaging.roles", &json!({})).unwrap_or(Value::Null); + let d = app.session.active().map(|st| st.doc.clone()); + let row = |r: &Value| { + let swatch = r["swatch"].as_str()?.to_string(); + let paint = d.as_ref().and_then(|d| d.swatch(&swatch)).map(|s| s.paint.clone()); + Some((r["role"].as_str()?.to_string(), swatch, paint, r["count"].as_u64()?)) + }; + kept = Rows { key: Some(key), rows: answer["roles"].as_array().into_iter().flatten().filter_map(row).collect() }; + set_pstate(ctx, STATE, kept.clone()); + kept.rows +} + +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { + let Some(st) = app.session.active() else { + empty_state(ui, "scissors", tl!("No document"), tl!("Open a document to mark its fold lines.")); + return; + }; + let selected = !st.selection.is_empty(); + if !selected { + dim_label(ui, tl!("No objects are selected.")); + ui.add_space(4.0); + } + let mut picked = None; + for (role, swatch, paint, count) in rows(app, ui.ctx()) { + let label = ROLES.iter().find(|(_, id)| *id == role).map_or(role.as_str(), |(l, _)| *l); + let clicked = ui + .add_enabled_ui(selected, |ui| { + ui.horizontal(|ui| { + let (chip, _) = ui.allocate_exact_size(vec2(14.0, 14.0), Sense::hover()); + widgets::paint_chip(ui, chip, paint.as_ref().unwrap_or(&vectorcraft_color::Paint::None)); + let resp = ui.selectable_label(false, tl!(label)).on_hover_text(swatch.as_str()); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| dim_label(ui, &count.to_string())); + resp.clicked() + }) + .inner + }) + .inner; + if clicked { + picked = Some(role); + } + } + if let Some(role) = picked + && let Err(e) = app.run(SET_ROLE, json!({ "role": role })) + { + app.status(e); + } +} + +/// Role Swatches… (the dialog of `addon.packaging.setRoleSwatch`): when another role is picked, its +/// Swatch field shows that role's current swatch. `__role` keeps the role the field was filled for. +pub(crate) fn follow_role(app: &mut VectorcraftApp, d: &mut crate::state::Dialog) { + let role = d.str("role"); + if d.str("__role") == role { + return; + } + let answer = app.session.execute("addon.packaging.roles", &json!({})).unwrap_or(Value::Null); + let current = answer["roles"].as_array().into_iter().flatten().find(|r| r["role"] == role.as_str()).and_then(|r| r["swatch"].as_str()); + if let Some(swatch) = current { + d.fields.insert("swatch".into(), json!(swatch)); + } + d.fields.insert("__role".into(), json!(role)); +} + +/// The panel's ≡ menu: Role Swatches… opens the options of `addon.packaging.setRoleSwatch` on the +/// Cut role and its current swatch. +pub fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui) { + if widgets::menu_item(ui, tl!("Role Swatches…"), app.session.active().is_some(), false) { + let swatch = rows(app, ui.ctx()).into_iter().find(|r| r.0 == "cut").map_or_else(|| "Cut".to_string(), |r| r.1); + let (id, p) = crate::menus::click_target("Role Swatches…", SET_ROLE_SWATCH, &json!({ "role": "cut", "swatch": swatch })); + crate::menus::invoke(app, &id, p); + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/ui-egui/src/addons/packaging/tests.rs b/crates/ui-egui/src/addons/packaging/tests.rs new file mode 100644 index 0000000000..cfe9909dc8 --- /dev/null +++ b/crates/ui-egui/src/addons/packaging/tests.rs @@ -0,0 +1,133 @@ +use serde_json::json; +use vectorcraft_engine::Session; + +use super::{ID, ROLES, SET_ROLE, SET_ROLE_SWATCH, show}; +use crate::tests_labels::painted_text; +use crate::{VectorcraftApp, menus}; + +/// An app with two rectangles, nothing selected → (app, their ids). +fn app() -> (VectorcraftApp, u64, u64) { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + let rect = |app: &mut VectorcraftApp, x: f64| { + app.run("shape.rectangle", json!({"x": x, "y": 20, "width": 40, "height": 40})).unwrap()["id"].as_u64().unwrap() + }; + let (a, b) = (rect(&mut app, 20.0), rect(&mut app, 100.0)); + app.run("select.none", json!({})).unwrap(); + (app, a, b) +} + +#[test] +fn the_plugins_submenu_opens_the_panel_and_makes_each_kind_of_line() { + let (mut app, a, _) = app(); + let entries = menus::menu_entries(&app); + let under = |label: &str| entries.iter().find(|e| e.path == ["Object", "Plug-ins", "Packaging Tools"] && e.label == label); + let item = under("Fold Lines").expect("listed"); + assert_eq!((item.command.as_deref(), &item.params), (Some("window.panel"), &json!({ "panel": ID }))); + for (label, role) in + [("Make Cut Line", "cut"), ("Make Crease Line", "crease"), ("Make Partial Cut Line", "partialCut"), ("Make Perforation Line", "perforation")] + { + let e = under(label).unwrap_or_else(|| panic!("{label} listed")); + assert_eq!((e.command.as_deref(), &e.params), (Some(SET_ROLE), &json!({ "role": role })), "{label}"); + } + assert!(!crate::panel_docking::showing(&app.ui, ID)); + menus::invoke(&mut app, "window.panel", item.params.clone()); + assert!(crate::panel_docking::showing(&app.ui, ID), "the panel opens"); + app.run("select.set", json!({"ids": [a]})).unwrap(); + let make = under("Make Partial Cut Line").unwrap(); + let (target, p) = menus::click_target(&make.label, SET_ROLE, &make.params); + menus::invoke(&mut app, &target, p); + let r = app.session.execute("addon.packaging.roles", &json!({"ids": [a]})).unwrap(); + assert_eq!(r["roles"][2]["count"], json!(1), "{r}"); +} + +#[test] +fn the_panel_counts_the_roles_and_says_when_nothing_is_selected() { + let (mut app, a, b) = app(); + app.run(SET_ROLE, json!({"role": "cut", "ids": [a, b]})).unwrap(); + app.run("select.none", json!({})).unwrap(); + let text = painted_text(&mut app, show); + assert!(text.contains("No objects are selected."), "{text}"); + for (label, _) in ROLES { + assert!(text.contains(label), "{label}: {text}"); + } + assert!(text.contains('2'), "the document's two cut lines: {text}"); + app.run("select.set", json!({"ids": [a]})).unwrap(); + let text = painted_text(&mut app, show); + assert!(!text.contains("No objects are selected."), "{text}"); + assert!(text.contains('1') && !text.contains('2'), "the selection's one cut line: {text}"); +} + +/// A click on the panel row that screen readers name `label`. egui finds what the pointer is over +/// from the frame before, so the move gets a frame of its own. +fn click_row(app: &mut VectorcraftApp, label: &str) { + crate::i18n::set_current(crate::i18n::Lang::EN); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + let mut time = 0.0; + let mut frame = |app: &mut VectorcraftApp, events: Vec| { + time += 0.1; + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(300.0, 200.0)); + let input = egui::RawInput { events, time: Some(time), screen_rect: Some(screen), ..Default::default() }; + let mut out = ctx.run_ui(input, |ui| show(app, ui)); + out.textures_delta.clear(); + out + }; + frame(app, vec![]); + let out = frame(app, vec![]); + let nodes = &out.platform_output.accesskit_update.as_ref().expect("an accessibility tree").nodes; + let node = nodes.iter().find(|(_, n)| n.label() == Some(label)); + let b = node.unwrap_or_else(|| panic!("no row named {label:?}")).1.bounds().expect("bounds"); + let at = egui::pos2((b.x0 + b.x1) as f32 * 0.5, (b.y0 + b.y1) as f32 * 0.5); + let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }; + frame(app, vec![egui::Event::PointerMoved(at)]); + frame(app, vec![button(true)]); + frame(app, vec![button(false)]); +} + +#[test] +fn clicking_a_row_gives_the_selection_that_role_and_does_nothing_without_one() { + let (mut app, a, b) = app(); + click_row(&mut app, "Crease Line"); + let none = app.session.execute("addon.packaging.roles", &json!({"ids": [a, b]})).unwrap(); + assert_eq!(none["roles"][1]["count"], json!(0), "nothing selected: {none}"); + app.run("select.set", json!({"ids": [a]})).unwrap(); + click_row(&mut app, "Crease Line"); + let r = app.session.execute("addon.packaging.roles", &json!({"ids": [a, b]})).unwrap(); + assert_eq!(r["roles"][1]["count"], json!(1), "{r}"); +} + +#[test] +fn role_swatches_opens_the_options_of_its_command() { + let (target, p) = menus::click_target("Role Swatches…", SET_ROLE_SWATCH, &json!({"role": "cut", "swatch": "Cut"})); + assert_eq!((target.as_str(), &p["command"]), ("ui.paramDialog", &json!(SET_ROLE_SWATCH))); + let doc = vectorcraft_engine::find_command(SET_ROLE_SWATCH).unwrap().params; + let ids: Vec<&str> = ROLES.iter().map(|(_, id)| *id).collect(); + assert!(doc.contains(&format!("role: {}", ids.join("|"))), "{doc}"); +} + +#[test] +fn role_swatches_shows_the_swatch_of_the_role_picked() { + let (mut app, a, _) = app(); + app.run(SET_ROLE, json!({"role": "crease", "ids": [a]})).unwrap(); + app.run("swatch.edit", json!({"name": "Crease", "newName": "Score"})).unwrap(); + let mut d = crate::state::Dialog::new("command", json!({"role": "cut", "swatch": "Cut"})); + super::follow_role(&mut app, &mut d); + assert_eq!(d.str("swatch"), "Cut"); + d.fields.insert("role".into(), json!("crease")); + super::follow_role(&mut app, &mut d); + assert_eq!(d.str("swatch"), "Score", "the Crease role's swatch"); + // A swatch typed for the same role stays. + d.fields.insert("swatch".into(), json!("Ink")); + super::follow_role(&mut app, &mut d); + assert_eq!(d.str("swatch"), "Ink"); + assert_eq!(crate::dialogs::form::params(&d), json!({"role": "crease", "swatch": "Ink"}), "the dialog sends no __role"); +} + +#[test] +fn the_panel_without_a_document_asks_for_one() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let text = painted_text(&mut app, show); + assert!(text.contains("No document"), "{text}"); +} diff --git a/crates/ui-egui/src/dialogs/command.rs b/crates/ui-egui/src/dialogs/command.rs index 64e3f13e64..7afe37ec0b 100644 --- a/crates/ui-egui/src/dialogs/command.rs +++ b/crates/ui-egui/src/dialogs/command.rs @@ -22,6 +22,9 @@ pub(super) const SPEC: DialogSpec = DialogSpec { let cmyk = app.session.active().is_some_and(|s| s.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk); let choices = |k: &str| rasterize_color_models(&command, k, cmyk).or_else(|| choices(&command, k)); form::param_fields(ui, d, &|k| lengths.contains(&k), &choices, &rank, app.session.general_unit()); + if command == crate::addons::packaging::SET_ROLE_SWATCH { + crate::addons::packaging::follow_role(app, d); + } if linked && d.fields.get("rowGutter") == d.fields.get("__rowGutterLinked") && let Some(g) = d.fields.get("gutter").cloned() @@ -159,6 +162,7 @@ fn order(command: &str) -> &'static [&'static str] { &["background", "colors", "contiguous", "match", "tolerance", "softness"] } crate::addons::cutting::BRIDGES => &["width", "count", "direction", "keepOriginal"], + crate::addons::packaging::SET_ROLE_SWATCH => &["role", "swatch"], _ => &[], } } @@ -168,6 +172,7 @@ fn choices(command: &str, key: &str) -> Option { match (command, key) { ("type.pathOptions", "effect") => Some(crate::menus::PATH_EFFECTS), ("type.pathOptions", "alignToPath") => Some(PATH_ALIGN), + (crate::addons::packaging::SET_ROLE_SWATCH, "role") => Some(crate::addons::packaging::ROLES), (crate::addons::typetools::RELEASE, "mode") => Some(crate::addons::typetools::RELEASE_MODES), (crate::addons::cutting::BRIDGES, "direction") => Some(crate::addons::cutting::BRIDGE_DIRECTIONS), ("text.areaOptions", "fit") => Some(AREA_FIT), diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index d89bcc9566..a6a69d9100 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -7000,4 +7000,21 @@ breakText Glyphs Глифи Nest Shapes… Подреждане на форми… Nest Shapes Подреждане на форми Roll Ролка + Packaging Tools Инструменти за опаковки + Fold Lines Линии на сгъване + Make Cut Line Направи линия за рязане + Make Crease Line Направи линия за биговане + Make Partial Cut Line Направи линия за частично рязане + Make Perforation Line Направи линия за перфорация + Make Fold Line Направи линия на сгъване + Role Swatches… Образци за видовете линии… + Role Swatches Образци за видовете линии + Role Вид линия + Cut Line Линия за рязане + Crease Line Линия за биговане + Partial Cut Line Линия за частично рязане + Perforation Line Линия за перфорация + No objects are selected. Няма избрани обекти. + Open a document to mark its fold lines. Отворете документ, за да отбележите линиите му на сгъване. + Remove Fold Line Role Премахни вида на линията @msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer трябва да е "singleLine", "everyLine", "standardSingleLine" или "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 25fdf12318..5da48b6cff 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -7053,4 +7053,21 @@ shape Open Otvoren Nest Shapes… Slaganje oblika… Nest Shapes Slaganje oblika Roll Rolna + Packaging Tools Alati za ambalažu + Fold Lines Linije savijanja + Make Cut Line Napravi liniju rezanja + Make Crease Line Napravi liniju bigovanja + Make Partial Cut Line Napravi liniju djelimičnog rezanja + Make Perforation Line Napravi liniju perforacije + Make Fold Line Napravi liniju savijanja + Role Swatches… Uzorci boja za vrste linija… + Role Swatches Uzorci boja za vrste linija + Role Vrsta linije + Cut Line Linija rezanja + Crease Line Linija bigovanja + Partial Cut Line Linija djelimičnog rezanja + Perforation Line Linija perforacije + No objects are selected. Nijedan objekat nije odabran. + Open a document to mark its fold lines. Otvorite dokument da označite njegove linije savijanja. + Remove Fold Line Role Ukloni vrstu linije @msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer mora biti "singleLine", "everyLine", "standardSingleLine" ili "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 541e425552..e861e61fec 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -7775,4 +7775,21 @@ breakText Glyphs Glifs @msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} píxels és massa gran per a JPEG (com a màxim 65535 píxels per costat): reduïu la resolució @msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} píxels és massa gran per a Targa (com a màxim 65535 píxels per costat): reduïu la resolució @msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} píxels és massa gran per a PSD (com a màxim {MAX_SIDE} píxels per costat): reduïu la resolució + Packaging Tools Eines d’embalatge + Fold Lines Línies de plegat + Make Cut Line Converteix en línia de tall + Make Crease Line Converteix en línia d’hendit + Make Partial Cut Line Converteix en línia de tall parcial + Make Perforation Line Converteix en línia de perforació + Make Fold Line Converteix en línia de plegat + Role Swatches… Mostres dels tipus de línia… + Role Swatches Mostres dels tipus de línia + Role Tipus de línia + Cut Line Línia de tall + Crease Line Línia d’hendit + Partial Cut Line Línia de tall parcial + Perforation Line Línia de perforació + No objects are selected. No hi ha cap objecte seleccionat. + Open a document to mark its fold lines. Obre un document per marcar-ne les línies de plegat. + Remove Fold Line Role Elimina el tipus de línia @msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer ha de ser "singleLine", "everyLine", "standardSingleLine" o "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 22099f8da4..1a4d359b93 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -9596,3 +9596,20 @@ breakText Glyphs Glyfy @msg turn on at least one side zapněte alespoň jednu stranu @msg with no hem and no grommets there is nothing to mark bez lemu a bez průchodek není co označit @msg {saving} failed: {e} {saving} se nezdařilo: {e} + Packaging Tools Nástroje pro obaly + Fold Lines Linie skladu + Make Cut Line Vytvořit linii řezu + Make Crease Line Vytvořit linii bigu + Make Partial Cut Line Vytvořit linii částečného řezu + Make Perforation Line Vytvořit linii perforace + Make Fold Line Vytvořit linii skladu + Role Swatches… Vzorník typů linií… + Role Swatches Vzorník typů linií + Role Typ linie + Cut Line Linie řezu + Crease Line Linie bigu + Partial Cut Line Linie částečného řezu + Perforation Line Linie perforace + No objects are selected. Nejsou vybrány žádné objekty. + Open a document to mark its fold lines. Otevřete dokument, abyste označili jeho linie skladu. + Remove Fold Line Role Odstranit typ linie diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index d767da9808..723766f020 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -7685,4 +7685,21 @@ breakText Glyphs Glyffer Nest Shapes… Pak figurer tæt… Nest Shapes Pak figurer tæt Roll Rulle + Packaging Tools Emballageværktøjer + Fold Lines Foldelinjer + Make Cut Line Gør til skærelinje + Make Crease Line Gør til rillelinje + Make Partial Cut Line Gør til delvis skærelinje + Make Perforation Line Gør til perforeringslinje + Make Fold Line Gør til foldelinje + Role Swatches… Farveprøver for linjetyper… + Role Swatches Farveprøver for linjetyper + Role Linjetype + Cut Line Skærelinje + Crease Line Rillelinje + Partial Cut Line Delvis skærelinje + Perforation Line Perforeringslinje + No objects are selected. Ingen objekter er markeret. + Open a document to mark its fold lines. Åbn et dokument for at markere dets foldelinjer. + Remove Fold Line Role Fjern linjetype @msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer skal være "singleLine", "everyLine", "standardSingleLine" eller "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 4352164089..21a07e09e5 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -9194,3 +9194,20 @@ breakText Glyphs Glyphen Nest Shapes… Formen verschachteln… Nest Shapes Formen verschachteln Roll Rolle + Packaging Tools Verpackungswerkzeuge + Fold Lines Faltlinien + Make Cut Line Als Schnittlinie festlegen + Make Crease Line Als Rilllinie festlegen + Make Partial Cut Line Als Teilschnittlinie festlegen + Make Perforation Line Als Perforationslinie festlegen + Make Fold Line Als Faltlinie festlegen + Role Swatches… Farbfelder der Linienarten… + Role Swatches Farbfelder der Linienarten + Role Linienart + Cut Line Schnittlinie + Crease Line Rilllinie + Partial Cut Line Teilschnittlinie + Perforation Line Perforationslinie + No objects are selected. Es sind keine Objekte ausgewählt. + Open a document to mark its fold lines. Öffnen Sie ein Dokument, um seine Faltlinien festzulegen. + Remove Fold Line Role Linienart entfernen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index c5f5735a07..ebd167f397 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -7749,4 +7749,21 @@ breakText Glyphs Γλύφοι @msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution τα {width} × {height} εικονοστοιχεία είναι υπερβολικό μέγεθος για JPEG (το πολύ 65535 εικονοστοιχεία ανά πλευρά): μειώστε την ανάλυση @msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution τα {width} × {height} εικονοστοιχεία είναι υπερβολικό μέγεθος για Targa (το πολύ 65535 εικονοστοιχεία ανά πλευρά): μειώστε την ανάλυση @msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution τα {w} × {h} εικονοστοιχεία είναι υπερβολικό μέγεθος για PSD (το πολύ {MAX_SIDE} εικονοστοιχεία ανά πλευρά): μειώστε την ανάλυση + Packaging Tools Εργαλεία συσκευασίας + Fold Lines Γραμμές δίπλωσης + Make Cut Line Δημιουργία γραμμής κοπής + Make Crease Line Δημιουργία γραμμής πτύχωσης + Make Partial Cut Line Δημιουργία γραμμής μερικής κοπής + Make Perforation Line Δημιουργία γραμμής διάτρησης + Make Fold Line Δημιουργία γραμμής δίπλωσης + Role Swatches… Δείγματα χρωμάτων τύπων γραμμής… + Role Swatches Δείγματα χρωμάτων τύπων γραμμής + Role Τύπος γραμμής + Cut Line Γραμμή κοπής + Crease Line Γραμμή πτύχωσης + Partial Cut Line Γραμμή μερικής κοπής + Perforation Line Γραμμή διάτρησης + No objects are selected. Δεν έχουν επιλεγεί αντικείμενα. + Open a document to mark its fold lines. Ανοίξτε ένα έγγραφο για να σημειώσετε τις γραμμές δίπλωσής του. + Remove Fold Line Role Αφαίρεση τύπου γραμμής @msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" το composer πρέπει να είναι "singleLine", "everyLine", "standardSingleLine" ή "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 582c466050..27d7d4aaf8 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -7736,4 +7736,21 @@ breakText Glyphs Glifos Nest Shapes… Anidar formas… Nest Shapes Anidar formas Roll Rollo + Packaging Tools Herramientas de embalaje + Fold Lines Líneas de plegado + Make Cut Line Convertir en línea de corte + Make Crease Line Convertir en línea de hendido + Make Partial Cut Line Convertir en línea de corte parcial + Make Perforation Line Convertir en línea de perforación + Make Fold Line Convertir en línea de plegado + Role Swatches… Muestras de los tipos de línea… + Role Swatches Muestras de los tipos de línea + Role Tipo de línea + Cut Line Línea de corte + Crease Line Línea de hendido + Partial Cut Line Línea de corte parcial + Perforation Line Línea de perforación + No objects are selected. No hay objetos seleccionados. + Open a document to mark its fold lines. Abre un documento para marcar sus líneas de plegado. + Remove Fold Line Role Quitar tipo de línea @msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer debe ser "singleLine", "everyLine", "standardSingleLine" o "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index b4747d4280..3f29fd7f7a 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -7832,4 +7832,21 @@ breakText Glyphs Merkit @msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} pikseliä on liian suuri JPEG-kuvalle (enintään 65535 pikseliä sivua kohti): pienennä tarkkuutta @msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} pikseliä on liian suuri Targa-kuvalle (enintään 65535 pikseliä sivua kohti): pienennä tarkkuutta @msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} pikseliä on liian suuri PSD-kuvalle (enintään {MAX_SIDE} pikseliä sivua kohti): pienennä tarkkuutta + Packaging Tools Pakkaustyökalut + Fold Lines Taittoviivat + Make Cut Line Tee leikkausviiva + Make Crease Line Tee nuuttausviiva + Make Partial Cut Line Tee osittainen leikkausviiva + Make Perforation Line Tee rei’itysviiva + Make Fold Line Tee taittoviiva + Role Swatches… Viivatyyppien värimallit… + Role Swatches Viivatyyppien värimallit + Role Viivatyyppi + Cut Line Leikkausviiva + Crease Line Nuuttausviiva + Partial Cut Line Osittainen leikkausviiva + Perforation Line Rei’itysviiva + No objects are selected. Objekteja ei ole valittu. + Open a document to mark its fold lines. Avaa asiakirja, niin voit merkitä sen taittoviivat. + Remove Fold Line Role Poista viivatyyppi @msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer on oltava "singleLine", "everyLine", "standardSingleLine" tai "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 63d5580ee9..20e94749c5 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -9581,4 +9581,21 @@ breakText Glyphs Glyphes @msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} pixels, c’est trop grand pour le PSD ({MAX_SIDE} pixels par côté au plus) : réduisez la résolution @msg this is a DWG drawing, which can't be read: save it from the CAD app as a DXF and open that c’est un dessin DWG, qui ne peut pas être lu : enregistrez-le depuis l’application de CAO au format DXF et ouvrez ce fichier @msg this binary DXF file is damaged or cut short ce fichier DXF binaire est endommagé ou tronqué + Packaging Tools Outils d’emballage + Fold Lines Lignes de pliage + Make Cut Line Convertir en ligne de découpe + Make Crease Line Convertir en ligne de rainage + Make Partial Cut Line Convertir en ligne de découpe partielle + Make Perforation Line Convertir en ligne de perforation + Make Fold Line Convertir en ligne de pliage + Role Swatches… Nuances des types de ligne… + Role Swatches Nuances des types de ligne + Role Type de ligne + Cut Line Ligne de découpe + Crease Line Ligne de rainage + Partial Cut Line Ligne de découpe partielle + Perforation Line Ligne de perforation + No objects are selected. Aucun objet n’est sélectionné. + Open a document to mark its fold lines. Ouvrez un document pour marquer ses lignes de pliage. + Remove Fold Line Role Supprimer le type de ligne @msg {_1} × {_2} pixels is too large for GIF (at most 65535 pixels a side): lower the resolution {_1} × {_2} pixels, c’est trop grand pour le GIF (65535 pixels par côté au plus) : réduisez la résolution diff --git a/crates/ui-egui/src/i18n/gu.tsv b/crates/ui-egui/src/i18n/gu.tsv index bfce55fb6f..ff59a464c5 100644 --- a/crates/ui-egui/src/i18n/gu.tsv +++ b/crates/ui-egui/src/i18n/gu.tsv @@ -4776,6 +4776,23 @@ swatch [Registration] [રજિસ્ટ્રેશન] Repeat Objects પુનરાવર્તિત ઑબ્જેક્ટ્સ @msg `{path}` must be one of {_1}, not {got} `{path}` એ {_1}માંથી એક હોવું જોઈએ, {got} નહીં Path Length પાથની લંબાઈ + Packaging Tools પેકેજિંગ ટૂલ્સ + Fold Lines ફોલ્ડ લાઇન્સ + Make Cut Line કટ લાઇન બનાવો + Make Crease Line ક્રીઝ લાઇન બનાવો + Make Partial Cut Line આંશિક કટ લાઇન બનાવો + Make Perforation Line પર્ફોરેશન લાઇન બનાવો + Make Fold Line ફોલ્ડ લાઇન બનાવો + Role Swatches… લાઇન પ્રકારના સ્વેચ… + Role Swatches લાઇન પ્રકારના સ્વેચ + Role લાઇન પ્રકાર + Cut Line કટ લાઇન + Crease Line ક્રીઝ લાઇન + Partial Cut Line આંશિક કટ લાઇન + Perforation Line પર્ફોરેશન લાઇન + No objects are selected. કોઈ ઑબ્જેક્ટ પસંદ કરેલ નથી. + Open a document to mark its fold lines. તેની ફોલ્ડ લાઇન્સ ચિહ્નિત કરવા દસ્તાવેજ ખોલો. + Remove Fold Line Role લાઇન પ્રકાર દૂર કરો Middle Eastern & South Asian Single-line Composer સિંગલ-લાઇન કમ્પોઝર (મધ્ય પૂર્વ અને દક્ષિણ એશિયા) Middle Eastern & South Asian Every-line Composer એવરી-લાઇન કમ્પોઝર (મધ્ય પૂર્વ અને દક્ષિણ એશિયા) Standard Single-line Composer સ્ટાન્ડર્ડ સિંગલ-લાઇન કમ્પોઝર diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 385f23fce8..6fe8f254f4 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -9188,3 +9188,20 @@ theme Appearance अपीयरेंस Nest Shapes… आकृतियाँ सटाकर रखें… Nest Shapes आकृतियाँ सटाकर रखें Roll रोल + Packaging Tools पैकेजिंग टूल्स + Fold Lines फ़ोल्ड लाइनें + Make Cut Line कट लाइन बनाएँ + Make Crease Line क्रीज़ लाइन बनाएँ + Make Partial Cut Line आंशिक कट लाइन बनाएँ + Make Perforation Line परफ़ोरेशन लाइन बनाएँ + Make Fold Line फ़ोल्ड लाइन बनाएँ + Role Swatches… लाइन प्रकार के स्वॉचेस… + Role Swatches लाइन प्रकार के स्वॉचेस + Role लाइन प्रकार + Cut Line कट लाइन + Crease Line क्रीज़ लाइन + Partial Cut Line आंशिक कट लाइन + Perforation Line परफ़ोरेशन लाइन + No objects are selected. कोई ऑब्जेक्ट सिलेक्ट नहीं है। + Open a document to mark its fold lines. फ़ोल्ड लाइनें मार्क करने के लिए कोई डॉक्यूमेंट ओपन करें। + Remove Fold Line Role लाइन प्रकार हटाएँ diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 1953789fc2..4a93411b47 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -9584,3 +9584,20 @@ breakText Glyphs Glifák @msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} pixel túl nagy a JPEG formátumhoz (oldalanként legfeljebb 65535 pixel): csökkentsd a felbontást @msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} pixel túl nagy a Targa formátumhoz (oldalanként legfeljebb 65535 pixel): csökkentsd a felbontást @msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} pixel túl nagy a PSD formátumhoz (oldalanként legfeljebb {MAX_SIDE} pixel): csökkentsd a felbontást + Packaging Tools Csomagolóeszközök + Fold Lines Hajtásvonalak + Make Cut Line Vágóvonallá alakítás + Make Crease Line Bigvonallá alakítás + Make Partial Cut Line Részleges vágóvonallá alakítás + Make Perforation Line Perforációs vonallá alakítás + Make Fold Line Hajtásvonallá alakítás + Role Swatches… Vonaltípusok színmintái… + Role Swatches Vonaltípusok színmintái + Role Vonaltípus + Cut Line Vágóvonal + Crease Line Bigvonal + Partial Cut Line Részleges vágóvonal + Perforation Line Perforációs vonal + No objects are selected. Nincs kijelölt objektum. + Open a document to mark its fold lines. Nyiss meg egy dokumentumot a hajtásvonalai megjelöléséhez. + Remove Fold Line Role Vonaltípus eltávolítása diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 94c1b41963..bf029526f7 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -7015,4 +7015,21 @@ breakText Glyphs Glif Nest Shapes… Susun Bentuk… Nest Shapes Susun Bentuk Roll Gulungan + Packaging Tools Alat kemasan + Fold Lines Garis lipat + Make Cut Line Jadikan garis potong + Make Crease Line Jadikan garis lekuk + Make Partial Cut Line Jadikan garis potong sebagian + Make Perforation Line Jadikan garis perforasi + Make Fold Line Jadikan garis lipat + Role Swatches… Swatch jenis garis… + Role Swatches Swatch jenis garis + Role Jenis garis + Cut Line Garis potong + Crease Line Garis lekuk + Partial Cut Line Garis potong sebagian + Perforation Line Garis perforasi + No objects are selected. Tidak ada objek yang dipilih. + Open a document to mark its fold lines. Buka dokumen untuk menandai garis lipatnya. + Remove Fold Line Role Hapus jenis garis @msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer harus "singleLine", "everyLine", "standardSingleLine", atau "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 5b4668d48c..360ec1d5ba 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -9228,3 +9228,20 @@ breakText Glyphs Glifi Nest Shapes… Annida forme… Nest Shapes Annida forme Roll Rotolo + Packaging Tools Strumenti di imballaggio + Fold Lines Linee di piega + Make Cut Line Trasforma in linea di taglio + Make Crease Line Trasforma in linea di cordonatura + Make Partial Cut Line Trasforma in linea di taglio parziale + Make Perforation Line Trasforma in linea di perforazione + Make Fold Line Trasforma in linea di piega + Role Swatches… Campioni dei tipi di linea… + Role Swatches Campioni dei tipi di linea + Role Tipo di linea + Cut Line Linea di taglio + Crease Line Linea di cordonatura + Partial Cut Line Linea di taglio parziale + Perforation Line Linea di perforazione + No objects are selected. Nessun oggetto selezionato. + Open a document to mark its fold lines. Apri un documento per segnarne le linee di piega. + Remove Fold Line Role Rimuovi tipo di linea diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index fd5f79e2c6..37089bb835 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -9295,3 +9295,20 @@ breakText Glyphs 字形 @msg turn on at least one side 少なくとも 1 つの辺をオンにしてください @msg with no hem and no grommets there is nothing to mark 折り返しもハトメもないため、マークを付けるものがありません @msg {saving} failed: {e} {saving} に失敗しました: {e} + Packaging Tools パッケージツール + Fold Lines 折り線 + Make Cut Line カット線にする + Make Crease Line 筋入れ線にする + Make Partial Cut Line 半抜き線にする + Make Perforation Line ミシン目線にする + Make Fold Line 折り線にする + Role Swatches… 線種のスウォッチ… + Role Swatches 線種のスウォッチ + Role 線種 + Cut Line カット線 + Crease Line 筋入れ線 + Partial Cut Line 半抜き線 + Perforation Line ミシン目線 + No objects are selected. オブジェクトが選択されていません。 + Open a document to mark its fold lines. ドキュメントを開くと折り線を設定できます。 + Remove Fold Line Role 線種を解除 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index a5be423a41..25fb4f87ed 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -9867,4 +9867,21 @@ font-script Japanese იაპონური Nest Shapes… ფორმების განლაგება… Nest Shapes ფორმების განლაგება Roll რულონი + Packaging Tools შეფუთვის ხელსაწყოები + Fold Lines მოკეცვის ხაზები + Make Cut Line ჭრის ხაზად გადაქცევა + Make Crease Line ბიგის ხაზად გადაქცევა + Make Partial Cut Line ნაწილობრივი ჭრის ხაზად გადაქცევა + Make Perforation Line პერფორაციის ხაზად გადაქცევა + Make Fold Line მოკეცვის ხაზად გადაქცევა + Role Swatches… ხაზის ტიპების ფერის ნიმუშები… + Role Swatches ხაზის ტიპების ფერის ნიმუშები + Role ხაზის ტიპი + Cut Line ჭრის ხაზი + Crease Line ბიგის ხაზი + Partial Cut Line ნაწილობრივი ჭრის ხაზი + Perforation Line პერფორაციის ხაზი + No objects are selected. ობიექტები არ არის მონიშნული. + Open a document to mark its fold lines. გახსენით დოკუმენტი მისი მოკეცვის ხაზების მოსანიშნად. + Remove Fold Line Role ხაზის ტიპის მოხსნა @msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer უნდა იყოს "singleLine", "everyLine", "standardSingleLine" ან "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 04a4e0fb15..0787180a1c 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -9191,3 +9191,20 @@ breakText Glyphs 글리프 @msg {what} would print more than {MAX_COPIES_PER_SHEET} copies on a sheet: scale it up {what}은(는) 한 장에 {MAX_COPIES_PER_SHEET}개보다 많이 인쇄됩니다: 크기를 키우세요 @msg {what} would print on more than {MAX_TILES} tiles: scale it down {what}은(는) 타일 {MAX_TILES}개보다 많이 나뉘어 인쇄됩니다: 크기를 줄이세요 @msg {what} {i} out of range {what} {i}이(가) 범위를 벗어났습니다 + Packaging Tools 패키징 도구 + Fold Lines 접는 선 + Make Cut Line 재단선으로 만들기 + Make Crease Line 오시선으로 만들기 + Make Partial Cut Line 하프 컷 선으로 만들기 + Make Perforation Line 미싱선으로 만들기 + Make Fold Line 접는 선으로 만들기 + Role Swatches… 선 종류 견본… + Role Swatches 선 종류 견본 + Role 선 종류 + Cut Line 재단선 + Crease Line 오시선 + Partial Cut Line 하프 컷 선 + Perforation Line 미싱선 + No objects are selected. 선택한 개체가 없습니다. + Open a document to mark its fold lines. 접는 선을 지정하려면 문서를 여세요. + Remove Fold Line Role 선 종류 제거 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 8035cd5bd1..ab30a3639f 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -9629,3 +9629,20 @@ breakText Glyphs Glyphs @msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} pixels is te groot voor JPEG (maximaal 65535 pixels per zijde): verlaag de resolutie @msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} pixels is te groot voor Targa (maximaal 65535 pixels per zijde): verlaag de resolutie @msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} pixels is te groot voor PSD (maximaal {MAX_SIDE} pixels per zijde): verlaag de resolutie + Packaging Tools Verpakkingsgereedschap + Fold Lines Vouwlijnen + Make Cut Line Snijlijn maken + Make Crease Line Rillijn maken + Make Partial Cut Line Halve snijlijn maken + Make Perforation Line Perforatielijn maken + Make Fold Line Vouwlijn maken + Role Swatches… Stalen van lijnsoorten… + Role Swatches Stalen van lijnsoorten + Role Lijnsoort + Cut Line Snijlijn + Crease Line Rillijn + Partial Cut Line Halve snijlijn + Perforation Line Perforatielijn + No objects are selected. Er zijn geen objecten geselecteerd. + Open a document to mark its fold lines. Open een document om de vouwlijnen te markeren. + Remove Fold Line Role Lijnsoort verwijderen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index a2e37a4e43..085935a9bd 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -9449,3 +9449,20 @@ breakText Glyphs Glify @msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} pikseli to za dużo dla formatu JPEG (najwyżej 65535 pikseli na bok): zmniejsz rozdzielczość @msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} pikseli to za dużo dla formatu Targa (najwyżej 65535 pikseli na bok): zmniejsz rozdzielczość @msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} pikseli to za dużo dla formatu PSD (najwyżej {MAX_SIDE} pikseli na bok): zmniejsz rozdzielczość + Packaging Tools Narzędzia do opakowań + Fold Lines Linie zgięcia + Make Cut Line Utwórz linię cięcia + Make Crease Line Utwórz linię bigowania + Make Partial Cut Line Utwórz linię nacięcia + Make Perforation Line Utwórz linię perforacji + Make Fold Line Utwórz linię zgięcia + Role Swatches… Próbki rodzajów linii… + Role Swatches Próbki rodzajów linii + Role Rodzaj linii + Cut Line Linia cięcia + Crease Line Linia bigowania + Partial Cut Line Linia nacięcia + Perforation Line Linia perforacji + No objects are selected. Nie zaznaczono żadnych obiektów. + Open a document to mark its fold lines. Otwórz dokument, aby oznaczyć jego linie zgięcia. + Remove Fold Line Role Usuń rodzaj linii diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 63f08ad5a8..0289161fe4 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -9667,3 +9667,20 @@ swatch Yellow Green Verde-amarelado Nest Shapes… Aninhar formas… Nest Shapes Aninhar formas Roll Rolo + Packaging Tools Ferramentas de Embalagem + Fold Lines Linhas de Dobra + Make Cut Line Transformar em Linha de Corte + Make Crease Line Transformar em Linha de Vinco + Make Partial Cut Line Transformar em Linha de Meio-Corte + Make Perforation Line Transformar em Linha de Perfuração + Make Fold Line Transformar em Linha de Dobra + Role Swatches… Amostras dos Tipos de Linha… + Role Swatches Amostras dos Tipos de Linha + Role Tipo de Linha + Cut Line Linha de Corte + Crease Line Linha de Vinco + Partial Cut Line Linha de Meio-Corte + Perforation Line Linha de Perfuração + No objects are selected. Nenhum objeto selecionado. + Open a document to mark its fold lines. Abra um documento para marcar suas linhas de dobra. + Remove Fold Line Role Remover Tipo de Linha diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index d43b45354a..e45ac9423a 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -9690,5 +9690,22 @@ shape Open Открытый @msg {path}: could not be read {path}: не удалось прочитать @msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} пикселей — слишком большой размер для JPEG (не более 65535 пикселей по стороне): уменьшите разрешение @msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} пикселей — слишком большой размер для Targa (не более 65535 пикселей по стороне): уменьшите разрешение + Packaging Tools Инструменты упаковки + Fold Lines Линии сгиба + Make Cut Line Сделать линией реза + Make Crease Line Сделать линией биговки + Make Partial Cut Line Сделать линией надреза + Make Perforation Line Сделать линией перфорации + Make Fold Line Сделать линией сгиба + Role Swatches… Образцы типов линий… + Role Swatches Образцы типов линий + Role Тип линии + Cut Line Линия реза + Crease Line Линия биговки + Partial Cut Line Линия надреза + Perforation Line Линия перфорации + No objects are selected. Объекты не выделены. + Open a document to mark its fold lines. Откройте документ, чтобы отметить его линии сгиба. + Remove Fold Line Role Убрать тип линии @msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} пикселей — слишком большой размер для PSD (не более {MAX_SIDE} пикселей по стороне): уменьшите разрешение @msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer должен быть "singleLine", "everyLine", "standardSingleLine" или "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 2795938a17..95e7c34b47 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -9265,4 +9265,21 @@ digits Hindi Hindi @msg {path}: could not be read {path}: kunde inte läsas @msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} pixlar är för stort för JPEG (högst 65535 pixlar per sida): sänk upplösningen @msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} pixlar är för stort för Targa (högst 65535 pixlar per sida): sänk upplösningen + Packaging Tools Förpackningsverktyg + Fold Lines Falslinjer + Make Cut Line Gör till skärlinje + Make Crease Line Gör till biglinje + Make Partial Cut Line Gör till halvskärlinje + Make Perforation Line Gör till perforeringslinje + Make Fold Line Gör till falslinje + Role Swatches… Färgrutor för linjetyper… + Role Swatches Färgrutor för linjetyper + Role Linjetyp + Cut Line Skärlinje + Crease Line Biglinje + Partial Cut Line Halvskärlinje + Perforation Line Perforeringslinje + No objects are selected. Inga objekt är markerade. + Open a document to mark its fold lines. Öppna ett dokument för att markera dess falslinjer. + Remove Fold Line Role Ta bort linjetyp @msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} pixlar är för stort för PSD (högst {MAX_SIDE} pixlar per sida): sänk upplösningen diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 51723c54d3..d8249b1c79 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -9550,3 +9550,20 @@ breakText Glyphs Glifler Nest Shapes… Şekilleri yerleştir… Nest Shapes Şekilleri yerleştir Roll Rulo + Packaging Tools Ambalaj Araçları + Fold Lines Katlama Çizgileri + Make Cut Line Kesim Çizgisi Yap + Make Crease Line Pilyaj Çizgisi Yap + Make Partial Cut Line Yarım Kesim Çizgisi Yap + Make Perforation Line Perforaj Çizgisi Yap + Make Fold Line Katlama Çizgisi Yap + Role Swatches… Çizgi Türü Renk Örnekleri… + Role Swatches Çizgi Türü Renk Örnekleri + Role Çizgi türü + Cut Line Kesim çizgisi + Crease Line Pilyaj çizgisi + Partial Cut Line Yarım kesim çizgisi + Perforation Line Perforaj çizgisi + No objects are selected. Hiçbir nesne seçili değil. + Open a document to mark its fold lines. Katlama çizgilerini işaretlemek için bir belgeyi açın. + Remove Fold Line Role Çizgi Türünü Kaldır diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 902af68c96..8ebb97a5be 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -9203,3 +9203,20 @@ breakText Glyphs Гліфи Nest Shapes… Розкладка фігур… Nest Shapes Розкладка фігур Roll Рулон + Packaging Tools Інструменти пакування + Fold Lines Лінії згину + Make Cut Line Зробити лінією різу + Make Crease Line Зробити лінією біговки + Make Partial Cut Line Зробити лінією надрізу + Make Perforation Line Зробити лінією перфорації + Make Fold Line Зробити лінією згину + Role Swatches… Зразки типів ліній… + Role Swatches Зразки типів ліній + Role Тип лінії + Cut Line Лінія різу + Crease Line Лінія біговки + Partial Cut Line Лінія надрізу + Perforation Line Лінія перфорації + No objects are selected. Об’єкти не виділено. + Open a document to mark its fold lines. Відкрийте документ, щоб позначити його лінії згину. + Remove Fold Line Role Прибрати тип лінії diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 0c0291280d..a9a9cb140a 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -7289,6 +7289,23 @@ breakText Glyphs Glyph Nest Shapes… Xếp hình tiết kiệm vật liệu… Nest Shapes Xếp hình tiết kiệm vật liệu Roll Cuộn + Packaging Tools Công cụ bao bì + Fold Lines Đường gấp + Make Cut Line Đặt làm đường cắt + Make Crease Line Đặt làm đường cấn + Make Partial Cut Line Đặt làm đường cắt một phần + Make Perforation Line Đặt làm đường đục lỗ + Make Fold Line Đặt làm đường gấp + Role Swatches… Mẫu màu của các loại đường… + Role Swatches Mẫu màu của các loại đường + Role Loại đường + Cut Line Đường cắt + Crease Line Đường cấn + Partial Cut Line Đường cắt một phần + Perforation Line Đường đục lỗ + No objects are selected. Không có đối tượng nào được chọn. + Open a document to mark its fold lines. Mở tài liệu để đánh dấu các đường gấp của nó. + Remove Fold Line Role Bỏ loại đường # Messages of the effects, 3D, image export, color profiles, swatch exchange files, color books and DXF import. @msg 3D effects require finite coordinates within 100 million points Hiệu ứng 3D yêu cầu tọa độ hữu hạn trong phạm vi 100 triệu điểm diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 6ba6d0485c..f3a87e6b84 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -9434,4 +9434,21 @@ breakText Glyphs 字形 @msg {path}: could not be read {path}:无法读取 @msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} 像素对 JPEG 而言过大 (每边最多 65535 像素):请降低分辨率 @msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} 像素对 Targa 而言过大 (每边最多 65535 像素):请降低分辨率 + Packaging Tools 包装工具 + Fold Lines 折线 + Make Cut Line 设为切割线 + Make Crease Line 设为压痕线 + Make Partial Cut Line 设为半切线 + Make Perforation Line 设为打孔线 + Make Fold Line 设为折线 + Role Swatches… 线条类型色板… + Role Swatches 线条类型色板 + Role 线条类型 + Cut Line 切割线 + Crease Line 压痕线 + Partial Cut Line 半切线 + Perforation Line 打孔线 + No objects are selected. 未选择任何对象。 + Open a document to mark its fold lines. 打开文档以标记其折线。 + Remove Fold Line Role 移除线条类型 @msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} 像素对 PSD 而言过大 (每边最多 {MAX_SIDE} 像素):请降低分辨率 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 5274909cf5..52715f8360 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -9244,4 +9244,21 @@ breakText Glyphs 字符 @msg {v} is not an asset id {v} 不是資產 ID @msg {what} can't be intertwined {what} 無法交織 @msg {what} doesn't fit the paper with its bleed and marks {what} 連同出血和標記無法容納於紙張 + Packaging Tools 包裝工具 + Fold Lines 摺線 + Make Cut Line 設為切割線 + Make Crease Line 設為壓痕線 + Make Partial Cut Line 設為半切線 + Make Perforation Line 設為打孔線 + Make Fold Line 設為摺線 + Role Swatches… 線條類型色票… + Role Swatches 線條類型色票 + Role 線條類型 + Cut Line 切割線 + Crease Line 壓痕線 + Partial Cut Line 半切線 + Perforation Line 打孔線 + No objects are selected. 未選取任何物件。 + Open a document to mark its fold lines. 開啟文件以標記其摺線。 + Remove Fold Line Role 移除線條類型 @msg {what} would print on more than {MAX_TILES} tiles: scale it down {what} 會列印在超過 {MAX_TILES} 個拼貼上:請縮小 diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index a36e9c245d..487d501054 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -190,6 +190,7 @@ pub fn show_icon_panel(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) { learn::ID => learn::show(app, ui), crate::addons::cleanup::ID => crate::addons::cleanup::show(app, ui), crate::addons::color::ID => crate::addons::color::show(app, ui), + crate::addons::packaging::ID => crate::addons::packaging::show(app, ui), crate::addons::pathtools::ID => crate::addons::pathtools::show(app, ui), crate::addons::pathtools::shape::ID => crate::addons::pathtools::shape::show(app, ui), crate::addons::pen::ID => crate::addons::pen::show(app, ui), @@ -245,6 +246,7 @@ pub fn panel_menu_items(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) -> bool variables::ID => variables::menu(app, ui), script_console::ID => script_console::menu(app, ui), "actions" => actions::menu(app, ui), + crate::addons::packaging::ID => crate::addons::packaging::panel_menu(app, ui), id if library_panel::parse(id).is_some() => library_panel::menu(app, ui, id), _ => return false, } diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 26d3753b51..2fbe54d633 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -227,6 +227,7 @@ pub const ICON_PANELS: &[(&str, &str, &str)] = &[ (crate::panels::learn::ID, "Learn", "graduation-cap"), (crate::addons::cleanup::ID, "Artwork Cleanup", "sparkles"), (crate::addons::color::ID, "Color Effects", "sun"), + (crate::addons::packaging::ID, "Fold Lines", "scissors"), (crate::addons::pathtools::ID, "Anchor Inspector", "dc-anchor-smooth"), (crate::addons::pathtools::shape::ID, "Parametric Shape", "hexagon"), (crate::addons::pen::ID, "Smart Pen", "spline-pointer"), diff --git a/docs/mcp.md b/docs/mcp.md index 425fac0a2a..e623b86cac 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -12562,6 +12562,58 @@ there is no step. → `{symbols: [name…], ids: [instance id…], skipped: [{la {"name":"run_command","arguments":{"command":"addon.document.symbolsFromLayers","params":{"replace":true}}} ``` +## Packaging Tools add-on + +Packaging Tools is a built-in add-on for the dielines of boxes and other folded packaging. Its commands are +`addon.packaging.setRole`, `addon.packaging.roles` and `addon.packaging.setRoleSwatch`. In the desktop app, Object › +Plug-ins › Packaging Tools › Fold Lines opens the Fold Lines panel, and Make Cut Line, Make Crease Line, Make Partial Cut +Line and Make Perforation Line run `addon.packaging.setRole` on the selection. + +### Fold line roles + +A fold line has one of four roles: Cut (`cut`), Crease (`crease`), Partial Cut (`partialCut`) and Perforation +(`perforation`). A path, compound path or live shape has a role when its top stroke is painted with the role's swatch, at +any tint. Each document has a role table that names the swatch of each role, stored in the document under +`packagingRoles` as `{"cut": "Cut", "crease": "Crease", "partialCut": "Partial Cut", "perforation": "Perforation"}`. A +role the table leaves out, or names with something other than a string, uses the swatch named after the role. When +`swatch.edit` renames a role's swatch, the table names the swatch by its new name and the role stays with it; another +role whose swatch did not exist yet and had that name moves to a new name ("Crease 2"). When `swatch.edit` turns Global +off for a role's swatch, which unlinks its strokes, the role moves to a new name, and the next `addon.packaging.setRole` +makes that swatch. When `swatch.merge` merges a role's swatch into a global swatch that carries no role, the kept swatch +takes the role. Native files keep the table and the strokes. PDF prints a role line on the separation of its swatch when +the swatch is a spot color, as the default role swatches are; a role carried by a global process color is written as an +ordinary color with no separation of its own. SVG, DXF and HP-GL keep the stroke's color, so the default role swatches +come out as four colors, and as four pens in HP-GL without a pen table. + +`addon.packaging.setRole` (`{role: cut|crease|partialCut|perforation|none, ids?, overprint?: true}`) gives the role to +each path, compound path and live shape among the objects (the selection by default), through groups and clip groups. +Objects that are locked or hidden, in a locked or hidden group or layer, or on a locked artboard are left out and +counted as skipped, also when `ids` names them. The top stroke is painted with the role's swatch at 100% and shown if +it was hidden, a path without a stroke gets a 1 pt stroke, and the stroke overprints unless `overprint` is false. When +the document lacks the role's swatch, the command makes it as a global spot swatch in the document's color mode: Cut 100% +magenta, Crease 100% cyan, Partial Cut 100% yellow and Perforation 50% magenta and 50% cyan (VectorCraft's own defaults). +When a color group, or a swatch that can't carry a role such as a process color that is not global, has the swatch's +name, the swatch is named "Cut 2" (and so on) and the role table names it. `none` takes the role away: the stroke keeps +the color it shows, no longer linked to the swatch, and stops overprinting. Text, images, symbol instances and other +objects are skipped and counted. The command fails with an unknown role and when the objects hold no path it can change. One undo +step, none when nothing changes. Result: `{ids, changed, skipped}`. + +`addon.packaging.roles` (`{ids?}`) returns `{roles: [{role, name, swatch, exists, count}], scope, selected}`: each role's +swatch, whether the document has it, and how many of the objects have the role. It counts the selection, or every object +of the document while nothing is selected and `ids` is left out (`scope` says which). Locked and hidden objects are +counted in every scope, and an object is counted once when both it and its group are given. The Fold Lines panel shows +these counts; clicking a role gives the selected paths that role. With nothing selected the panel says so. + +`addon.packaging.setRoleSwatch` (`{role, swatch}`) makes another global or spot color swatch carry the role. Objects +stroked with the old swatch keep their color and no longer have the role. It fails with a missing swatch, a gradient or +pattern swatch, a process color that is not global, a tint swatch, `[None]`, `[Registration]` and a swatch another role +has. One undo step, none when the role already has that swatch. Result: `{role, swatch, changed}`. The Fold Lines +panel's menu opens it as Role Swatches…. + +```json +{"name":"run_command","arguments":{"command":"addon.packaging.setRole","params":{"role":"crease"}}} +``` + ## Image Tools add-on Image Tools is a built-in add-on for placed images: its commands are `addon.image.removeBackground`,