M3.77: [Registration] swatch on every plate, trim marks on every artboard, Japanese marks and live Crop Marks
[Registration] is a built-in swatch that prints 100% on every plate: Separations
Preview shows it on each process and spot plate and PDF export writes it as
/Separation /All. It can't be edited, moved, duplicated, merged or deleted;
swatch.list shows it after None and the Swatches panel applies it as a swatch.
Object > Create Trim Marks strokes the marks in Registration and marks every
artboard when nothing is selected ({style?, allArtboards?}). The preference
japaneseCropMarks (General) switches to Japanese marks: double lines at the
trim and bleed edges plus centre marks. Effect > Crop Marks (cropMarks) is a
live effect whose marks follow the object; rendering, SVG/PDF export and
Expand Appearance draw them. The mark geometry lives in doc::marks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4591c85845
commit
c1df6f0621
|
|
@ -328,6 +328,14 @@ impl Paint {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
/// The Registration colour: 100% of every ink, linked to [`swatch::REGISTRATION`].
|
||||
pub fn registration() -> Self {
|
||||
Paint::Solid { color: Color::cmyk(1.0, 1.0, 1.0, 1.0), swatch: Some(swatch::REGISTRATION.into()), tint: 1.0 }
|
||||
}
|
||||
/// Is this a colour linked to the Registration swatch (it prints on every plate)?
|
||||
pub fn is_registration(&self) -> bool {
|
||||
matches!(self, Paint::Solid { swatch: Some(s), .. } if s == swatch::REGISTRATION)
|
||||
}
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Paint::None => "None".into(),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,16 @@ pub struct Swatch {
|
|||
pub spot: bool,
|
||||
}
|
||||
|
||||
/// The name of the built-in Registration swatch ([`registration`]).
|
||||
pub const REGISTRATION: &str = "[Registration]";
|
||||
|
||||
/// The built-in Registration swatch: a global colour of 100% of every ink that prints on every
|
||||
/// plate, process and spot (printer's marks). Every document has it; it can't be edited or deleted.
|
||||
pub fn registration() -> &'static Swatch {
|
||||
static S: std::sync::OnceLock<Swatch> = std::sync::OnceLock::new();
|
||||
S.get_or_init(|| Swatch { name: REGISTRATION.into(), paint: Paint::solid(Color::cmyk(1.0, 1.0, 1.0, 1.0)), global: true, spot: false })
|
||||
}
|
||||
|
||||
impl Swatch {
|
||||
/// A tint swatch's base swatch and tint: its colour links to another (global) swatch.
|
||||
pub fn tint_of(&self) -> Option<(&str, f32)> {
|
||||
|
|
@ -25,6 +35,10 @@ impl Swatch {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
/// None and Registration: built in, they can't be edited, moved or deleted.
|
||||
pub fn is_reserved(&self) -> bool {
|
||||
self.paint.is_none() || self.name == REGISTRATION
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ pub mod appearance;
|
|||
pub mod graph;
|
||||
pub mod hit;
|
||||
pub mod live;
|
||||
pub mod marks;
|
||||
pub mod node;
|
||||
pub mod overprint;
|
||||
pub mod pattern;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
//! Printer's marks drawn as art: the trim marks of Object → Create Trim Marks and Effect → Crop
|
||||
//! Marks, stroked in the Registration colour so that they print on every plate.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_geom::{Point, Rect, shapes};
|
||||
|
||||
use crate::{Appearance, Node, NodeId};
|
||||
|
||||
/// Trim mark style (the preference `japaneseCropMarks` picks Japanese).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MarkStyle {
|
||||
/// One line per edge at each corner, set off from the trim box.
|
||||
#[default]
|
||||
Roman,
|
||||
/// Double lines at each corner (the trim and the bleed edges) and centre marks on each side.
|
||||
Japanese,
|
||||
}
|
||||
|
||||
impl MarkStyle {
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
MarkStyle::Roman => "roman",
|
||||
MarkStyle::Japanese => "japanese",
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an id, ignoring case.
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
[MarkStyle::Roman, MarkStyle::Japanese].into_iter().find(|m| m.id().eq_ignore_ascii_case(s))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trim marks around a rectangle (the trim box).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct TrimMarks {
|
||||
pub style: MarkStyle,
|
||||
/// Roman: the gap between the trim box and the marks.
|
||||
pub offset: f64,
|
||||
/// How long each mark is.
|
||||
pub length: f64,
|
||||
/// Japanese: the bleed, between the trim lines and the outer lines (3 mm).
|
||||
pub bleed: f64,
|
||||
/// Stroke weight.
|
||||
pub weight: f64,
|
||||
}
|
||||
|
||||
impl Default for TrimMarks {
|
||||
fn default() -> Self {
|
||||
Self { style: MarkStyle::Roman, offset: 9.0, length: 18.0, bleed: 3.0 * 72.0 / 25.4, weight: 0.3 }
|
||||
}
|
||||
}
|
||||
|
||||
impl TrimMarks {
|
||||
/// The default marks of `style`.
|
||||
pub fn of(style: MarkStyle) -> Self {
|
||||
Self { style, ..Default::default() }
|
||||
}
|
||||
|
||||
/// How far the marks reach outside the trim box.
|
||||
pub fn reach(&self) -> f64 {
|
||||
let gap = match self.style {
|
||||
MarkStyle::Roman => self.offset,
|
||||
MarkStyle::Japanese => self.bleed,
|
||||
};
|
||||
gap + self.length + self.weight
|
||||
}
|
||||
|
||||
/// The marks' lines around `r`.
|
||||
pub fn lines(&self, r: Rect) -> Vec<(Point, Point)> {
|
||||
let (l, mut out) = (self.length, vec![]);
|
||||
// Each corner with its outward directions.
|
||||
let corners = [(r.x0, r.y0, -1.0, -1.0), (r.x1, r.y0, 1.0, -1.0), (r.x1, r.y1, 1.0, 1.0), (r.x0, r.y1, -1.0, 1.0)];
|
||||
match self.style {
|
||||
MarkStyle::Roman => {
|
||||
let o = self.offset;
|
||||
for (x, y, sx, sy) in corners {
|
||||
out.push((Point::new(x + sx * o, y), Point::new(x + sx * (o + l), y)));
|
||||
out.push((Point::new(x, y + sy * o), Point::new(x, y + sy * (o + l))));
|
||||
}
|
||||
}
|
||||
MarkStyle::Japanese => {
|
||||
let b = self.bleed;
|
||||
for (x, y, sx, sy) in corners {
|
||||
// The trim lines, outside the bleed, and the bleed lines meeting at its corner.
|
||||
out.push((Point::new(x + sx * b, y), Point::new(x + sx * (b + l), y)));
|
||||
out.push((Point::new(x, y + sy * b), Point::new(x, y + sy * (b + l))));
|
||||
out.push((Point::new(x, y + sy * b), Point::new(x + sx * (b + l), y + sy * b)));
|
||||
out.push((Point::new(x + sx * b, y), Point::new(x + sx * b, y + sy * (b + l))));
|
||||
}
|
||||
// A cross outside the bleed at the middle of each side.
|
||||
let c = r.center();
|
||||
for (p, dx, dy) in [
|
||||
(Point::new(c.x, r.y0), 0.0, -1.0),
|
||||
(Point::new(r.x1, c.y), 1.0, 0.0),
|
||||
(Point::new(c.x, r.y1), 0.0, 1.0),
|
||||
(Point::new(r.x0, c.y), -1.0, 0.0),
|
||||
] {
|
||||
let at = |d: f64| Point::new(p.x + dx * d, p.y + dy * d);
|
||||
out.push((at(b), at(b + l)));
|
||||
let m = at(b + l / 2.0);
|
||||
out.push((Point::new(m.x - dy * l / 2.0, m.y - dx * l / 2.0), Point::new(m.x + dy * l / 2.0, m.y + dx * l / 2.0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The marks around `r` as a group named "Trim Marks" of lines stroked in Registration, with
|
||||
/// ids from `alloc` (the group's last).
|
||||
pub fn group(&self, r: Rect, alloc: &mut dyn FnMut() -> NodeId) -> Node {
|
||||
let ap = Appearance::basic(Paint::None, Paint::registration(), self.weight);
|
||||
let children = self.lines(r).into_iter().map(|(a, b)| Arc::new(Node::path(alloc(), shapes::line(a, b), ap.clone()))).collect();
|
||||
let mut g = Node::group(alloc(), children);
|
||||
g.name = Some("Trim Marks".into());
|
||||
g
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn roman_marks_sit_off_the_corners_and_japanese_ones_are_doubled() {
|
||||
let r = Rect::new(0.0, 0.0, 100.0, 50.0);
|
||||
let roman = TrimMarks::default().lines(r);
|
||||
assert_eq!(roman.len(), 8);
|
||||
assert!(roman.iter().all(|(a, b)| !r.contains(*a) && !r.contains(*b)));
|
||||
assert_eq!(roman[0], (Point::new(-9.0, 0.0), Point::new(-27.0, 0.0)));
|
||||
let jp = TrimMarks::of(MarkStyle::Japanese);
|
||||
let lines = jp.lines(r);
|
||||
assert_eq!(lines.len(), 24, "four lines per corner and a cross per side");
|
||||
// Two horizontal lines at the top-left corner: the trim line and the bleed line.
|
||||
let tl: Vec<f64> = lines[..4].iter().filter(|(a, b)| a.y == b.y).map(|(a, _)| a.y).collect();
|
||||
assert_eq!(tl.len(), 2);
|
||||
assert!((tl[0] - 0.0).abs() < 1e-9 && (tl[1] + jp.bleed).abs() < 1e-9, "{tl:?}");
|
||||
let mut n = 0;
|
||||
let g = jp.group(r, &mut || {
|
||||
n += 1;
|
||||
NodeId(n)
|
||||
});
|
||||
assert_eq!((g.children().unwrap().len(), g.id), (24, NodeId(25)));
|
||||
assert!(g.children().unwrap()[0].appearance.stroke().unwrap().paint.is_registration());
|
||||
assert_eq!(MarkStyle::parse("Japanese"), Some(MarkStyle::Japanese));
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_color::swatch::REGISTRATION;
|
||||
use vectorcraft_color::{Color, Paint, Swatch, tint_percent};
|
||||
|
||||
use crate::{AppearanceItem, Document, Node, NodeId, NodeKind};
|
||||
|
|
@ -21,8 +22,10 @@ impl Document {
|
|||
pub fn swatches_iter_mut(&mut self) -> impl Iterator<Item = &mut Swatch> {
|
||||
self.swatches.iter_mut().chain(self.swatch_groups.iter_mut().flat_map(|g| g.swatches.iter_mut()))
|
||||
}
|
||||
/// Swatch `name`, the built-in Registration swatch ([`vectorcraft_color::swatch::registration`])
|
||||
/// included.
|
||||
pub fn swatch(&self, name: &str) -> Option<&Swatch> {
|
||||
self.swatches_iter().find(|s| s.name == name)
|
||||
self.swatches_iter().find(|s| s.name == name).or_else(|| (name == REGISTRATION).then(vectorcraft_color::swatch::registration))
|
||||
}
|
||||
pub fn swatch_mut(&mut self, name: &str) -> Option<&mut Swatch> {
|
||||
self.swatches_iter_mut().find(|s| s.name == name)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ use vectorcraft_doc::{Appearance, AppearanceItem, Document, Node, NodeKind, Stro
|
|||
use vectorcraft_geom::{FillRule, PathData};
|
||||
|
||||
use crate::group::{has_own_paint, paints};
|
||||
use crate::{GeomContext, apply_geometry_with, evaluate_container, has_geometry, has_pathfinder, is_geometry, needs_outline, reshape};
|
||||
use crate::{
|
||||
GeomContext, apply_geometry_with, crop_marks_art, evaluate_container, has_crop_marks, has_geometry, has_pathfinder, is_geometry, needs_outline,
|
||||
reshape,
|
||||
};
|
||||
|
||||
fn item_effects(item: &AppearanceItem) -> &[vectorcraft_doc::Effect] {
|
||||
match item {
|
||||
|
|
@ -34,7 +37,8 @@ fn clear_item_effects(item: &mut AppearanceItem) {
|
|||
|
||||
/// Does anything in `n`'s subtree need baking?
|
||||
pub fn needs_bake(n: &Node) -> bool {
|
||||
has_pathfinder(n)
|
||||
has_crop_marks(n)
|
||||
|| has_pathfinder(n)
|
||||
|| has_own_paint(n)
|
||||
|| has_geometry(&n.appearance.effects)
|
||||
|| n.appearance.items.iter().any(|i| has_geometry(item_effects(i)))
|
||||
|
|
@ -74,6 +78,10 @@ fn bake_node(d: &mut Document, n: &Node) -> Option<Node> {
|
|||
if !needs_bake(n) {
|
||||
return None;
|
||||
}
|
||||
// Crop marks: the object and its marks.
|
||||
if let Some(m) = crop_marks_art(n) {
|
||||
return Some(bake_pieces(d, m));
|
||||
}
|
||||
// Type, images, symbol instances and live objects: reshaped through their outlines.
|
||||
if needs_outline(n) && has_geometry(&n.appearance.effects) {
|
||||
let symbol = match &n.kind {
|
||||
|
|
@ -85,21 +93,8 @@ fn bake_node(d: &mut Document, n: &Node) -> Option<Node> {
|
|||
}
|
||||
// Groups and layers: Pathfinder, geometry effects and their own fills and strokes. The new
|
||||
// pieces (results, outlines, the fills' and strokes' art) get ids of their own.
|
||||
if let Some(mut m) = evaluate_container(n) {
|
||||
let mut seen = std::collections::HashSet::from([m.id]);
|
||||
let children = m.children().cloned().unwrap_or_default();
|
||||
let children = children
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
let mut c = Arc::unwrap_or_clone(c);
|
||||
fresh_ids(d, &mut c, &mut seen);
|
||||
Arc::new(bake_node(d, &c).unwrap_or(c))
|
||||
})
|
||||
.collect();
|
||||
if let Some(ch) = m.children_mut() {
|
||||
*ch = children;
|
||||
}
|
||||
return Some(m);
|
||||
if let Some(m) = evaluate_container(n) {
|
||||
return Some(bake_pieces(d, m));
|
||||
}
|
||||
if let Some(ch) = n.children() {
|
||||
if matches!(n.kind, NodeKind::Compound { .. })
|
||||
|
|
@ -117,6 +112,25 @@ fn bake_node(d: &mut Document, n: &Node) -> Option<Node> {
|
|||
bake_leaf(d, n)
|
||||
}
|
||||
|
||||
/// `m`, art evaluated from an object, with its children baked and given ids of their own (the
|
||||
/// pieces copied from the source keep the source's ids until then).
|
||||
fn bake_pieces(d: &mut Document, mut m: Node) -> Node {
|
||||
let mut seen = std::collections::HashSet::from([m.id]);
|
||||
let children = m.children().cloned().unwrap_or_default();
|
||||
let children = children
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
let mut c = Arc::unwrap_or_clone(c);
|
||||
fresh_ids(d, &mut c, &mut seen);
|
||||
Arc::new(bake_node(d, &c).unwrap_or(c))
|
||||
})
|
||||
.collect();
|
||||
if let Some(ch) = m.children_mut() {
|
||||
*ch = children;
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// Give `n` and its descendants a new id wherever one was already `seen` (the pieces evaluated art
|
||||
/// copies from its source keep ids of their own).
|
||||
pub fn fresh_ids(d: &mut Document, n: &mut Node, seen: &mut std::collections::HashSet<vectorcraft_doc::NodeId>) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ mod bake;
|
|||
mod clip;
|
||||
mod distort;
|
||||
mod group;
|
||||
mod marks;
|
||||
mod raster;
|
||||
mod reshape;
|
||||
pub mod stroke;
|
||||
|
|
@ -46,6 +47,7 @@ pub use clip::clip_outline;
|
|||
pub use group::{
|
||||
OutlineHook, PATHFINDER_EFFECTS, evaluate_container, has_container_appearance, has_pathfinder, is_pathfinder, member_shapes, pathfinder_children,
|
||||
};
|
||||
pub use marks::{CROP_MARKS, crop_marks_art, has_crop_marks};
|
||||
pub use raster::{RasterFx, outset, raster_effects};
|
||||
pub use reshape::{expand_outlined, needs_outline, outline_art, outline_text, reshape};
|
||||
pub use warp::{WarpStyle, warp_point};
|
||||
|
|
@ -228,6 +230,13 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
|||
for (id, label, _) in PATHFINDER_EFFECTS {
|
||||
v.push(g(id, label, PATHFINDER, "{} (groups and layers: live Pathfinder over the members)", json!({})));
|
||||
}
|
||||
v.push(g(
|
||||
CROP_MARKS,
|
||||
"Crop Marks",
|
||||
&["Effect"],
|
||||
"{style?: \"roman\"|\"japanese\" (default: the japaneseCropMarks preference when applied)} trim marks in [Registration] around the object's bounds, following it",
|
||||
json!({}),
|
||||
));
|
||||
v
|
||||
}
|
||||
|
||||
|
|
@ -272,9 +281,9 @@ pub fn is_raster(id: &str) -> bool {
|
|||
matches!(id, "stylize.dropShadow" | "stylize.innerGlow" | "stylize.outerGlow" | "stylize.feather" | "blur.gaussian")
|
||||
}
|
||||
|
||||
/// Does `id` change geometry?
|
||||
/// Does `id` change geometry? (Crop Marks adds art of its own instead, [`crop_marks_art`].)
|
||||
pub fn is_geometry(id: &str) -> bool {
|
||||
!is_raster(id) && !is_pathfinder(id) && catalog_index().contains_key(id)
|
||||
!is_raster(id) && !is_pathfinder(id) && id != CROP_MARKS && catalog_index().contains_key(id)
|
||||
}
|
||||
|
||||
/// Any visible geometry effect in the list?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
//! Effect → Crop Marks: live trim marks in the Registration colour around the object, which follow
|
||||
//! it as it moves or changes size. The renderer, the exporters ([`crate::bake_document`]) and
|
||||
//! Expand Appearance draw the object as [`crop_marks_art`].
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::Node;
|
||||
use vectorcraft_doc::marks::{MarkStyle, TrimMarks};
|
||||
|
||||
/// The effect id.
|
||||
pub const CROP_MARKS: &str = "cropMarks";
|
||||
|
||||
/// Does `n` carry a visible Crop Marks effect?
|
||||
pub fn has_crop_marks(n: &Node) -> bool {
|
||||
n.appearance.effects.iter().any(|e| e.visible && e.id == CROP_MARKS)
|
||||
}
|
||||
|
||||
/// The art of an object with Crop Marks: a group (with the object's id, name and transparency) of
|
||||
/// the object itself, without the effect, and the marks around its visual bounds (the last Crop
|
||||
/// Marks effect's style). `None` without a visible Crop Marks effect.
|
||||
pub fn crop_marks_art(n: &Node) -> Option<Node> {
|
||||
let fx = n.appearance.effects.iter().rev().find(|e| e.visible && e.id == CROP_MARKS)?;
|
||||
let style = fx.params.get("style").and_then(|v| v.as_str()).and_then(MarkStyle::parse).unwrap_or_default();
|
||||
let bounds = n.visual_bounds()?;
|
||||
let marks = TrimMarks::of(style).group(bounds, &mut || n.id);
|
||||
let mut object = n.clone();
|
||||
object.appearance.effects.retain(|e| e.id != CROP_MARKS);
|
||||
// The group takes over the object's identity, transparency, mask and link.
|
||||
let mut g = Node::group(n.id, vec![]);
|
||||
(g.visible, g.locked) = (n.visible, n.locked);
|
||||
g.name = object.name.take();
|
||||
g.opacity = std::mem::replace(&mut object.opacity, 1.0);
|
||||
g.blend = std::mem::take(&mut object.blend);
|
||||
g.isolate = std::mem::take(&mut object.isolate);
|
||||
g.mask = object.mask.take();
|
||||
g.attrs = object.attrs.take();
|
||||
if let Some(ch) = g.children_mut() {
|
||||
*ch = vec![Arc::new(object), Arc::new(marks)];
|
||||
}
|
||||
Some(g)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::{Appearance, Effect, NodeId};
|
||||
use vectorcraft_geom::{Rect, shapes};
|
||||
|
||||
#[test]
|
||||
fn the_marks_surround_the_object_and_follow_it() {
|
||||
let mut n = Node::path(NodeId(7), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 50.0)), Appearance::default_art());
|
||||
assert!(crop_marks_art(&n).is_none());
|
||||
n.opacity = 0.5;
|
||||
n.appearance.effects.push(Effect { id: CROP_MARKS.into(), params: json!({"style": "japanese"}), visible: true });
|
||||
let art = crop_marks_art(&n).unwrap();
|
||||
assert_eq!((art.id, art.opacity), (NodeId(7), 0.5));
|
||||
let [object, marks] = art.children().unwrap().as_slice() else { panic!() };
|
||||
assert!(object.appearance.effects.is_empty() && object.opacity == 1.0);
|
||||
assert_eq!(marks.children().unwrap().len(), 24, "Japanese marks");
|
||||
// Around the stroked bounds (0.5 outside the path), by the marks' reach.
|
||||
let reach = TrimMarks::of(MarkStyle::Japanese).reach() + 0.5;
|
||||
let b = art.visual_bounds().unwrap();
|
||||
assert!((b.x0 + reach).abs() < 2.0 && (b.x1 - 100.0 - reach).abs() < 2.0, "{b:?}");
|
||||
// Moved, the marks move along.
|
||||
n.transform(vectorcraft_geom::Affine::translate((40.0, 0.0)), false);
|
||||
let b2 = crop_marks_art(&n).unwrap().visual_bounds().unwrap();
|
||||
assert!((b2.x0 - b.x0 - 40.0).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Expand Appearance",
|
||||
["Object"],
|
||||
None,
|
||||
"{ids?: [..], target?} turn each object's appearance into objects: every fill and stroke becomes an object of its own (strokes outlined, brushed strokes their brush art) grouped in paint order under the object's id and transparency, geometry effects are baked, raster effects become an embedded image (shadows and outer glows below the art; blur, feather and inner glow replace it), type with effects or fills of its own is outlined; a group's or layer's own fills and strokes become objects among its members, which are expanded too. Hidden fills, strokes and effects are dropped. Enabled when a selected or targeted object's appearance isn't basic (`ids` then pick which objects) → {ids}",
|
||||
"{ids?: [..], target?} turn each object's appearance into objects: every fill and stroke becomes an object of its own (strokes outlined, brushed strokes their brush art) grouped in paint order under the object's id and transparency, geometry effects are baked, raster effects become an embedded image (shadows and outer glows below the art; blur, feather and inner glow replace it), type with effects or fills of its own is outlined; Crop Marks become a group of the object and its marks; a group's or layer's own fills and strokes become objects among its members, which are expanded too. Hidden fills, strokes and effects are dropped. Enabled when a selected or targeted object's appearance isn't basic (`ids` then pick which objects) → {ids}",
|
||||
can_expand,
|
||||
expand_appearance
|
||||
),
|
||||
|
|
@ -119,10 +119,14 @@ fn edit_effects(
|
|||
pub(crate) fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "effect.apply";
|
||||
let id = str_param(p, "effect").or_else(|| str_param(p, "id")).ok_or_else(|| bad(C, "missing `effect` id"))?;
|
||||
let params = p.get("params").cloned().unwrap_or(Value::Null);
|
||||
let mut params = p.get("params").cloned().unwrap_or(Value::Null);
|
||||
if !params.is_null() && !params.is_object() {
|
||||
return Err(bad(C, "`params` must be an object"));
|
||||
}
|
||||
// Crop Marks take their style from the preferences when applied.
|
||||
if id == effects::CROP_MARKS && params.get("style").is_none() {
|
||||
params["style"] = json!(s.crop_mark_style().id());
|
||||
}
|
||||
let effect = effects::new_effect(id, ¶ms).ok_or_else(|| bad(C, format!("unknown effect `{id}`")))?;
|
||||
let label = effects::effect_info(id).map(|e| e.label.trim_end_matches('…').to_string()).unwrap_or_default();
|
||||
let item = item_target(s, p, C)?;
|
||||
|
|
@ -370,6 +374,22 @@ fn expand_node(d: &mut Document, id: NodeId, brushes: &[vectorcraft_brush::Brush
|
|||
if !expandable(&n) {
|
||||
return;
|
||||
}
|
||||
// Crop Marks: a group of the object and its marks, then the object expands on its own.
|
||||
if let Some(mut m) = effects::crop_marks_art(&n) {
|
||||
let mut seen = std::collections::HashSet::from([id]);
|
||||
for c in m.children_mut().into_iter().flatten() {
|
||||
effects::fresh_ids(d, Arc::make_mut(c), &mut seen);
|
||||
}
|
||||
let object = m.children().and_then(|c| c.first()).map(|c| c.id);
|
||||
if let Some(slot) = d.node_mut(id) {
|
||||
*slot = m;
|
||||
out.push(id);
|
||||
}
|
||||
if let Some(object) = object {
|
||||
expand_node(d, object, brushes, out);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let image = raster_image(d, &n);
|
||||
let mut v = n.clone();
|
||||
strip_raster(&mut v, false);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ use std::sync::Arc;
|
|||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::marks::{MarkStyle, TrimMarks};
|
||||
use vectorcraft_doc::{Appearance, ImageObject, LiveShape, Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::{Affine, Anchor, PathData, Point, Rect, Vec2, shapes};
|
||||
use vectorcraft_geom::{Affine, Anchor, PathData, Point, Rect, Vec2};
|
||||
|
||||
use super::edit::{duplicate_in, selected_roots};
|
||||
use super::*;
|
||||
|
|
@ -115,7 +116,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Create Trim Marks",
|
||||
["Object"],
|
||||
None,
|
||||
"{offset?: pt (9), length?: pt (18), weight?: pt (0.3)} trim marks around the selection (or the first artboard) as a group → {id}",
|
||||
"{style?: \"roman\"|\"japanese\" (default: the japaneseCropMarks preference; japanese: double lines at the trim and bleed edges plus centre marks), allArtboards?: bool (default: true when nothing is selected), offset?: pt (9, roman), length?: pt (18), bleed?: pt (8.5, japanese), weight?: pt (0.3)} trim marks stroked in [Registration] (printing on every plate) around the selection, or around every artboard, one group each, as one undo step → {id: the first group, ids}",
|
||||
has_doc,
|
||||
trim_marks
|
||||
),
|
||||
|
|
@ -518,47 +519,48 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
// ---------- Trim marks ----------
|
||||
|
||||
fn trim_marks(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let off = f64_or(p, "offset", 9.0).max(0.0);
|
||||
let len = f64_or(p, "length", 18.0).max(0.1);
|
||||
let weight = f64_or(p, "weight", 0.3).max(0.01);
|
||||
let r = if st.selection.is_empty() {
|
||||
st.doc.artboards.first().map(|a| a.rect)
|
||||
} else {
|
||||
let roots = selected_roots(s)?;
|
||||
s.doc()?.doc.bounds_of(&roots, true)
|
||||
impl Session {
|
||||
/// The trim mark style the preferences ask for (General → Use Japanese Crop Marks).
|
||||
pub fn crop_mark_style(&self) -> MarkStyle {
|
||||
if self.prefs.japanese_crop_marks { MarkStyle::Japanese } else { MarkStyle::Roman }
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_marks(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "object.createTrimMarks";
|
||||
let style = match str_param(p, "style") {
|
||||
Some(v) => MarkStyle::parse(v).ok_or_else(|| bad(C, "`style` must be roman or japanese"))?,
|
||||
None => s.crop_mark_style(),
|
||||
};
|
||||
let d = TrimMarks::default();
|
||||
let marks = TrimMarks {
|
||||
style,
|
||||
offset: f64_or(p, "offset", d.offset).max(0.0),
|
||||
length: f64_or(p, "length", d.length).max(0.1),
|
||||
bleed: f64_or(p, "bleed", d.bleed).max(0.0),
|
||||
weight: f64_or(p, "weight", d.weight).max(0.01),
|
||||
};
|
||||
let st = s.doc()?;
|
||||
let rects: Vec<Rect> = if bool_or(p, "allArtboards", st.selection.is_empty()) {
|
||||
st.doc.artboards.iter().map(|a| a.rect).collect()
|
||||
} else {
|
||||
st.doc.bounds_of(&selected_roots(s)?, true).into_iter().collect()
|
||||
};
|
||||
if rects.is_empty() {
|
||||
return Err(bad(C, "nothing to mark"));
|
||||
}
|
||||
.ok_or_else(|| EngineError::Other("nothing to mark".into()))?;
|
||||
let parent = s.doc()?.insertion_parent();
|
||||
let lines = [
|
||||
// Top-left.
|
||||
(Point::new(r.x0 - off - len, r.y0), Point::new(r.x0 - off, r.y0)),
|
||||
(Point::new(r.x0, r.y0 - off - len), Point::new(r.x0, r.y0 - off)),
|
||||
// Top-right.
|
||||
(Point::new(r.x1 + off, r.y0), Point::new(r.x1 + off + len, r.y0)),
|
||||
(Point::new(r.x1, r.y0 - off - len), Point::new(r.x1, r.y0 - off)),
|
||||
// Bottom-right.
|
||||
(Point::new(r.x1 + off, r.y1), Point::new(r.x1 + off + len, r.y1)),
|
||||
(Point::new(r.x1, r.y1 + off), Point::new(r.x1, r.y1 + off + len)),
|
||||
// Bottom-left.
|
||||
(Point::new(r.x0 - off - len, r.y1), Point::new(r.x0 - off, r.y1)),
|
||||
(Point::new(r.x0, r.y1 + off), Point::new(r.x0, r.y1 + off + len)),
|
||||
];
|
||||
let id = s.edit("Create Trim Marks", |d, sel| {
|
||||
let mut children = vec![];
|
||||
for (a, b) in lines {
|
||||
let id = d.alloc_id();
|
||||
children.push(Arc::new(Node::path(id, shapes::line(a, b), Appearance::basic(Paint::None, Paint::solid(Color::BLACK), weight))));
|
||||
let ids = s.edit("Create Trim Marks", |d, sel| {
|
||||
let mut ids = vec![];
|
||||
for r in &rects {
|
||||
let g = marks.group(*r, &mut || d.alloc_id());
|
||||
ids.push(g.id);
|
||||
d.insert(parent, usize::MAX, g)?;
|
||||
}
|
||||
let gid = d.alloc_id();
|
||||
let mut g = Node::group(gid, children);
|
||||
g.name = Some("Trim Marks".into());
|
||||
d.insert(parent, usize::MAX, g)?;
|
||||
sel.set([gid]);
|
||||
Ok(gid)
|
||||
sel.set(ids.iter().copied());
|
||||
Ok(ids)
|
||||
})?;
|
||||
Ok(json!({ "id": id.0 }))
|
||||
Ok(json!({ "id": ids[0].0, "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() }))
|
||||
}
|
||||
|
||||
// ---------- Convert to Shape ----------
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Fill",
|
||||
[],
|
||||
None,
|
||||
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}, none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch is recorded as the gradient's swatch and fits each object, keeping its aspect), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|freeform, stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: global or spot colour (or tint) swatch the stop links to (its colour comes from the swatch), tint?: 0..100}] (at least 2; default white→black), angle?: deg, start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial; without start/end the gradient is placed on each object's bounds), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray)} sets the selection's fill and the default (new art fits a gradient to itself)",
|
||||
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}, none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch is recorded as the gradient's swatch and fits each object, keeping its aspect; the built-in \"[Registration]\" prints on every plate), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|freeform, stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: global or spot colour (or tint) swatch the stop links to (its colour comes from the swatch), tint?: 0..100}] (at least 2; default white→black), angle?: deg, start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial; without start/end the gradient is placed on each object's bounds), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray)} sets the selection's fill and the default (new art fits a gradient to itself)",
|
||||
has_doc,
|
||||
|s, p| set_paint(s, p, true)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -294,6 +294,8 @@ pub const PREF_SPECS: &[PrefSpec] = &[
|
|||
p!("overrideCharColor", "Type", "Graphic Styles", "Override Character Color", bool),
|
||||
// Object → Pattern → Tile Edge Color
|
||||
p!("patternTileEdgeColor", "Guides & Grid", "Pattern Editing", "Tile Edge Color", color),
|
||||
// Object → Create Trim Marks, Effect → Crop Marks
|
||||
p!("japaneseCropMarks", "General", "Options", "Use Japanese Crop Marks", bool),
|
||||
];
|
||||
|
||||
pub fn spec(key: &str) -> Option<&'static PrefSpec> {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Swatches",
|
||||
[],
|
||||
None,
|
||||
"{group?: name (only that colour group's swatches)} → {swatches: [{name, kind: \"none\"|\"color\"|\"gradient\"|\"pattern\", group, global, spot, color?, hex?, tintOf?: base swatch of a tint swatch, tint?: its %, gradient?, pattern?}], groups: [{name, swatches: [names]}]}",
|
||||
"{group?: name (only that colour group's swatches)} → {swatches: [{name, kind: \"none\"|\"color\"|\"gradient\"|\"pattern\", group, global, spot, color?, hex?, tintOf?: base swatch of a tint swatch, tint?: its %, gradient?, pattern?}] (the built-in [Registration] after None: it prints on every plate and can't be edited, moved or deleted), groups: [{name, swatches: [names]}]}",
|
||||
has_doc,
|
||||
swatch_list
|
||||
),
|
||||
|
|
@ -210,7 +210,7 @@ fn swatch_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Some(g) => g.swatches.iter().collect(),
|
||||
None => vec![d.swatch(n).ok_or_else(|| bad(C, format!("no swatch or colour group `{n}`")))?],
|
||||
};
|
||||
if swatches.iter().any(|w| w.paint.is_none()) {
|
||||
if swatches.iter().any(|w| w.is_reserved()) {
|
||||
return Err(bad(C, format!("`{n}` can't be deleted")));
|
||||
}
|
||||
global.extend(swatches.iter().filter(|w| w.global).map(|w| w.name.clone()));
|
||||
|
|
@ -355,7 +355,7 @@ fn swatch_move(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
for n in &names {
|
||||
let sw = d.swatch(n).ok_or_else(|| bad(C, format!("no swatch `{n}` (swatches and colour groups move separately)")))?;
|
||||
if sw.paint.is_none() {
|
||||
if sw.is_reserved() {
|
||||
return Err(bad(C, format!("`{n}` can't be moved")));
|
||||
}
|
||||
if group.is_some() && sw.paint.color().is_none() {
|
||||
|
|
@ -384,6 +384,9 @@ fn swatch_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let name = str_param(p, "name").ok_or_else(|| bad("swatch.duplicate", "missing name"))?.to_string();
|
||||
let new = s.edit("Duplicate Swatch", |d, _| {
|
||||
let src = d.swatch(&name).cloned().ok_or_else(|| EngineError::Other(format!("no swatch `{name}`")))?;
|
||||
if src.is_reserved() {
|
||||
return Err(bad("swatch.duplicate", format!("`{name}` can't be duplicated")));
|
||||
}
|
||||
let nm = d.free_swatch_name(&format!("{name} copy"));
|
||||
let copy = Swatch { name: nm.clone(), ..src };
|
||||
if let Some(pos) = d.swatches.iter().position(|sw| sw.name == name) {
|
||||
|
|
@ -480,7 +483,7 @@ fn swatch_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let name = str_param(p, "name").ok_or_else(|| bad(C, "missing `name`"))?.to_string();
|
||||
let d = &s.doc()?.doc;
|
||||
let sw = d.swatch(&name).cloned().ok_or_else(|| bad(C, format!("no swatch `{name}`")))?;
|
||||
if sw.paint.is_none() {
|
||||
if sw.is_reserved() {
|
||||
return Err(bad(C, format!("`{name}` can't be edited")));
|
||||
}
|
||||
let given = |k: &str| p.get(k).is_some_and(|v| !v.is_null());
|
||||
|
|
@ -567,7 +570,11 @@ fn swatch_list(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Some(g) => vec![d.swatch_groups.iter().find(|x| x.name == g).ok_or_else(|| bad("swatch.list", format!("no colour group `{g}`")))?],
|
||||
None => d.swatch_groups.iter().collect(),
|
||||
};
|
||||
let ungrouped = d.swatches.iter().filter(|_| only.is_none()).map(|w| swatch_json(w, None));
|
||||
// The built-in Registration swatch lists after None, as the panel shows it.
|
||||
let none = d.swatches.iter().take_while(|w| w.paint.is_none()).count();
|
||||
let (specials, rest) = d.swatches.split_at(none);
|
||||
let all = specials.iter().chain([vectorcraft_color::swatch::registration()]).chain(rest);
|
||||
let ungrouped = all.filter(|_| only.is_none()).map(|w| swatch_json(w, None));
|
||||
let grouped = groups.iter().flat_map(|g| g.swatches.iter().map(|w| swatch_json(w, Some(&g.name))));
|
||||
let swatches: Vec<Value> = ungrouped.chain(grouped).collect();
|
||||
let groups: Vec<Value> =
|
||||
|
|
@ -653,7 +660,11 @@ fn swatch_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let names = names_param(p, C)?;
|
||||
let d = &s.doc()?.doc;
|
||||
for n in &names {
|
||||
if d.swatch(n).ok_or_else(|| bad(C, format!("no swatch `{n}`")))?.paint.color().is_none() {
|
||||
let sw = d.swatch(n).ok_or_else(|| bad(C, format!("no swatch `{n}`")))?;
|
||||
if sw.is_reserved() {
|
||||
return Err(bad(C, format!("`{n}` can't be merged")));
|
||||
}
|
||||
if sw.paint.color().is_none() {
|
||||
return Err(bad(C, format!("`{n}` isn't a solid colour")));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -338,6 +338,8 @@ pub struct Prefs {
|
|||
/// Preferences dialog field, so it has no [`cmd::prefscmds::PREF_SPECS`] row.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub width_profiles: Vec<vectorcraft_doc::SavedProfile>,
|
||||
/// General → Use Japanese Crop Marks: the style of Create Trim Marks and Effect → Crop Marks.
|
||||
pub japanese_crop_marks: bool,
|
||||
}
|
||||
|
||||
impl Default for Prefs {
|
||||
|
|
@ -458,6 +460,7 @@ impl Default for Prefs {
|
|||
eyedropper: Default::default(),
|
||||
flattener_presets: vec![],
|
||||
width_profiles: vec![],
|
||||
japanese_crop_marks: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -870,6 +873,8 @@ mod tests_prefs;
|
|||
#[cfg(test)]
|
||||
mod tests_proxyitems;
|
||||
#[cfg(test)]
|
||||
mod tests_registration;
|
||||
#[cfg(test)]
|
||||
mod tests_strokegeom;
|
||||
#[cfg(test)]
|
||||
mod tests_strokereach;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
//! The built-in [Registration] swatch (every plate, PDF `/Separation /All`), Object → Create Trim
|
||||
//! Marks (Registration, every artboard, Japanese marks) and Effect → Crop Marks.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_color::swatch::REGISTRATION;
|
||||
use vectorcraft_doc::{Document, Node};
|
||||
use vectorcraft_geom::Affine;
|
||||
use vectorcraft_render::proof::ProofSetup;
|
||||
use vectorcraft_render::{RenderOptions, Renderer};
|
||||
|
||||
use super::*;
|
||||
use crate::tests_colormgmt::GLOBAL;
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
s.execute(id, &p).unwrap()
|
||||
}
|
||||
|
||||
fn id_of(v: &Value) -> NodeId {
|
||||
NodeId(v["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
fn node(s: &Session, id: NodeId) -> Node {
|
||||
s.doc().unwrap().doc.node(id).unwrap().clone()
|
||||
}
|
||||
|
||||
/// The 4× render of `doc` (400×400 px), on white, showing `plates` only when given.
|
||||
fn render(doc: &Document, plates: Option<&[&str]>) -> vectorcraft_render::Rendered {
|
||||
let proof = plates.map(|p| ProofSetup { separations: Some(p.iter().map(|n| n.to_string()).collect()), ..Default::default() });
|
||||
let mut r = Renderer::new();
|
||||
r.threads = 0;
|
||||
r.render(doc, 400, 400, Affine::scale(4.0), &RenderOptions { background: Some([255, 255, 255, 255]), proof, ..Default::default() })
|
||||
}
|
||||
|
||||
fn dark(p: [u8; 4]) -> bool {
|
||||
p[0] < 90 && p[1] < 90 && p[2] < 90
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registration_is_a_reserved_swatch() {
|
||||
let mut s = session();
|
||||
let a = id_of(&run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})));
|
||||
run(&mut s, "paint.setStroke", json!({"swatch": REGISTRATION}));
|
||||
let st = node(&s, a).appearance.stroke().unwrap().paint.clone();
|
||||
assert!(st.is_registration() && st == Paint::registration(), "{st:?}");
|
||||
// Listed after None, global; it can't be deleted, edited, moved, duplicated or merged.
|
||||
let list = run(&mut s, "swatch.list", json!({}));
|
||||
assert_eq!(list["swatches"][0]["kind"], "none");
|
||||
assert_eq!((list["swatches"][1]["name"].clone(), list["swatches"][1]["global"].clone()), (json!(REGISTRATION), json!(true)));
|
||||
for (cmd, p) in [
|
||||
("swatch.delete", json!({"name": REGISTRATION})),
|
||||
("swatch.edit", json!({"name": REGISTRATION, "color": "#ff0000"})),
|
||||
("swatch.move", json!({"name": REGISTRATION, "to": 5})),
|
||||
("swatch.duplicate", json!({"name": REGISTRATION})),
|
||||
("swatch.merge", json!({"names": ["White", REGISTRATION]})),
|
||||
("swatch.new", json!({"name": REGISTRATION, "color": "#ff0000"})),
|
||||
] {
|
||||
let r = s.execute(cmd, &p);
|
||||
assert!(r.is_err() || cmd == "swatch.new", "{cmd}");
|
||||
if cmd == "swatch.new" {
|
||||
assert_ne!(r.unwrap()["name"], REGISTRATION, "the name is taken");
|
||||
}
|
||||
}
|
||||
assert!(s.doc().unwrap().doc.swatch(REGISTRATION).is_some());
|
||||
// Unused swatches never list it.
|
||||
assert!(!run(&mut s, "swatch.unused", json!({}))["names"].as_array().unwrap().iter().any(|n| n == REGISTRATION));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_marks_print_on_every_plate() {
|
||||
let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Spot Ink", "color": {"c": 0, "m": 60, "y": 90, "k": 0}, "global": true}));
|
||||
run(&mut s, "swatch.setSpot", json!({"name": "Spot Ink"}));
|
||||
run(&mut s, "shape.rectangle", json!({"x": 30, "y": 30.125, "width": 40, "height": 40}));
|
||||
run(&mut s, "paint.setStroke", json!({"none": true}));
|
||||
let g = id_of(&run(&mut s, "object.createTrimMarks", json!({"weight": 1})));
|
||||
let n = node(&s, g);
|
||||
assert_eq!(n.children().unwrap().len(), 8);
|
||||
assert!(n.children().unwrap().iter().all(|c| c.appearance.stroke().unwrap().paint.is_registration()));
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
// The top-left mark runs from x = 21 to 3 at y = 30.125: 100% ink on each plate.
|
||||
for plate in ["Cyan", "Magenta", "Yellow", "Black", "Spot Ink"] {
|
||||
let p = render(&doc, Some(&[plate])).pixel(48, 120);
|
||||
assert!(dark(p), "{plate}: {p:?}");
|
||||
}
|
||||
// A plain black mark would print on the black plate only.
|
||||
run(&mut s, "paint.setStroke", json!({"ids": [n.children().unwrap()[0].id.0], "color": {"c": 0, "m": 0, "y": 0, "k": 100}}));
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
assert!(!dark(render(&doc, Some(&["Cyan"])).pixel(48, 120)));
|
||||
assert!(!dark(render(&doc, Some(&["Spot Ink"])).pixel(48, 120)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_writes_registration_as_separation_all() {
|
||||
let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut s = session();
|
||||
run(&mut s, "shape.rectangle", json!({"x": 30, "y": 30, "width": 40, "height": 40}));
|
||||
run(&mut s, "object.createTrimMarks", json!({}));
|
||||
let bytes =
|
||||
vectorcraft_pdf::export(&s.doc().unwrap().doc, &vectorcraft_pdf::PdfOptions { compress: false, created: Some(0), ..Default::default() })
|
||||
.unwrap();
|
||||
let pdf = String::from_utf8_lossy(&bytes);
|
||||
assert!(pdf.contains("/Separation/All/DeviceCMYK"), "Registration prints on every plate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_marks_cover_every_artboard_and_take_the_japanese_style() {
|
||||
let mut s = session();
|
||||
run(&mut s, "artboard.new", json!({"x": 200, "y": 0, "width": 100, "height": 100}));
|
||||
let r = run(&mut s, "object.createTrimMarks", json!({}));
|
||||
let ids = r["ids"].as_array().unwrap().clone();
|
||||
assert_eq!(ids.len(), 2, "one group per artboard when nothing is selected");
|
||||
let b = s.doc().unwrap().doc.bounds_of(&[NodeId(ids[1].as_u64().unwrap())], false).unwrap();
|
||||
assert!(b.x0 < 200.0 && b.x0 > 150.0 && b.x1 > 300.0, "{b:?}");
|
||||
assert_eq!(s.doc().unwrap().selection.objects.len(), 2);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(s.doc().unwrap().doc.layers[0].children().unwrap().is_empty(), "one undo step");
|
||||
// Japanese marks: double lines at the trim and bleed edges, and centre marks.
|
||||
run(&mut s, "prefs.set", json!({"key": "japaneseCropMarks", "value": true}));
|
||||
assert_eq!(s.crop_mark_style(), vectorcraft_doc::marks::MarkStyle::Japanese);
|
||||
assert!(s.execute("object.createTrimMarks", &json!({"allArtboards": false})).is_err(), "nothing selected to mark");
|
||||
let r = run(&mut s, "object.createTrimMarks", json!({}));
|
||||
let first = node(&s, NodeId(r["ids"][0].as_u64().unwrap()));
|
||||
let lines = first.children().unwrap();
|
||||
assert_eq!(lines.len(), 24);
|
||||
// At the top-left corner of the first artboard: horizontal lines at y = 0 and y = -bleed.
|
||||
let ys: Vec<f64> = lines[..4].iter().filter_map(|l| l.path_data().and_then(|p| p.bounds())).filter(|b| b.height() < 1e-9).map(|b| b.y0).collect();
|
||||
assert_eq!(ys.len(), 2);
|
||||
assert!(ys.iter().any(|y| y.abs() < 1e-9) && ys.iter().any(|y| (y + 3.0 * 72.0 / 25.4).abs() < 1e-6), "{ys:?}");
|
||||
// An explicit style wins over the preference.
|
||||
let r = run(&mut s, "object.createTrimMarks", json!({"style": "roman"}));
|
||||
assert_eq!(node(&s, NodeId(r["ids"][0].as_u64().unwrap())).children().unwrap().len(), 8);
|
||||
assert!(s.execute("object.createTrimMarks", &json!({"style": "western"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crop_marks_effect_follows_the_object() {
|
||||
let mut s = session();
|
||||
let a = id_of(&run(&mut s, "shape.rectangle", json!({"x": 30, "y": 30.125, "width": 40, "height": 40})));
|
||||
run(&mut s, "paint.setStroke", json!({"none": true}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "cropMarks"}));
|
||||
let fx = node(&s, a).appearance.effects[0].clone();
|
||||
assert_eq!((fx.id.as_str(), fx.params["style"].as_str()), ("cropMarks", Some("roman")), "the style comes from the preferences");
|
||||
// The marks render outside the object (0.3 pt at 4×: about a pixel).
|
||||
let mark = |s: &Session, x: u32| render(&s.doc().unwrap().doc, None).pixel(x, 120);
|
||||
assert!(mark(&s, 48)[0] < 200, "{:?}", mark(&s, 48));
|
||||
// Moved, the marks move with it.
|
||||
run(&mut s, "object.move", json!({"dx": 10, "dy": 0}));
|
||||
assert_eq!(mark(&s, 48)[0], 255, "nothing left behind");
|
||||
assert!(mark(&s, 88)[0] < 200);
|
||||
// SVG export and Expand Appearance draw the marks as paths.
|
||||
let svg = vectorcraft_svg::export(&s.doc().unwrap().doc, &Default::default());
|
||||
assert_eq!(svg.matches("<path").count(), 9, "{svg}");
|
||||
run(&mut s, "effect.expandAppearance", json!({}));
|
||||
let n = node(&s, a);
|
||||
assert!(n.appearance.effects.is_empty() && n.children().unwrap().len() == 2, "a group of the object and its marks");
|
||||
assert_eq!(n.children().unwrap()[1].children().unwrap().len(), 8);
|
||||
// Japanese crop marks.
|
||||
let b = id_of(&run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})));
|
||||
run(&mut s, "effect.apply", json!({"ids": [b.0], "effect": "cropMarks", "params": {"style": "japanese"}}));
|
||||
let art = vectorcraft_render::effects::crop_marks_art(&node(&s, b)).unwrap();
|
||||
assert_eq!(art.children().unwrap()[1].children().unwrap().len(), 24);
|
||||
}
|
||||
|
|
@ -204,7 +204,7 @@ fn swatch_list_reports_kinds_and_groups() {
|
|||
let mut s = session();
|
||||
let all = run(&mut s, "swatch.list", json!({}));
|
||||
let list = all["swatches"].as_array().unwrap();
|
||||
assert_eq!(list.len(), doc(&s).swatches_iter().count());
|
||||
assert_eq!(list.len(), doc(&s).swatches_iter().count() + 1, "and the built-in [Registration]");
|
||||
let find = |n: &str| list.iter().find(|w| w["name"] == n).unwrap().clone();
|
||||
assert_eq!(find("[None]")["kind"], "none");
|
||||
assert_eq!(find("Red")["kind"], "color");
|
||||
|
|
|
|||
|
|
@ -253,8 +253,13 @@ impl Exporter<'_> {
|
|||
Some(SeparationSpace::new(SeparationColorant::Custom(sw.name.clone()), alt))
|
||||
}
|
||||
|
||||
/// A solid paint, in the Separation colour space at its tint when it's linked to a spot swatch.
|
||||
/// A solid paint, in the Separation colour space at its tint when it's linked to a spot swatch
|
||||
/// or to Registration (`/All`: every plate).
|
||||
fn solid(&mut self, c: &Color, link: Option<&str>, tint: f32) -> krilla::color::Color {
|
||||
if link == Some(vectorcraft_color::swatch::REGISTRATION) {
|
||||
let alt = krilla::color::RegularColor::Cmyk(cmyk::Color::new(255, 255, 255, 255));
|
||||
return SepColor::new(q(tint), SeparationSpace::new(SeparationColorant::AllColorants, alt)).into();
|
||||
}
|
||||
match link.and_then(|n| self.separation(n)) {
|
||||
Some(space) => SepColor::new(q(tint), space).into(),
|
||||
None => self.col(c),
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ pub(crate) fn has_fx(n: &Node) -> bool {
|
|||
}
|
||||
|
||||
/// Does `n` carry visible object-level effects that only apply through its art (type, images,
|
||||
/// symbol instances, live objects), or is it a group or layer with an appearance of its own (see
|
||||
/// [`Renderer::draw_object_fx`])?
|
||||
/// symbol instances, live objects; Crop Marks on anything), or is it a group or layer with an
|
||||
/// appearance of its own (see [`Renderer::draw_object_fx`])?
|
||||
pub(crate) fn has_object_fx(n: &Node) -> bool {
|
||||
(effects::needs_outline(n) && visible(&n.appearance.effects)) || effects::has_container_appearance(n)
|
||||
(effects::needs_outline(n) && visible(&n.appearance.effects)) || effects::has_container_appearance(n) || effects::has_crop_marks(n)
|
||||
}
|
||||
|
||||
/// Is `n` a group or layer (whose evaluated art keeps its knockout setting)?
|
||||
|
|
@ -136,6 +136,8 @@ pub(crate) fn visual_bounds(n: &Node) -> Option<Rect> {
|
|||
/// the subtree (a shadow can reach the view while its object is outside it).
|
||||
pub(crate) fn cull_bounds(n: &Node) -> Option<Rect> {
|
||||
match &n.kind {
|
||||
// The object and its crop marks.
|
||||
_ if effects::has_crop_marks(n) => effects::crop_marks_art(n).and_then(|art| cull_bounds(&art)),
|
||||
// The evaluated art (its fills, strokes and geometry effects) and the raster effects' reach.
|
||||
_ if effects::has_container_appearance(n) => {
|
||||
let art = effects::evaluate_container(n);
|
||||
|
|
@ -378,7 +380,8 @@ impl Renderer {
|
|||
/// What [`Self::draw_object_fx`] draws inside the object's transparency group.
|
||||
fn draw_object_fx_art(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>, cache: bool) {
|
||||
let art = self.fx_art(f.doc, a, cache);
|
||||
let rfx = effects::raster_effects(&a.appearance.effects);
|
||||
// With Crop Marks, the object inside the art paints its own raster effects.
|
||||
let rfx = if effects::has_crop_marks(a) { vec![] } else { effects::raster_effects(&a.appearance.effects) };
|
||||
match cull_bounds(&art) {
|
||||
Some(reach) if !f.opts.outline && !rfx.is_empty() => {
|
||||
// Feather and Inner Glow clip to the art's outline.
|
||||
|
|
@ -413,7 +416,7 @@ impl Renderer {
|
|||
_ => None,
|
||||
};
|
||||
let container = is_container(a);
|
||||
let art = match effects::reshape(a, symbol.as_ref()) {
|
||||
let art = match effects::crop_marks_art(a).or_else(|| effects::reshape(a, symbol.as_ref())) {
|
||||
Some(r) => r,
|
||||
None if container => effects::evaluate_container(a).unwrap_or_else(|| (**a).clone()),
|
||||
None if matches!(a.kind, NodeKind::Text(_) | NodeKind::Image(_)) => (**a).clone(),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
//! into process inks (colour-managed), or a spot ink at the colour's tint when it's linked to a
|
||||
//! spot swatch (solid colours and gradient stops alike). With one plate visible the plate
|
||||
//! renders as greyscale ink coverage (black = 100%); with several, the visible inks are
|
||||
//! composited through the proof profile, spots multiplied on top.
|
||||
//! composited through the proof profile, spots multiplied on top. Colours linked to the
|
||||
//! Registration swatch print their tint on every plate, spot plates included.
|
||||
//! Placed raster images are not separated (limitation).
|
||||
//! * **Overprint Preview** (and Separations Preview, which implies it): fills and strokes that
|
||||
//! overprint ([`vectorcraft_doc::FillLayer::overprint`], characters' too) are drawn with
|
||||
|
|
@ -21,6 +22,7 @@ use std::sync::{Arc, RwLock};
|
|||
|
||||
use vectorcraft_color::cms::{self, Cms, PROCESS_PLATES};
|
||||
pub use vectorcraft_color::cms::{Intent, ProofSetup, ProofTarget};
|
||||
use vectorcraft_color::swatch::REGISTRATION;
|
||||
use vectorcraft_color::{BlendMode, Color, Paint};
|
||||
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeKind};
|
||||
|
||||
|
|
@ -33,6 +35,19 @@ pub struct Inks {
|
|||
pub cmyk: [f32; 4],
|
||||
/// Spot ink (swatch name, tint 0..1).
|
||||
pub spot: Option<(String, f32)>,
|
||||
/// Registration: the tint (every process ink's) prints on every spot plate too.
|
||||
pub registration: bool,
|
||||
}
|
||||
|
||||
impl Inks {
|
||||
/// The tint this colour prints on spot plate `name`.
|
||||
pub fn spot_tint(&self, name: &str) -> f32 {
|
||||
match &self.spot {
|
||||
_ if self.registration => self.cmyk[0],
|
||||
Some((n, t)) if n == name => *t,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A printing plate.
|
||||
|
|
@ -77,14 +92,17 @@ fn spot_swatch<'a>(doc: &'a Document, name: &str) -> Option<&'a Color> {
|
|||
pub type Link<'a> = Option<(&'a str, f32)>;
|
||||
|
||||
/// Separate one colour into inks. A colour linked to a spot swatch prints on that plate only, at
|
||||
/// its tint.
|
||||
/// its tint; one linked to the Registration swatch prints its tint on every plate.
|
||||
pub fn inks(doc: &Document, c: &Cms, color: &Color, link: Link, intent: Intent) -> Inks {
|
||||
if let Some((REGISTRATION, tint)) = link {
|
||||
return Inks { cmyk: [tint.clamp(0.0, 1.0); 4], spot: None, registration: true };
|
||||
}
|
||||
if let Some((name, tint)) = link
|
||||
&& spot_swatch(doc, name).is_some()
|
||||
{
|
||||
return Inks { cmyk: [0.0; 4], spot: Some((name.to_string(), tint.clamp(0.0, 1.0))) };
|
||||
return Inks { cmyk: [0.0; 4], spot: Some((name.to_string(), tint.clamp(0.0, 1.0))), registration: false };
|
||||
}
|
||||
Inks { cmyk: c.to_cmyk(color, intent), spot: None }
|
||||
Inks { cmyk: c.to_cmyk(color, intent), spot: None, registration: false }
|
||||
}
|
||||
|
||||
fn map_paint(p: &mut Paint, f: &mut dyn FnMut(&Color, Link) -> Color) {
|
||||
|
|
@ -169,7 +187,7 @@ fn plate_color(doc: &Document, c: &Cms, proof: &ProofSetup, visible: &[String],
|
|||
let name = &visible[0];
|
||||
let v = match PROCESS_PLATES.iter().position(|p| p == name) {
|
||||
Some(i) => ink.cmyk[i],
|
||||
None => ink.spot.as_ref().filter(|(n, _)| n == name).map_or(0.0, |s| s.1),
|
||||
None => ink.spot_tint(name),
|
||||
};
|
||||
[1.0 - v; 3]
|
||||
} else {
|
||||
|
|
@ -180,13 +198,15 @@ fn plate_color(doc: &Document, c: &Cms, proof: &ProofSetup, visible: &[String],
|
|||
}
|
||||
}
|
||||
let mut rgb = c.proof_cmyk_to_srgb(cmyk, proof);
|
||||
if let Some((name, t)) = &ink.spot
|
||||
&& visible.contains(name)
|
||||
&& let Some(sc) = spot_swatch(doc, name)
|
||||
{
|
||||
let s = c.display_rgb(sc);
|
||||
for i in 0..3 {
|
||||
rgb[i] *= 1.0 - t * (1.0 - s[i]);
|
||||
for name in visible.iter().filter(|v| !PROCESS_PLATES.contains(&v.as_str())) {
|
||||
let t = ink.spot_tint(name);
|
||||
if t > 0.0
|
||||
&& let Some(sc) = spot_swatch(doc, name)
|
||||
{
|
||||
let s = c.display_rgb(sc);
|
||||
for i in 0..3 {
|
||||
rgb[i] *= 1.0 - t * (1.0 - s[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
rgb
|
||||
|
|
|
|||
|
|
@ -1848,9 +1848,15 @@ fn effect_menu() -> Vec<Item> {
|
|||
Sep,
|
||||
Item::Header("Vector Effects"),
|
||||
];
|
||||
// Submenus in Illustrator's order.
|
||||
let order = ["3D and Materials", "Convert to Shape", "Distort & Transform", "Path", "Pathfinder", "Stylize", "SVG Filters", "Warp", "Blur"];
|
||||
// Submenus (and Crop Marks, an item of its own) in the reference app's order.
|
||||
let order =
|
||||
["3D and Materials", "Convert to Shape", "Crop Marks", "Distort & Transform", "Path", "Pathfinder", "Stylize", "SVG Filters", "Warp", "Blur"];
|
||||
let top_level = |e: &vectorcraft_effects::EffectInfo| e.menu == ["Effect"] && order.contains(&e.label);
|
||||
for sub_name in order {
|
||||
if let Some(e) = cat.iter().find(|e| top_level(e) && e.label == sub_name) {
|
||||
out.push(Item::Cmd(e.label, "effect.apply", json!({ "effect": e.id })));
|
||||
continue;
|
||||
}
|
||||
let items: Vec<Item> = cat
|
||||
.iter()
|
||||
.filter(|e| e.menu.last().copied() == Some(sub_name))
|
||||
|
|
@ -1880,7 +1886,7 @@ fn effect_menu() -> Vec<Item> {
|
|||
}
|
||||
}
|
||||
// Anything not placed above (future effects) still shows up.
|
||||
for e in cat.iter().filter(|e| !e.menu.last().is_some_and(|m| order.contains(m))) {
|
||||
for e in cat.iter().filter(|e| !top_level(e) && !e.menu.last().is_some_and(|m| order.contains(m))) {
|
||||
out.push(Item::Cmd(e.label, "effect.dialog", json!({ "effect": e.id })));
|
||||
}
|
||||
out
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use std::sync::Arc;
|
|||
use egui::{Color32, Rect, Response, Sense, Shape, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::libraries::{BuiltinLibrary, GRADIENT_LIBRARIES, SWATCH_LIBRARIES};
|
||||
use vectorcraft_color::swatch::REGISTRATION;
|
||||
use vectorcraft_color::{Color, GradientKind, Paint, Swatch, SwatchLibrary};
|
||||
use vectorcraft_doc::Document;
|
||||
use vectorcraft_engine::cmd::swatchlib;
|
||||
|
|
@ -106,8 +107,6 @@ impl Entry {
|
|||
}
|
||||
}
|
||||
|
||||
const REGISTRATION: &str = "[Registration]";
|
||||
|
||||
/// The panel's rows for the kind filter and the find field's `query` (a case-insensitive name
|
||||
/// match; a colour group whose name matches shows all its swatches).
|
||||
fn entries(app: &VectorcraftApp, kind: Kind, query: &str) -> Vec<Entry> {
|
||||
|
|
@ -148,7 +147,7 @@ fn entries(app: &VectorcraftApp, kind: Kind, query: &str) -> Vec<Entry> {
|
|||
/// The paint command params that apply entry `e` (`paint.setFill`); none for a colour group.
|
||||
fn swatch_params(e: &Entry) -> Option<Value> {
|
||||
Some(match e {
|
||||
Entry::Registration => json!({"color": {"c": 1.0, "m": 1.0, "y": 1.0, "k": 1.0}}),
|
||||
Entry::Registration => json!({"swatch": REGISTRATION}),
|
||||
Entry::Swatch { paint, .. } if paint.is_none() => json!({"none": true}),
|
||||
Entry::Swatch { name, .. } => json!({"swatch": name}),
|
||||
Entry::Folder(_) => return None,
|
||||
|
|
@ -369,10 +368,14 @@ fn held(resp: &Response) -> Option<std::sync::Arc<PanelDrag>> {
|
|||
resp.dnd_hover_payload::<PanelDrag>().filter(|d| matches!(**d, PanelDrag::Paint { .. }))
|
||||
}
|
||||
|
||||
/// The drag a tile or row of `e` starts, moving rows `names`. Registration and colour groups have
|
||||
/// no paint of their own (the chip under the pointer draws Registration's mark).
|
||||
/// The drag a tile or row of `e` starts, moving rows `names`. Colour groups have no paint of their
|
||||
/// own (the chip under the pointer draws Registration's mark for Registration).
|
||||
fn drag_of(e: &Entry, names: Vec<String>) -> PanelDrag {
|
||||
let paint = if let Entry::Swatch { paint, .. } = e { paint.clone() } else { Paint::None };
|
||||
let paint = match e {
|
||||
Entry::Swatch { paint, .. } => paint.clone(),
|
||||
Entry::Registration => Paint::registration(),
|
||||
Entry::Folder(_) => Paint::None,
|
||||
};
|
||||
let rows = SwatchRows { grabbed: e.name().to_string(), names, groups: e.is_folder() };
|
||||
PanelDrag::Paint { paint, params: swatch_params(e).unwrap_or_default(), rows: Some(rows) }
|
||||
}
|
||||
|
|
|
|||
15
docs/mcp.md
15
docs/mcp.md
|
|
@ -581,3 +581,18 @@ paths and compound paths, and `path.reverse {reversed?}` makes subpaths run coun
|
|||
{"name":"run_command","arguments":{"command":"attributes.set","params":{"ids":[12],"url":"https://example.com","imageMap":"rectangle"}}}
|
||||
{"name":"run_command","arguments":{"command":"path.setFillRule","params":{"rule":"evenOdd"}}}
|
||||
```
|
||||
|
||||
## Registration and trim marks
|
||||
|
||||
Every document has the built-in `[Registration]` swatch (listed after None by `swatch.list`): a colour that prints on
|
||||
every plate, process and spot. `paint.setStroke {swatch: "[Registration]"}` applies it; it can't be edited, moved,
|
||||
duplicated, merged or deleted. Separations Preview shows it on each plate and PDF export writes it as
|
||||
`/Separation /All`. `object.createTrimMarks {style?, allArtboards?}` draws trim marks in Registration around the
|
||||
selection, or around every artboard when nothing is selected; `effect.apply {effect: "cropMarks"}` adds live crop
|
||||
marks that follow the object. Both use Japanese marks (double lines at the trim and bleed edges, centre marks) when
|
||||
the preference `japaneseCropMarks` is on or `style: "japanese"` is given.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"prefs.set","params":{"key":"japaneseCropMarks","value":true}}}
|
||||
{"name":"run_command","arguments":{"command":"object.createTrimMarks","params":{}}}
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in New Issue