mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 04:07:12 -06:00
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:
co-authored by
Claude Opus 5.5
parent
4591c85845
commit
c1df6f0621
@@ -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 ----------
|
||||
|
||||
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 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)
|
||||
let rects: Vec<Rect> = if bool_or(p, "allArtboards", st.selection.is_empty()) {
|
||||
st.doc.artboards.iter().map(|a| a.rect).collect()
|
||||
} else {
|
||||
let roots = selected_roots(s)?;
|
||||
s.doc()?.doc.bounds_of(&roots, true)
|
||||
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")));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user