mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
M4.80: Print Tiling tool and View > Show Print Tiling
View > Show Print Tiling (view.printTiling) draws the pages of the document's print settings on the canvas, dashed and numbered, from print.preview's own layout (print.tiling), so the canvas and the printed job agree. The Print Tiling tool replaces the no-op tool: a drag places the first page's imageable corner (print.tiling.set, saved as print_setup.tileOrigin, one undo step); a double click on the canvas or the tool button resets it. Tiles start at the origin and extend to cover the art; the Print dialog shows the tool's placement with a Reset button. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9779d21962
commit
1b9b37454b
19 files changed
+739
-27
No files matched your search
+6
-6
@@ -33,7 +33,7 @@ since 2026-10-01.
|
||||
|
||||
| Dimension | Estimate | Evidence and what's missing |
|
||||
|---|---|---|
|
||||
| **Breadth:** menus, tools and panels exist | ~90% | 18 menu items still stubbed; every tool implemented except Touch Type, vertical type ×3 and Print Tiling; 51 panel modules |
|
||||
| **Breadth:** menus, tools and panels exist | ~90% | 18 menu items still stubbed; every tool implemented except Touch Type and vertical type ×3; 51 panel modules |
|
||||
| **Depth:** each feature behaves like Illustrator | ~68–74% | Strong: paint, appearance and colour (M3), Pathfinder and booleans, selection, drawing, files (M4). Weak: advanced type (~35%), brushes and symbols (in progress), raster effects (~20%) |
|
||||
| **Large missing subsystems** | 0–20% | 3D & Materials (0%), Photoshop-style raster effects and the Effect Gallery (~1 of ~56 filters), SVG Filters, vertical/CJK type, Variables (data merge), scripting |
|
||||
| **Interaction fidelity:** modifiers, cursors, small behaviours | ~30–40% | The dedicated pass hasn't started, and there has been no side-by-side session with Illustrator yet. A power user notices this first |
|
||||
@@ -64,7 +64,7 @@ Ordered by how much each gap blocks someone from switching. Sizes are one-agent
|
||||
5. **Brushes, symbols and libraries:** brush options depth; original brush, symbol and graphic-style libraries
|
||||
generated in code (never Adobe's); dynamic symbols; Start Global Edit. 14–22 h.
|
||||
6. **Automation:** Variables (data merge), a scripting surface over the command registry, batch. 8–12 h.
|
||||
7. **Views and windows:** multiple windows and arrange, Consolidate All Windows, the Print Tiling tool, video
|
||||
7. **Views and windows:** multiple windows and arrange, Consolidate All Windows, video
|
||||
rulers. 15–25 h.
|
||||
8. **Hardening at scale:** a corpus of real-world SVG/PDF/EPS files, idle-machine perf budgets, Windows, Linux and
|
||||
browser QA, accessibility, packaging. 50–80 h.
|
||||
@@ -128,7 +128,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- `.vectorcraft` (lossless JSON, compressed, atomic saves, save down to v1), Save As with a format chooser, Save a Copy, Revert, templates, Data Recovery after a crash, background save and export.
|
||||
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units; PDF and PDF-compatible `.ai` in and out (layers, masks, editable text, spot colours, presets, security, marks and bleed, ICC output intents, raster effects, subset fonts).
|
||||
- EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export.
|
||||
- Place and the Links panel, Package, File Info, Print with print presets and PostScript output, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours.
|
||||
- Place and the Links panel, Package, File Info, Print with print presets, PostScript output and print tiling, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours.
|
||||
- **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped.
|
||||
- **Tests and robustness:** ~2,840 automated tests: model-based property tests, a junk-parameter sweep over every command, import fuzzing (SVG, PDF, libraries), golden renders, and MCP end-to-end tests over stdio. Shipped code never panics (workspace lints and a rollback safety net; see `docs/development.md`).
|
||||
|
||||
@@ -147,7 +147,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | 🟡 live Blends, Envelopes (warp/mesh/top object), Width tool, Liquify tools, Puppet Warp and Perspective Grid landed; fidelity pass pending | 8–12 |
|
||||
| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder; SVG Filters, Document Raster Effects Settings, 3D pending | 86–135 |
|
||||
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 |
|
||||
| M11 | Artboards & views (artboard panel/tool done, Trim View, middle-button pan; print tiling, multiple windows, presentation polish) | 🟡 | 15–25 |
|
||||
| M11 | Artboards & views (artboard panel/tool, Trim View, middle-button pan and print tiling done; multiple windows, presentation polish) | 🟡 | 15–25 |
|
||||
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (12 presets, 18 ms/1k² image), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, regenerate in place) done; graph Design/Column/Marker designs pending | 6–10 |
|
||||
| M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 | 18–27 |
|
||||
| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) | 🟡 Preferences, shortcut editor, workspaces, a custom title bar on Windows/Linux and content-sized dialogs and menus done; accessibility, Windows/Linux packaging pending | 18–28 |
|
||||
@@ -183,7 +183,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
|
||||
| Symbols, blends, envelopes, Repeat, perspective | 5 | 75% | symbol libraries (original), dynamic symbols, perspective edge cases | 8–12 |
|
||||
| Image Trace, graphs, image tools | 3 | 70% | graph Design/Column/Marker, Create Object Mosaic, Crop Image polish | 6–10 |
|
||||
| Layers, artboards, document setup | 5 | 80% | Layers panel options depth, artboard presets/rearrange polish (Document Setup and New Document are done) | 5–8 |
|
||||
| View & navigation | 3 | 70% | New View/Edit Views, multiple windows/arrange, print tiling, Snap to Pixel/Glyph | 10–15 |
|
||||
| View & navigation | 3 | 70% | New View/Edit Views, multiple windows/arrange, Snap to Pixel/Glyph (print tiling is done) | 9–14 |
|
||||
| Guides, grids, smart guides, snapping, rulers | 3 | 75% | global/video rulers, smart-guide preference depth | 4–8 |
|
||||
| File formats | 6 | 85% | DWG (no open spec: DXF instead), PSD placement as layers, the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X and presets are done | 6–10 |
|
||||
| Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 |
|
||||
@@ -201,7 +201,7 @@ With 4–6 agents working on disjoint crates (as the layering allows) the wall-c
|
||||
**~85–150 h** to full parity.
|
||||
|
||||
_Inventories (2026-10-05):_ 18 menu items still stubbed (`todo(…)` in `crates/ui-egui/src/menus.rs`); every tool
|
||||
implemented except five (Touch Type, vertical type ×3, Print Tiling); 51 panel modules; Illustrator-style live
|
||||
implemented except four (Touch Type, vertical type ×3); 51 panel modules; Illustrator-style live
|
||||
effects ~44/54, Photoshop-style raster effects ~1/56, 3D 0/5; ~2,840 tests; ~221k lines of Rust.
|
||||
|
||||
_Where we already beat Illustrator:_ exact curve booleans, off-thread multithreaded rendering, undo that never runs out,
|
||||
|
||||
@@ -48,6 +48,7 @@ mod place;
|
||||
pub mod prefscmds;
|
||||
pub mod print;
|
||||
pub mod printpresets;
|
||||
pub mod printtiling;
|
||||
pub mod rasterfx;
|
||||
mod recolor;
|
||||
pub mod recovery;
|
||||
@@ -241,6 +242,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(assets::specs());
|
||||
v.extend(css::specs());
|
||||
v.extend(printpresets::specs());
|
||||
v.extend(printtiling::specs());
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Print Setup",
|
||||
[],
|
||||
None,
|
||||
"{settings?: {copies: 1 (1–999), collate: true, reverse, artboards: all|range|ignore (all the art as one page), range: \"1-3, 5\" (1-based, with artboards: range), skipBlank (leave out artboards with no art that prints), media: letter|legal|tabloid|a3|a4|a5|b4|b5|custom, width, height (custom paper, 72–14400 pt), orientation: portrait|landscape|portraitFlipped|landscapeFlipped, autoRotate: true (turn the paper to each artboard; orientation ignored), transverse (the page a quarter turn on the paper), printLayers: visiblePrintable|visible|all (template layers never print), placement: {origin: topLeft|top|topRight|left|center|right|bottomLeft|bottom|bottomRight (the point of the printed area, the artboard with its bleed and marks, on the same point of the imageable area), x, y (pt, right and down)}, scaling: none|fit|custom|tileFull|tileImageable, scale: {width: 100, height: 100} (% with custom and tiling), overlap: 0 (pt between tiles), tileRange: \"\" (1-based tiles across then down; empty: all), margin: 0 (pt the device can't print around the paper: the imageable area is inside it), marks: {trim, registration, colorBars, pageInfo, kind: roman|japanese, weight: 0.25, offset: 6} (as document.exportPdf, at the paper's scale; page information adds the tile and the ink), bleed: {useDocument: true (the document's bleed, document.setup), top, bottom, left, right} (pt), output: {mode: composite|separations (one grey page per ink that prints), emulsion: up|down (down mirrors), image: positive|negative, spotsToProcess, inks: [{name: \"Cyan\"|…|a spot swatch, print: true, frequency (lpi, default 60), angle (default C 15, M 75, Y 0, K 45, spots 45)}]}, graphics: {autoFlatness: true, flatness: 1 (0.2–100), fonts: none|subset|complete}, color: {intent: perceptual|relativeColorimetric|saturation|absoluteColorimetric, preserveNumbers: true (CMYK colours keep their values in separations)}}} store the print settings with the document, over the ones it has (null keeps a value), as one undo step; no settings → {settings} (the current ones, defaults if never set up)",
|
||||
"{settings?: {copies: 1 (1–999), collate: true, reverse, artboards: all|range|ignore (all the art as one page), range: \"1-3, 5\" (1-based, with artboards: range), skipBlank (leave out artboards with no art that prints), media: letter|legal|tabloid|a3|a4|a5|b4|b5|custom, width, height (custom paper, 72–14400 pt), orientation: portrait|landscape|portraitFlipped|landscapeFlipped, autoRotate: true (turn the paper to each artboard; orientation ignored), transverse (the page a quarter turn on the paper), printLayers: visiblePrintable|visible|all (template layers never print), placement: {origin: topLeft|top|topRight|left|center|right|bottomLeft|bottom|bottomRight (the point of the printed area, the artboard with its bleed and marks, on the same point of the imageable area), x, y (pt, right and down)}, scaling: none|fit|custom|tileFull|tileImageable, scale: {width: 100, height: 100} (% with custom and tiling), overlap: 0 (pt between tiles), tileRange: \"\" (1-based tiles across then down; empty: all), margin: 0 (pt the device can't print around the paper: the imageable area is inside it), marks: {trim, registration, colorBars, pageInfo, kind: roman|japanese, weight: 0.25, offset: 6} (as document.exportPdf, at the paper's scale; page information adds the tile and the ink), bleed: {useDocument: true (the document's bleed, document.setup), top, bottom, left, right} (pt), output: {mode: composite|separations (one grey page per ink that prints), emulsion: up|down (down mirrors), image: positive|negative, spotsToProcess, inks: [{name: \"Cyan\"|…|a spot swatch, print: true, frequency (lpi, default 60), angle (default C 15, M 75, Y 0, K 45, spots 45)}]}, graphics: {autoFlatness: true, flatness: 1 (0.2–100), fonts: none|subset|complete}, color: {intent: perceptual|relativeColorimetric|saturation|absoluteColorimetric, preserveNumbers: true (CMYK colours keep their values in separations)}, tileOrigin: {placed: false, x, y} (where the Print Tiling tool put the pages: print.tiling.set)}} store the print settings with the document, over the ones it has (null keeps a value), as one undo step; no settings → {settings} (the current ones, defaults if never set up)",
|
||||
has_doc,
|
||||
setup
|
||||
),
|
||||
@@ -53,7 +53,7 @@ fn saved(doc: &Document) -> Value {
|
||||
}
|
||||
|
||||
/// The settings of `doc` with `p.settings` over them, checked.
|
||||
fn settings(cmd: &str, doc: &Document, p: &Value) -> Result<PrintSettings> {
|
||||
pub(crate) fn settings(cmd: &str, doc: &Document, p: &Value) -> Result<PrintSettings> {
|
||||
settings_over(cmd, saved(doc), p)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ pub(crate) fn settings_over(cmd: &str, mut v: Value, p: &Value) -> Result<PrintS
|
||||
Ok(set)
|
||||
}
|
||||
|
||||
fn to_json(set: &PrintSettings) -> Result<Value> {
|
||||
pub(crate) fn to_json(set: &PrintSettings) -> Result<Value> {
|
||||
serde_json::to_value(set).map_err(|e| EngineError::Other(e.to_string()))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
//! View → Show Print Tiling (`view.printTiling`) and the Print Tiling tool (`print.tiling.set`):
|
||||
//! the pages of the document's print settings drawn on the canvas, from `print.preview`'s layout
|
||||
//! (`print.tiling`), and where the tool put them ([`vectorcraft_pdf::TileOrigin`], saved with
|
||||
//! the print settings).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Document;
|
||||
use vectorcraft_geom::Point;
|
||||
use vectorcraft_pdf::{TileOrigin, TilingPage};
|
||||
|
||||
use super::fileio::pdf::pdf_error;
|
||||
use super::print::{settings, to_json};
|
||||
use super::*;
|
||||
|
||||
const SET: &str = "print.tiling.set";
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
query "view.printTiling",
|
||||
"Show Print Tiling",
|
||||
["View"],
|
||||
None,
|
||||
"{on?: bool (default: toggle)} show the pages of the document's print settings on the canvas (each document has its own setting; the Print Tiling tool shows them too) → {on}",
|
||||
has_doc,
|
||||
show
|
||||
),
|
||||
cmd!(
|
||||
"print.tiling.set",
|
||||
"Print Tiling",
|
||||
[],
|
||||
None,
|
||||
"{origin: [x, y] (document point: the top-left corner of the first page's imageable area), artboard? (0-based: the artboard it is measured from; default the printed artboard containing origin, else the first; ignored with artboards: ignore)} | {reset: true} place the printed pages as the Print Tiling tool does, saved with the print settings (tileOrigin: {placed, x, y}, from the artboard's top-left corner; it replaces the placement, and tiles start there), as one undo step; reset → the placement places them again → {tileOrigin}",
|
||||
has_doc,
|
||||
set_origin
|
||||
),
|
||||
cmd!(
|
||||
query "print.tiling",
|
||||
"Print Tiling Pages",
|
||||
[],
|
||||
None,
|
||||
"{settings?: {…print.setup settings} (over the document's)} the pages as View → Show Print Tiling draws them, from print.preview's layout → {tileOrigin, pages: [{artboard (0-based; null with artboards ignored), number (1-based tile across then down, or page), printed (tiles outside tileRange don't), page: [x0, y0, x1, y1] (the paper's edge), imageable: [x0, y0, x1, y1] (inside the unprintable margin)}] (document space; every tile when tiling, else each page once)}",
|
||||
has_doc,
|
||||
tiling
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn show(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc_mut()?;
|
||||
st.print_tiling = p.get("on").and_then(Value::as_bool).unwrap_or(!st.print_tiling);
|
||||
Ok(json!({ "on": st.print_tiling }))
|
||||
}
|
||||
|
||||
/// The pages of `doc`'s print settings in document space (what View → Show Print Tiling draws);
|
||||
/// an error when they can't print.
|
||||
pub fn pages(doc: &Document) -> Result<Vec<TilingPage>> {
|
||||
pages_with(doc, &json!({}))
|
||||
}
|
||||
|
||||
fn pages_with(doc: &Document, p: &Value) -> Result<Vec<TilingPage>> {
|
||||
const C: &str = "print.tiling";
|
||||
let set = settings(C, doc, p)?;
|
||||
let pv = vectorcraft_pdf::preview(doc, &set).map_err(|e| pdf_error(C, e))?;
|
||||
Ok(vectorcraft_pdf::tiling(&pv, &set))
|
||||
}
|
||||
|
||||
fn tiling(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let doc = &s.doc()?.doc;
|
||||
let origin = settings("print.tiling", doc, p)?.tile_origin;
|
||||
Ok(json!({ "tileOrigin": origin, "pages": pages_with(doc, p)? }))
|
||||
}
|
||||
|
||||
/// `[x, y]` at `key` of `p`.
|
||||
fn point(p: &Value, key: &str) -> Option<Point> {
|
||||
let a = p.get(key)?.as_array()?;
|
||||
Some(Point::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?))
|
||||
}
|
||||
|
||||
fn set_origin(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let doc = &s.doc()?.doc;
|
||||
let current = settings(SET, doc, &json!({}))?;
|
||||
let mut set = current.clone();
|
||||
set.tile_origin = if bool_or(p, "reset", false) {
|
||||
TileOrigin::default()
|
||||
} else {
|
||||
let at = point(p, "origin").filter(|q| q.x.is_finite() && q.y.is_finite());
|
||||
let at = at.ok_or_else(|| bad(SET, "give origin: [x, y], or reset: true"))?;
|
||||
let regions = vectorcraft_pdf::print_regions(doc, &set).map_err(|e| pdf_error(SET, e))?;
|
||||
let picked = match p.get("artboard").filter(|v| !v.is_null()) {
|
||||
Some(v) => {
|
||||
let i = v.as_u64().ok_or_else(|| bad(SET, "artboard must be a 0-based index"))? as usize;
|
||||
regions.iter().find(|(a, _)| a.is_none() || *a == Some(i)).ok_or_else(|| bad(SET, format!("artboard {i} doesn't print")))?
|
||||
}
|
||||
None => regions.iter().find(|(_, r)| r.contains(at)).or(regions.first()).ok_or_else(|| bad(SET, "nothing prints"))?,
|
||||
};
|
||||
let corner = picked.1.origin();
|
||||
TileOrigin { placed: true, x: at.x - corner.x, y: at.y - corner.y }
|
||||
};
|
||||
set.check().map_err(|e| pdf_error(SET, e))?;
|
||||
if set != current {
|
||||
let stored = to_json(&set)?;
|
||||
s.edit("Print Tiling", |d, _| {
|
||||
d.print_setup = Some(stored);
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Ok(json!({ "tileOrigin": set.tile_origin }))
|
||||
}
|
||||
@@ -129,6 +129,8 @@ pub struct DocState {
|
||||
pub recovered: bool,
|
||||
/// The document's Data Recovery copy, once one was written ([`cmd::recovery`]).
|
||||
pub recovery: Option<cmd::recovery::RecoveryCopy>,
|
||||
/// View → Show Print Tiling, per document (view state: not saved, not undoable).
|
||||
pub print_tiling: bool,
|
||||
}
|
||||
|
||||
static NEXT_DOC_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
@@ -160,6 +162,7 @@ impl DocState {
|
||||
view,
|
||||
recovered: false,
|
||||
recovery: None,
|
||||
print_tiling: false,
|
||||
}
|
||||
}
|
||||
/// Unsaved changes: the document differs from the saved one (selection changes don't count).
|
||||
@@ -1221,6 +1224,8 @@ mod tests_printpreview;
|
||||
#[cfg(test)]
|
||||
mod tests_printps;
|
||||
#[cfg(test)]
|
||||
mod tests_printtiling;
|
||||
#[cfg(test)]
|
||||
mod tests_proxyitems;
|
||||
#[cfg(test)]
|
||||
mod tests_rastersettings;
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
//! View → Show Print Tiling (`view.printTiling`, `print.tiling`) and the Print Tiling tool
|
||||
//! (`print.tiling.set`): the pages drawn are print.preview's, and where the tool puts them is
|
||||
//! saved with the print settings.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
|
||||
use super::*;
|
||||
use crate::tooling::ViewInfo;
|
||||
|
||||
/// A 300 × 200 document with a rectangle.
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn rect(v: &Value) -> [f64; 4] {
|
||||
let a: Vec<f64> = v.as_array().unwrap().iter().map(|x| x.as_f64().unwrap()).collect();
|
||||
[a[0], a[1], a[2], a[3]]
|
||||
}
|
||||
|
||||
fn close(a: [f64; 4], b: [f64; 4]) -> bool {
|
||||
a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6)
|
||||
}
|
||||
|
||||
/// `t` (a print.preview transform) applied to `(x, y)`.
|
||||
fn apply(t: &Value, (x, y): (f64, f64)) -> (f64, f64) {
|
||||
let c: Vec<f64> = t.as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
|
||||
(c[0] * x + c[2] * y + c[4], c[1] * x + c[3] * y + c[5])
|
||||
}
|
||||
|
||||
fn tiling(s: &mut Session, settings: Value) -> Value {
|
||||
s.execute("print.tiling", &json!({ "settings": settings })).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_print_tiling_is_a_per_document_view_toggle() {
|
||||
let mut s = session();
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
assert_eq!(s.execute("view.printTiling", &json!({})).unwrap()["on"], true);
|
||||
assert!(s.doc().unwrap().print_tiling);
|
||||
assert_eq!(s.execute("view.printTiling", &json!({})).unwrap()["on"], false);
|
||||
assert_eq!(s.execute("view.printTiling", &json!({"on": true})).unwrap()["on"], true);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "a view toggle is no undo step");
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
assert!(!s.doc().unwrap().print_tiling, "each document has its own");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiling_pages_are_print_preview_tiles() {
|
||||
let mut s = session();
|
||||
let set = json!({"scaling": "tileImageable", "scale": {"width": 600, "height": 600}, "margin": 18, "overlap": 9, "tileRange": "1-2, 5"});
|
||||
let pv = s.execute("print.preview", &json!({ "settings": set })).unwrap();
|
||||
let grid = &pv["tiles"][0];
|
||||
let tiles = grid["tiles"].as_array().unwrap();
|
||||
let pages = tiling(&mut s, set.clone())["pages"].as_array().unwrap().clone();
|
||||
assert!(tiles.len() > 4);
|
||||
assert_eq!(pages.len(), tiles.len(), "one page per tile");
|
||||
for (i, (p, t)) in pages.iter().zip(tiles).enumerate() {
|
||||
assert_eq!(p["number"], i + 1);
|
||||
assert!(close(rect(&p["imageable"]), rect(t)), "tiles are imageable areas");
|
||||
let printed = grid["printed"].as_array().unwrap().contains(&json!(i + 1));
|
||||
assert_eq!(p["printed"], printed);
|
||||
// The paper reaches the margin (18 pt on paper, 3 pt of the document at 600%) further.
|
||||
let (pg, im) = (rect(&p["page"]), rect(&p["imageable"]));
|
||||
assert!((im[0] - pg[0] - 3.0).abs() < 1e-6 && (pg[3] - im[3] - 3.0).abs() < 1e-6);
|
||||
}
|
||||
// Full pages: the tiles are the paper, the imageable area inside it.
|
||||
let full = json!({"scaling": "tileFull", "scale": {"width": 400, "height": 400}, "margin": 18});
|
||||
let pv = s.execute("print.preview", &json!({ "settings": full })).unwrap();
|
||||
let pages = tiling(&mut s, full)["pages"].as_array().unwrap().clone();
|
||||
assert_eq!(pages.len(), pv["tiles"][0]["tiles"].as_array().unwrap().len());
|
||||
assert!(close(rect(&pages[0]["page"]), rect(&pv["tiles"][0]["tiles"][0])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn untiled_pages_come_from_the_sheets_once_per_page() {
|
||||
let mut s = session();
|
||||
s.execute("artboard.new", &json!({"x": 400, "y": 0, "width": 300, "height": 200})).unwrap();
|
||||
// Letter turned landscape for the wide artboards, centred on each.
|
||||
let set = json!({"output": {"mode": "separations"}, "margin": 36});
|
||||
let pv = s.execute("print.preview", &json!({ "settings": set })).unwrap();
|
||||
assert!(pv["sheets"].as_array().unwrap().len() >= 8, "a page per ink");
|
||||
let pages = tiling(&mut s, set)["pages"].as_array().unwrap().clone();
|
||||
assert_eq!(pages.len(), 2, "each page once, not once per ink");
|
||||
for (p, cx) in pages.iter().zip([150.0, 550.0]) {
|
||||
let pg = rect(&p["page"]);
|
||||
assert!(close(pg, [cx - 396.0, 100.0 - 306.0, cx + 396.0, 100.0 + 306.0]), "{pg:?}");
|
||||
assert!(close(rect(&p["imageable"]), [pg[0] + 36.0, pg[1] + 36.0, pg[2] - 36.0, pg[3] - 36.0]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_tool_puts_the_first_page_where_it_is_dragged_as_one_undo_step() {
|
||||
let mut s = session();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("printTiling", v).unwrap();
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 50.0, 50.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 45.0, 40.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 40.0, 30.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 40.0, 30.0), v).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.history.undo.len(), undo + 1);
|
||||
assert_eq!(st.history.undo.last().unwrap().label, "Print Tiling");
|
||||
let set = s.execute("print.setup", &json!({})).unwrap()["settings"].clone();
|
||||
assert_eq!(set["tileOrigin"], json!({"placed": true, "x": 40.0, "y": 30.0}));
|
||||
// The page's imageable corner is the point dragged to, on the paper and on the canvas.
|
||||
let pv = s.execute("print.preview", &json!({"settings": {"margin": 12}})).unwrap();
|
||||
let (x, y) = apply(&pv["sheets"][0]["transform"], (40.0, 30.0));
|
||||
assert!((x - 12.0).abs() < 1e-6 && (y - 12.0).abs() < 1e-6, "({x}, {y})");
|
||||
let page = &tiling(&mut s, json!({"margin": 12}))["pages"][0];
|
||||
assert!((rect(&page["imageable"])[0] - 40.0).abs() < 1e-6 && (rect(&page["imageable"])[1] - 30.0).abs() < 1e-6);
|
||||
// Saved in the native file.
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
assert_eq!(vectorcraft_format::load(&vectorcraft_format::save(doc, false)).unwrap().print_setup, doc.print_setup);
|
||||
// A double click puts the pages back where the placement puts them.
|
||||
s.pointer(&PointerEvent::new(PointerKind::DoubleClick, 10.0, 10.0), v).unwrap();
|
||||
assert_eq!(s.execute("print.setup", &json!({})).unwrap()["settings"]["tileOrigin"]["placed"], false);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("print.setup", &json!({})).unwrap()["settings"]["tileOrigin"]["placed"], true);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.print_setup.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiles_start_at_the_tile_origin_and_cover_the_art() {
|
||||
let mut s = session();
|
||||
let tiles = json!({"scaling": "tileImageable", "scale": {"width": 400, "height": 400}});
|
||||
s.execute("print.setup", &json!({ "settings": tiles })).unwrap();
|
||||
let before = tiling(&mut s, json!({}))["pages"].as_array().unwrap().len();
|
||||
s.execute("print.tiling.set", &json!({"origin": [60, 35]})).unwrap();
|
||||
let pages = tiling(&mut s, json!({}))["pages"].as_array().unwrap().clone();
|
||||
let pv = s.execute("print.preview", &json!({})).unwrap();
|
||||
assert_eq!(pages.len(), pv["tiles"][0]["tiles"].as_array().unwrap().len(), "tile count equals print.preview");
|
||||
assert_eq!(pages.len(), pv["sheets"].as_array().unwrap().len());
|
||||
assert!(pages.len() > before, "the grid moved off the corner takes more tiles");
|
||||
let rects: Vec<[f64; 4]> = pages.iter().map(|p| rect(&p["imageable"])).collect();
|
||||
assert!(rects.iter().any(|r| (r[0] - 60.0).abs() < 1e-6 && (r[1] - 35.0).abs() < 1e-6), "a tile starts at the origin");
|
||||
let all = rects.iter().fold([f64::MAX, f64::MAX, f64::MIN, f64::MIN], |a, r| [a[0].min(r[0]), a[1].min(r[1]), a[2].max(r[2]), a[3].max(r[3])]);
|
||||
assert!(all[0] <= 0.0 && all[1] <= 0.0 && all[2] >= 300.0 && all[3] >= 200.0, "the tiles cover the artboard: {all:?}");
|
||||
// Bad input is refused.
|
||||
assert!(s.execute("print.tiling.set", &json!({})).is_err());
|
||||
assert!(s.execute("print.tiling.set", &json!({"origin": [1, 2], "artboard": 7})).is_err());
|
||||
assert!(s.execute("print.tiling.set", &json!({"origin": [1e9, 2]})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_saved_before_the_tile_origin_still_load() {
|
||||
let mut s = session();
|
||||
let mut old = serde_json::to_value(vectorcraft_pdf::PrintSettings::default()).unwrap();
|
||||
old.as_object_mut().unwrap().remove("tileOrigin");
|
||||
old["copies"] = json!(2);
|
||||
let mut doc = (*s.doc().unwrap().doc).clone();
|
||||
doc.print_setup = Some(old);
|
||||
s.doc_mut().unwrap().doc = std::sync::Arc::new(doc);
|
||||
let set = s.execute("print.setup", &json!({})).unwrap()["settings"].clone();
|
||||
assert_eq!((set["copies"].clone(), set["tileOrigin"]["placed"].clone()), (json!(2), json!(false)));
|
||||
assert_eq!(tiling(&mut s, json!({}))["pages"].as_array().unwrap().len(), 1);
|
||||
}
|
||||
@@ -102,7 +102,7 @@ fn art_bounds(doc: &Document) -> Option<Rect> {
|
||||
}
|
||||
|
||||
/// The regions that print: `(artboard, rect)`.
|
||||
fn regions(doc: &Document, set: &PrintSettings) -> Result<Vec<(Option<usize>, Rect)>, PdfError> {
|
||||
pub(crate) fn regions(doc: &Document, set: &PrintSettings) -> Result<Vec<(Option<usize>, Rect)>, PdfError> {
|
||||
let count = doc.artboards.len();
|
||||
let picked: Vec<usize> = match set.artboards {
|
||||
PrintArtboards::Ignore => {
|
||||
@@ -128,6 +128,22 @@ fn scaled(sides: [f64; 4], (sx, sy): (f64, f64)) -> [f64; 4] {
|
||||
[sides[0] * sy, sides[1] * sy, sides[2] * sx, sides[3] * sx]
|
||||
}
|
||||
|
||||
/// The tiles along one axis of a printed area `extent` long, `tile` long each and `step` apart:
|
||||
/// the first (tile `k` starts at `start + k × step`) and how many. Tiles showing no more of the area
|
||||
/// than the `overlap` are left out. Without a `start`, the first tile starts at the area's edge.
|
||||
fn span(start: Option<f64>, extent: f64, tile: f64, step: f64, overlap: f64) -> (f64, f64) {
|
||||
// Tiles that end (or start) exactly on an edge show none of the area beyond it.
|
||||
const EPS: f64 = 1e-6;
|
||||
match start {
|
||||
None => (0.0, ((extent - overlap) / step).ceil().max(1.0)),
|
||||
Some(g) => {
|
||||
let first = ((overlap - tile - g) / step + EPS).floor() + 1.0;
|
||||
let last = ((extent - overlap - g) / step - EPS).ceil() - 1.0;
|
||||
(first, (last - first + 1.0).max(1.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The page turns of a `w` × `h` paper (flipped, emulsion down, transverse) as one transform from
|
||||
/// the paper onto the page, and the page's size.
|
||||
fn turns(set: &PrintSettings, (w, h): (f64, f64), flipped: bool) -> (Affine, (f64, f64)) {
|
||||
@@ -151,6 +167,7 @@ pub(crate) fn layout(doc: &Document, set: &PrintSettings, warnings: &mut Vec<Str
|
||||
let marks = set.marks.printer_marks();
|
||||
let bleed = set.bleed.of(doc);
|
||||
let (pw, ph) = set.paper();
|
||||
let origin = set.tile_origin.placed.then_some((set.tile_origin.x, set.tile_origin.y));
|
||||
let (mut pages, mut grids) = (vec![], vec![]);
|
||||
for (artboard, r) in regions(doc, set)? {
|
||||
let what = artboard.map_or_else(|| "The art".to_string(), |i| format!("Artboard {}", i + 1));
|
||||
@@ -225,9 +242,17 @@ pub(crate) fn layout(doc: &Document, set: &PrintSettings, warnings: &mut Vec<Str
|
||||
page_bleed,
|
||||
};
|
||||
if !set.scaling.tiles() {
|
||||
let (fx, fy) = set.placement.origin.factors();
|
||||
let x = imageable.x0 + (imageable.width() - extent.0) * fx + set.placement.x;
|
||||
let y = imageable.y0 + (imageable.height() - extent.1) * fy + set.placement.y;
|
||||
// The printed area's top-left corner: where the tile origin puts it, else the placement.
|
||||
let (x, y) = match origin {
|
||||
Some((ox, oy)) => (imageable.x0 - ox * scale.0 - most[2], imageable.y0 - oy * scale.1 - most[0]),
|
||||
None => {
|
||||
let (fx, fy) = set.placement.origin.factors();
|
||||
(
|
||||
imageable.x0 + (imageable.width() - extent.0) * fx + set.placement.x,
|
||||
imageable.y0 + (imageable.height() - extent.1) * fy + set.placement.y,
|
||||
)
|
||||
}
|
||||
};
|
||||
let trim = Rect::new(x + most[2], y + most[0], x + most[2] + size.0, y + most[0] + size.1);
|
||||
if !imageable.inflate(0.01, 0.01).contains_rect(Rect::new(x, y, x + extent.0, y + extent.1)) {
|
||||
warnings.push(format!("{what} with its bleed and marks is larger than the imageable area: part of it doesn't print"));
|
||||
@@ -242,7 +267,13 @@ pub(crate) fn layout(doc: &Document, set: &PrintSettings, warnings: &mut Vec<Str
|
||||
return Err(PdfError::BadSetting(format!("overlap: {o} pt is half a tile or more")));
|
||||
}
|
||||
let step = (tile.0 - o, tile.1 - o);
|
||||
let (cols, rows) = (((extent.0 - o) / step.0).ceil().max(1.0), ((extent.1 - o) / step.1).ceil().max(1.0));
|
||||
// The tile origin's page starts the grid: its imageable area's corner is the origin's point
|
||||
// (a full page's imageable area is inside the margin).
|
||||
let inside = if set.scaling == PrintScaling::TileFull { m } else { 0.0 };
|
||||
let start = origin.map(|(ox, oy)| (most[2] + ox * scale.0 - inside, most[0] + oy * scale.1 - inside));
|
||||
let (first_x, cols) = span(start.map(|s| s.0), extent.0, tile.0, step.0, o);
|
||||
let (first_y, rows) = span(start.map(|s| s.1), extent.1, tile.1, step.1, o);
|
||||
let grid = (start.map_or(0.0, |s| s.0) + first_x * step.0, start.map_or(0.0, |s| s.1) + first_y * step.1);
|
||||
if cols * rows > MAX_TILES as f64 {
|
||||
return Err(PdfError::BadSetting(format!("{what} would print on more than {MAX_TILES} tiles: scale it down")));
|
||||
}
|
||||
@@ -259,7 +290,8 @@ pub(crate) fn layout(doc: &Document, set: &PrintSettings, warnings: &mut Vec<Str
|
||||
let at = if set.scaling == PrintScaling::TileFull { (0.0, 0.0) } else { (imageable.x0, imageable.y0) };
|
||||
let window = |t: usize| {
|
||||
let (c, rw) = ((t % cols) as f64, (t / cols) as f64);
|
||||
Rect::new(c * step.0, rw * step.1, c * step.0 + tile.0, rw * step.1 + tile.1)
|
||||
let (x, y) = (grid.0 + c * step.0, grid.1 + rw * step.1);
|
||||
Rect::new(x, y, x + tile.0, y + tile.1)
|
||||
};
|
||||
for &t in &pick {
|
||||
let w = window(t);
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
//! - General: copies (collated or not), reverse order, the artboards (all, a range or ignored:
|
||||
//! all the art as one page), blank artboards skipped, the paper and its orientation (or turned
|
||||
//! to each artboard's), transverse, which layers print (template layers never do), the
|
||||
//! placement on the imageable area, and the scale: none, fit, custom, or tiles of the paper or
|
||||
//! of its imageable area, overlapping, a range of them.
|
||||
//! placement on the imageable area (or where the Print Tiling tool put the pages,
|
||||
//! [`TileOrigin`]), and the scale: none, fit, custom, or tiles of the paper or of its imageable
|
||||
//! area, overlapping, a range of them.
|
||||
//! - Marks and bleed as PDF export draws them ([`crate::MarkSettings`], [`crate::BleedSettings`]),
|
||||
//! around the artboard on the paper, at the paper's scale.
|
||||
//! - Output: composite, or separations, one page per ink that prints ([`print_inks`]) in the
|
||||
@@ -16,11 +17,13 @@
|
||||
//!
|
||||
//! Halftone screens and flatness are left to the output device (warnings say so). [`preview`]
|
||||
//! lists the pages, tiles and inks without writing the file; [`plan`] lays the job out for other
|
||||
//! writers (PostScript), which can write screens and flatness themselves.
|
||||
//! writers (PostScript), which can write screens and flatness themselves; [`tiling`] turns the
|
||||
//! layout into the pages in document space (View → Show Print Tiling).
|
||||
|
||||
mod layout;
|
||||
mod plates;
|
||||
mod settings;
|
||||
mod tiling;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
@@ -33,6 +36,7 @@ use vectorcraft_doc::{ColorMode, Document, Node, NodeKind};
|
||||
pub use layout::{MAX_TILES, TileGrid};
|
||||
pub use plates::print_inks;
|
||||
pub use settings::*;
|
||||
pub use tiling::{TilingPage, tiling};
|
||||
|
||||
use crate::export::{Exporter, Sheet, Writer};
|
||||
use crate::marks::PageBoxes;
|
||||
@@ -145,12 +149,25 @@ pub fn keep_layers(nodes: &mut Vec<Arc<Node>>, which: PrintLayers) {
|
||||
});
|
||||
}
|
||||
|
||||
/// `doc` as `set` prints it: live effects applied (raster effects are rendered by the app first),
|
||||
/// only the layers that print.
|
||||
fn printed_doc<'a>(doc: &'a Document, set: &PrintSettings) -> Cow<'a, Document> {
|
||||
let mut doc = vectorcraft_effects::bake_document(doc).map_or(Cow::Borrowed(doc), Cow::Owned);
|
||||
keep_layers(&mut doc.to_mut().layers, set.print_layers);
|
||||
doc
|
||||
}
|
||||
|
||||
/// What of `doc` prints on pages of its own: each artboard printed (its rect), or all the art
|
||||
/// (`None`) with artboards ignored.
|
||||
pub fn print_regions(doc: &Document, set: &PrintSettings) -> Result<Vec<(Option<usize>, kurbo::Rect)>, PdfError> {
|
||||
set.check()?;
|
||||
layout::regions(&printed_doc(doc, set), set)
|
||||
}
|
||||
|
||||
impl<'a> Job<'a> {
|
||||
fn new(doc: &'a Document, set: &PrintSettings, postscript: bool) -> Result<Self, PdfError> {
|
||||
set.check()?;
|
||||
// Live effects print as their result (raster effects are rendered by the app first).
|
||||
let mut doc = vectorcraft_effects::bake_document(doc).map_or(Cow::Borrowed(doc), Cow::Owned);
|
||||
keep_layers(&mut doc.to_mut().layers, set.print_layers);
|
||||
let doc = printed_doc(doc, set);
|
||||
let mut warnings = vec![];
|
||||
let (layouts, tiles) = layout::layout(&doc, set, &mut warnings)?;
|
||||
let separations = set.output.mode == OutputMode::Separations;
|
||||
|
||||
@@ -214,6 +214,8 @@ pub struct PrintSettings {
|
||||
pub output: PrintOutput,
|
||||
pub graphics: PrintGraphics,
|
||||
pub color: PrintColor,
|
||||
/// Where the Print Tiling tool put the pages.
|
||||
pub tile_origin: TileOrigin,
|
||||
}
|
||||
|
||||
impl Default for PrintSettings {
|
||||
@@ -244,6 +246,7 @@ impl Default for PrintSettings {
|
||||
output: PrintOutput::default(),
|
||||
graphics: PrintGraphics::default(),
|
||||
color: PrintColor::default(),
|
||||
tile_origin: TileOrigin::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,6 +370,20 @@ impl Default for PrintColor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the Print Tiling tool put the pages (View → Show Print Tiling).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct TileOrigin {
|
||||
/// The tool placed the pages: the top-left corner of the first page's imageable area is at
|
||||
/// (`x`, `y`) from the top-left corner of the art that prints (the artboard, or all the art
|
||||
/// with artboards ignored), in document points. It replaces the placement, and tiles start
|
||||
/// there (and go on to the left and up as far as the art does). Off: the placement places the
|
||||
/// art and tiles start at the top-left corner of the art with its bleed and marks.
|
||||
pub placed: bool,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
/// Most copies of a job.
|
||||
pub const MAX_COPIES: u32 = 999;
|
||||
|
||||
@@ -385,6 +402,8 @@ impl PrintSettings {
|
||||
within("scale.height", self.scale.height, 1.0, 1000.0, "%")?;
|
||||
within("overlap", self.overlap, 0.0, 720.0, " pt")?;
|
||||
within("margin", self.margin, 0.0, 144.0, " pt")?;
|
||||
within("tileOrigin.x", self.tile_origin.x, -14_400.0, 14_400.0, " pt")?;
|
||||
within("tileOrigin.y", self.tile_origin.y, -14_400.0, 14_400.0, " pt")?;
|
||||
self.marks.check()?;
|
||||
self.bleed.check()?;
|
||||
within("graphics.flatness", self.graphics.flatness, 0.2, 100.0, "")?;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
//! View → Show Print Tiling: the pages of a job in document space, as [`super::preview`] lays
|
||||
//! them out: the layout [`super::plan`] gives the PDF and PostScript writers, so the canvas shows
|
||||
//! what prints (without separating the plates, which the overlay doesn't need).
|
||||
|
||||
use kurbo::{Affine, Rect};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{PrintPreview, PrintScaling, PrintSettings};
|
||||
|
||||
/// One page of the print tiling.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TilingPage {
|
||||
/// 0-based artboard (`None`: artboards ignored).
|
||||
pub artboard: Option<usize>,
|
||||
/// 1-based: the tile (across then down), or the page of the job.
|
||||
pub number: usize,
|
||||
/// It prints (tiles outside the tile range don't).
|
||||
pub printed: bool,
|
||||
/// The paper's edge, `[x0, y0, x1, y1]` in document space.
|
||||
pub page: [f64; 4],
|
||||
/// Its imageable area (inside the unprintable margin).
|
||||
pub imageable: [f64; 4],
|
||||
}
|
||||
|
||||
fn arr(r: Rect) -> [f64; 4] {
|
||||
[r.x0, r.y0, r.x1, r.y1]
|
||||
}
|
||||
|
||||
/// The pages of the job `pv` previews (printed with `set`), in document space: every tile of each
|
||||
/// artboard when tiling, else each page once (not once per ink).
|
||||
pub fn tiling(pv: &PrintPreview, set: &PrintSettings) -> Vec<TilingPage> {
|
||||
let m = set.margin;
|
||||
if set.scaling.tiles() {
|
||||
// Tiles are the paper (full pages) or its imageable area, at the print scale.
|
||||
let (sx, sy) = (set.scale.width / 100.0, set.scale.height / 100.0);
|
||||
let inset = (m / sx, m / sy);
|
||||
return pv
|
||||
.tiles
|
||||
.iter()
|
||||
.flat_map(|g| {
|
||||
g.tiles.iter().enumerate().map(move |(i, t)| {
|
||||
let tile = Rect::new(t[0], t[1], t[2], t[3]);
|
||||
let (page, imageable) = if set.scaling == PrintScaling::TileFull {
|
||||
(tile, tile.inflate(-inset.0, -inset.1))
|
||||
} else {
|
||||
(tile.inflate(inset.0, inset.1), tile)
|
||||
};
|
||||
TilingPage {
|
||||
artboard: g.artboard,
|
||||
number: i + 1,
|
||||
printed: g.printed.contains(&(i + 1)),
|
||||
page: arr(page),
|
||||
imageable: arr(imageable),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let mut out: Vec<TilingPage> = vec![];
|
||||
let mut last = None;
|
||||
for s in &pv.sheets {
|
||||
// Separations repeat each page once per ink, one after the other.
|
||||
if last == Some((s.artboard, s.tile)) {
|
||||
continue;
|
||||
}
|
||||
last = Some((s.artboard, s.tile));
|
||||
let to_page = Affine::new(s.transform);
|
||||
let det = to_page.determinant();
|
||||
if !det.is_finite() || det.abs() < 1e-12 {
|
||||
continue;
|
||||
}
|
||||
let back = to_page.inverse();
|
||||
let paper = Rect::new(0.0, 0.0, s.width, s.height);
|
||||
let page = back.transform_rect_bbox(paper);
|
||||
let imageable = back.transform_rect_bbox(paper.inflate(-m, -m));
|
||||
out.push(TilingPage { artboard: s.artboard, number: out.len() + 1, printed: true, page: arr(page), imageable: arr(imageable) });
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -20,6 +20,7 @@ pub mod meshblend;
|
||||
pub mod params;
|
||||
pub mod pen;
|
||||
pub mod place;
|
||||
pub mod printtiling;
|
||||
pub mod select;
|
||||
pub mod shape;
|
||||
pub mod slice;
|
||||
@@ -357,6 +358,7 @@ pub fn create(id: &str) -> Box<dyn Tool> {
|
||||
.or_else(|| distort::create(other))
|
||||
.or_else(|| extra::create(other))
|
||||
.or_else(|| slice::create(other))
|
||||
.or_else(|| printtiling::create(other))
|
||||
.unwrap_or_else(|| Box::new(NoopTool(tool_info(other).map(|t| t.id).unwrap_or("selection")))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//! The Print Tiling tool: drag to put the printed pages where they print. The pointer is the
|
||||
//! top-left corner of the first page's imageable area (snapping to the artboard's edges); the
|
||||
//! drag previews `print.tiling.set` and commits it as one undo step. A double click puts the pages
|
||||
//! back where the placement puts them (`print.tiling.set {reset}`). The canvas shows the pages
|
||||
//! while the tool is active (as View → Show Print Tiling does).
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_geom::{Point, Rect};
|
||||
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
/// Create the Print Tiling tool by id (None = not ours).
|
||||
pub fn create(id: &str) -> Option<Box<dyn Tool>> {
|
||||
(id == "printTiling").then(|| Box::new(PrintTilingTool::default()) as Box<dyn Tool>)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PrintTilingTool {
|
||||
/// The artboard the drag started on (index and rect), while dragging.
|
||||
drag: Option<(Option<usize>, Option<Rect>)>,
|
||||
began: bool,
|
||||
last: Point,
|
||||
}
|
||||
|
||||
/// `v` snapped to the nearest of `edges` within `tol`.
|
||||
fn snap1(v: f64, edges: [f64; 2], tol: f64) -> f64 {
|
||||
edges.into_iter().filter(|e| (e - v).abs() <= tol).min_by(|a, b| (a - v).abs().total_cmp(&(b - v).abs())).unwrap_or(v)
|
||||
}
|
||||
|
||||
impl Tool for PrintTilingTool {
|
||||
fn id(&self) -> &'static str {
|
||||
"printTiling"
|
||||
}
|
||||
|
||||
fn busy(&self) -> bool {
|
||||
self.drag.is_some()
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
match ev.kind {
|
||||
PointerKind::Down => {
|
||||
let hit = cx.doc.artboards.iter().position(|a| a.rect.contains(ev.pos));
|
||||
self.drag = Some((hit, hit.and_then(|i| cx.doc.artboards.get(i)).map(|a| a.rect)));
|
||||
(self.began, self.last) = (false, ev.pos);
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Drag => {
|
||||
let Some((artboard, rect)) = self.drag else { return vec![] };
|
||||
let tol = cx.tol(5.0);
|
||||
let p = match rect {
|
||||
Some(r) => Point::new(snap1(ev.pos.x, [r.x0, r.x1], tol), snap1(ev.pos.y, [r.y0, r.y1], tol)),
|
||||
None => ev.pos,
|
||||
};
|
||||
self.last = p;
|
||||
let mut out = vec![];
|
||||
if !self.began {
|
||||
self.began = true;
|
||||
out.push(Action::Begin("Print Tiling".into()));
|
||||
}
|
||||
out.push(Action::Preview("print.tiling.set".into(), json!({ "origin": [p.x, p.y], "artboard": artboard })));
|
||||
out
|
||||
}
|
||||
PointerKind::Up => {
|
||||
let began = std::mem::take(&mut self.began);
|
||||
if self.drag.take().is_some() && began { vec![Action::Commit] } else { vec![] }
|
||||
}
|
||||
PointerKind::DoubleClick => {
|
||||
self.drag = None;
|
||||
vec![Action::Exec("print.tiling.set".into(), json!({ "reset": true }))]
|
||||
}
|
||||
PointerKind::Move => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if key == ToolKey::Escape { self.deactivate(cx) } else { vec![] }
|
||||
}
|
||||
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
match self.drag {
|
||||
// How far the corner is from the artboard's.
|
||||
Some((_, Some(r))) if self.began => {
|
||||
vec![Overlay::Measure { p: self.last, text: cx.offset_label(self.last.x - r.x0, self.last.y - r.y0) }]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::Crosshair
|
||||
}
|
||||
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.drag = None;
|
||||
if std::mem::take(&mut self.began) { vec![Action::Cancel] } else { vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::Selection;
|
||||
|
||||
use super::*;
|
||||
use crate::testutil::{cx, doc_with_rect, paint};
|
||||
|
||||
fn ev(kind: PointerKind, x: f64, y: f64) -> PointerEvent {
|
||||
PointerEvent::new(kind, x, y)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_previews_the_origin_and_commits_once() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let (s, pd) = (Selection::default(), paint());
|
||||
let c = cx(&d, &s, &pd);
|
||||
let mut t = PrintTilingTool::default();
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Down, 50.0, 60.0)).is_empty());
|
||||
let a = t.pointer(&c, &ev(PointerKind::Drag, 40.0, 30.0));
|
||||
assert_eq!(a.first(), Some(&Action::Begin("Print Tiling".into())));
|
||||
assert_eq!(a.get(1), Some(&Action::Preview("print.tiling.set".into(), json!({"origin": [40.0, 30.0], "artboard": 0}))));
|
||||
// Near the artboard's edge the corner snaps to it.
|
||||
let a = t.pointer(&c, &ev(PointerKind::Drag, 2.0, 497.0));
|
||||
assert_eq!(a, vec![Action::Preview("print.tiling.set".into(), json!({"origin": [0.0, 500.0], "artboard": 0}))]);
|
||||
assert!(matches!(t.overlays(&c).as_slice(), [Overlay::Measure { .. }]));
|
||||
assert_eq!(t.pointer(&c, &ev(PointerKind::Up, 2.0, 497.0)), vec![Action::Commit]);
|
||||
assert!(!t.busy());
|
||||
// A click alone changes nothing; a double click resets.
|
||||
t.pointer(&c, &ev(PointerKind::Down, 10.0, 10.0));
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Up, 10.0, 10.0)).is_empty());
|
||||
assert_eq!(t.pointer(&c, &ev(PointerKind::DoubleClick, 10.0, 10.0)), vec![Action::Exec("print.tiling.set".into(), json!({"reset": true}))]);
|
||||
// Escape mid-drag cancels.
|
||||
t.pointer(&c, &ev(PointerKind::Down, 10.0, 10.0));
|
||||
t.pointer(&c, &ev(PointerKind::Drag, 20.0, 20.0));
|
||||
assert_eq!(t.key(&c, ToolKey::Escape, Mods::default()), vec![Action::Cancel]);
|
||||
assert_eq!(crate::create("printTiling").id(), "printTiling");
|
||||
}
|
||||
}
|
||||
@@ -289,6 +289,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if !app.session.slices_hidden() {
|
||||
slice_overlay(app, &painter, &xf);
|
||||
}
|
||||
if app.session.active().is_some_and(|d| d.print_tiling) || app.session.tool_id() == "printTiling" {
|
||||
print_tiling_overlay(app, &painter, &xf);
|
||||
}
|
||||
|
||||
// Selection visuals and tool overlays.
|
||||
if app.ui.view.edges {
|
||||
@@ -955,6 +958,40 @@ fn slice_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The print tiling (View → Show Print Tiling, or the Print Tiling tool): each page of the
|
||||
/// document's print settings as `print.preview` lays it out, its paper edge and its imageable
|
||||
/// area dashed, numbered at its top left; tiles outside the tile range dimmer. Cached per revision.
|
||||
fn print_tiling_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let key = (st.uid, st.revision);
|
||||
let pages = match &app.canvas.print_tiling {
|
||||
Some((k, pages)) if *k == key => pages.clone(),
|
||||
_ => {
|
||||
// Settings that can't print have no pages to show.
|
||||
let pages = std::sync::Arc::new(vectorcraft_engine::cmd::printtiling::pages(&st.doc).unwrap_or_default());
|
||||
app.canvas.print_tiling = Some((key, pages.clone()));
|
||||
pages
|
||||
}
|
||||
};
|
||||
let font = egui::FontId::proportional(10.0);
|
||||
let ring = |r: [f64; 4]| {
|
||||
let q = xf.quad(Rect::new(r[0], r[1], r[2], r[3]));
|
||||
q.iter().chain(q.first()).copied().collect::<Vec<Pos2>>()
|
||||
};
|
||||
for page in pages.iter() {
|
||||
let color = if page.printed { PRINT_TILING } else { PRINT_TILING.gamma_multiply(0.45) };
|
||||
p.add(Shape::line(ring(page.page), Stroke::new(0.75, color)));
|
||||
let inner = ring(page.imageable);
|
||||
p.extend(Shape::dashed_line(&inner, Stroke::new(1.0, color), 4.0, 3.0));
|
||||
if let Some(corner) = inner.iter().copied().reduce(|a, b| pos2(a.x.min(b.x), a.y.min(b.y))) {
|
||||
p.text(corner + vec2(3.0, 2.0), egui::Align2::LEFT_TOP, page.number.to_string(), font.clone(), color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The print tiling's lines: a neutral grey that reads on the paper and on the pasteboard.
|
||||
const PRINT_TILING: Color32 = Color32::from_gray(96);
|
||||
|
||||
fn selection_overlay(app: &VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let tool = app.session.tool_id();
|
||||
|
||||
@@ -65,6 +65,9 @@ pub(super) fn defaults(kind: &str) -> Option<&'static Value> {
|
||||
(kind == KIND || kind == PRESET_KIND).then(|| &*DEFAULTS)
|
||||
}
|
||||
|
||||
/// Set when the Print Tiling tool placed the pages (`tileOrigin`): the placement is ignored.
|
||||
const PLACED: &str = "tileOrigin.placed";
|
||||
|
||||
/// The fields that aren't print settings.
|
||||
const NOT_SETTINGS: [&str; 5] = ["printer", "toFile", "discard", "preset", "name"];
|
||||
|
||||
@@ -507,8 +510,17 @@ pub(super) fn move_placement(d: &mut Dialog, sheet: &Value, (dx, dy): (f64, f64)
|
||||
}
|
||||
// The transform's linear part is the page's turn times the scale, and the turn's inverse is
|
||||
// its transpose: the columns over the scale.
|
||||
let x = get(d, "placement.x").as_f64().unwrap_or(0.0) + (a * dx + b * dy) / sx;
|
||||
let y = get(d, "placement.y").as_f64().unwrap_or(0.0) + (c * dx + e * dy) / sy;
|
||||
let (px, py) = ((a * dx + b * dy) / sx, (c * dx + e * dy) / sy);
|
||||
if get(d, PLACED).as_bool() == Some(true) {
|
||||
// Pages the Print Tiling tool placed: their corner moves the other way over the art.
|
||||
let x = get(d, "tileOrigin.x").as_f64().unwrap_or(0.0) - px / sx;
|
||||
let y = get(d, "tileOrigin.y").as_f64().unwrap_or(0.0) - py / sy;
|
||||
set(d, "tileOrigin.x", json!(round2(x)));
|
||||
set(d, "tileOrigin.y", json!(round2(y)));
|
||||
return;
|
||||
}
|
||||
let x = get(d, "placement.x").as_f64().unwrap_or(0.0) + px;
|
||||
let y = get(d, "placement.y").as_f64().unwrap_or(0.0) + py;
|
||||
set(d, "placement.x", json!(round2(x)));
|
||||
set(d, "placement.y", json!(round2(y)));
|
||||
}
|
||||
@@ -606,8 +618,9 @@ fn general(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
});
|
||||
let scaling = choice::<PrintScaling>(d, "scaling").unwrap_or(PrintScaling::None);
|
||||
let tiles = scaling.tiles();
|
||||
let placed = get(d, PLACED).as_bool() == Some(true);
|
||||
row(ui, "Placement:", |ui| {
|
||||
ui.add_enabled_ui(!tiles, |ui| {
|
||||
ui.add_enabled_ui(!tiles && !placed, |ui| {
|
||||
let origin = choice::<Origin>(d, "placement.origin").unwrap_or_default();
|
||||
let current = Origin::ALL.iter().position(|o| *o == origin).unwrap_or(4);
|
||||
if let Some(o) = widgets::reference_point(ui, current).and_then(|i| Origin::ALL.get(i)) {
|
||||
@@ -619,6 +632,14 @@ fn general(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
length(ui, d, "placement.y", unit, true);
|
||||
});
|
||||
});
|
||||
if placed {
|
||||
row(ui, "", |ui| {
|
||||
note(ui, "The Print Tiling tool placed the pages.");
|
||||
if widgets::flat_button(ui, "Reset", 56.0).on_hover_text("Place the pages with the placement again").clicked() {
|
||||
set(d, PLACED, json!(false));
|
||||
}
|
||||
});
|
||||
}
|
||||
row(ui, "Scaling:", |ui| {
|
||||
pick::<PrintScaling>(ui, d, "scaling", 180.0, |_| true);
|
||||
});
|
||||
|
||||
@@ -60,6 +60,8 @@ mod tests_place;
|
||||
#[cfg(test)]
|
||||
mod tests_printps;
|
||||
#[cfg(test)]
|
||||
mod tests_printtiling;
|
||||
#[cfg(test)]
|
||||
mod tests_recolor;
|
||||
#[cfg(test)]
|
||||
mod tests_recovery;
|
||||
@@ -178,11 +180,16 @@ pub struct CanvasCache {
|
||||
pub worker_started: bool,
|
||||
/// The slices as laid out for (document uid, revision): the canvas draws them every frame.
|
||||
pub slices: Option<SliceCache>,
|
||||
/// The print tiling's pages for (document uid, revision) (View → Show Print Tiling).
|
||||
pub print_tiling: Option<PrintTilingCache>,
|
||||
}
|
||||
|
||||
/// [`CanvasCache::slices`]: the layout of the slices of (document uid, revision).
|
||||
pub type SliceCache = ((u64, u64), std::sync::Arc<Vec<vectorcraft_doc::SliceArea>>);
|
||||
|
||||
/// [`CanvasCache::print_tiling`]: the print tiling of (document uid, revision).
|
||||
pub type PrintTilingCache = ((u64, u64), std::sync::Arc<Vec<vectorcraft_pdf::TilingPage>>);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CacheKey {
|
||||
pub doc: usize,
|
||||
@@ -284,6 +291,7 @@ impl VectorcraftApp {
|
||||
worker: None,
|
||||
worker_started: false,
|
||||
slices: None,
|
||||
print_tiling: None,
|
||||
},
|
||||
perf: Perf::default(),
|
||||
integrated_titlebar: false,
|
||||
|
||||
@@ -226,7 +226,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"tool.options",
|
||||
"Tool Options…",
|
||||
"",
|
||||
"{tool: id} what double-clicking a tool button opens: gradient → the Gradient panel (window.panel), eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions)",
|
||||
"{tool: id} what double-clicking a tool button opens: gradient → the Gradient panel (window.panel), eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true})",
|
||||
),
|
||||
(
|
||||
"ui.colorGuideOptions",
|
||||
@@ -1027,6 +1027,7 @@ pub fn dynamic_label(app: &VectorcraftApp, id: &str, label: &str) -> String {
|
||||
"view.grid" => if v.grid { "Hide Grid" } else { "Show Grid" }.into(),
|
||||
"view.guides.lock" => if app.session.guides_locked() { "Unlock Guides" } else { "Lock Guides" }.into(),
|
||||
"view.slices.hide" => if app.session.slices_hidden() { "Show Slices" } else { "Hide Slices" }.into(),
|
||||
"view.printTiling" => if app.session.active().is_some_and(|d| d.print_tiling) { "Hide Print Tiling" } else { "Show Print Tiling" }.into(),
|
||||
id if id.starts_with("file.openRecent") => recent_slot(app, id)
|
||||
.map(|p| std::path::Path::new(p).file_name().map_or(p.clone(), |f| f.to_string_lossy().to_string()))
|
||||
.unwrap_or_else(|| "—".into()),
|
||||
@@ -1698,7 +1699,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
|
||||
Sep,
|
||||
c("Hide Edges", "view.edges"),
|
||||
c("Hide Artboards", "view.artboards"),
|
||||
todo("Show Print Tiling"),
|
||||
c("Show Print Tiling", "view.printTiling"),
|
||||
c("Hide Slices", "view.slices.hide"),
|
||||
c("Lock Slices", "view.slices.lock"),
|
||||
Sep,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//! View → Show Print Tiling and the Print Tiling tool on the canvas and in the menus.
|
||||
|
||||
use egui::{Pos2, vec2};
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
|
||||
fn app() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
app
|
||||
}
|
||||
|
||||
/// One headless canvas frame on an 800 × 600 window; the texts it painted.
|
||||
fn frame_texts(app: &mut VectorcraftApp, ctx: &egui::Context) -> Vec<String> {
|
||||
let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| crate::canvas::show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
out.shapes
|
||||
.iter()
|
||||
.filter_map(|c| match &c.shape {
|
||||
egui::Shape::Text(t) => Some(t.galley.text().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_canvas_numbers_the_print_tiles_of_print_preview() {
|
||||
let mut app = app();
|
||||
let ctx = egui::Context::default();
|
||||
assert!(!frame_texts(&mut app, &ctx).iter().any(|t| t == "1"), "hidden by default");
|
||||
assert_eq!(crate::menus::dynamic_label(&app, "view.printTiling", "Show Print Tiling"), "Show Print Tiling");
|
||||
app.run("view.printTiling", json!({})).unwrap();
|
||||
assert_eq!(crate::menus::dynamic_label(&app, "view.printTiling", "Show Print Tiling"), "Hide Print Tiling");
|
||||
assert!(frame_texts(&mut app, &ctx).iter().any(|t| t == "1"), "the page is numbered");
|
||||
// Tiled at 400%: every tile print.preview lays out is drawn and numbered.
|
||||
app.run("print.setup", json!({"settings": {"scaling": "tileImageable", "scale": {"width": 400, "height": 400}}})).unwrap();
|
||||
let texts = frame_texts(&mut app, &ctx);
|
||||
let pv = app.session.execute("print.preview", &json!({})).unwrap();
|
||||
let count = pv["tiles"][0]["tiles"].as_array().unwrap().len();
|
||||
assert!(count > 1);
|
||||
assert_eq!(app.canvas.print_tiling.as_ref().map(|(_, p)| p.len()), Some(count));
|
||||
for n in 1..=count {
|
||||
assert!(texts.contains(&n.to_string()), "tile {n} numbered: {texts:?}");
|
||||
}
|
||||
// The Print Tiling tool shows the tiling too; double-clicking its button resets it.
|
||||
app.run("view.printTiling", json!({})).unwrap();
|
||||
app.run("print.tiling.set", json!({"origin": [10, 10]})).unwrap();
|
||||
app.select_tool("printTiling");
|
||||
assert!(frame_texts(&mut app, &ctx).iter().any(|t| t == "1"));
|
||||
app.run("tool.options", json!({"tool": "printTiling"})).unwrap();
|
||||
assert_eq!(app.session.execute("print.setup", &json!({})).unwrap()["settings"]["tileOrigin"]["placed"], false);
|
||||
}
|
||||
@@ -182,7 +182,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
|
||||
/// Open a tool's options (`tool.options`, a double-click on its button): the Gradient tool's are
|
||||
/// the Gradient panel, the Eyedropper's the Eyedropper Options dialog.
|
||||
/// the Gradient panel, the Eyedropper's the Eyedropper Options dialog; the Print Tiling tool's
|
||||
/// resets the print tiling.
|
||||
pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::Value, String> {
|
||||
match tool {
|
||||
"gradient" if app.ui.open_panel.as_deref() == Some("gradient") => Ok(json!({ "open": "gradient" })),
|
||||
@@ -191,6 +192,8 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::
|
||||
crate::dialogs::eyedropper::open(app);
|
||||
Ok(json!({ "dialog": crate::dialogs::eyedropper::KIND }))
|
||||
}
|
||||
// A double click on the Print Tiling tool puts the pages back where the placement puts them.
|
||||
"printTiling" => app.run("print.tiling.set", json!({ "reset": true })),
|
||||
_ if vectorcraft_tools::tool_info(tool).is_none() => Err(format!("unknown tool `{tool}`")),
|
||||
_ => Err(format!("the {tool} tool has no options")),
|
||||
}
|
||||
|
||||
+22
@@ -1584,3 +1584,25 @@ claims the standard.
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"document.exportPdf","params":{"path":"/tmp/press.pdf","preset":"PDF/X-1a:2001","output":{"outputIntent":"VectorCraft Generic CMYK (SWOP-like)","outputCondition":"Coated","trapped":false}}}}
|
||||
```
|
||||
|
||||
## Print tiling
|
||||
|
||||
View → Show Print Tiling (`view.printTiling {on?}`, per document, not undoable) draws the pages of the document's print
|
||||
settings on the canvas: each page's paper edge, its imageable area dashed, numbered (tiles across then down; tiles
|
||||
outside `tileRange` dimmer). The Print Tiling tool (`printTiling`) shows them too. `print.tiling {settings?}` answers
|
||||
what is drawn, from the layout `print.preview` and both writers (PDF and PostScript) use, so the canvas and the
|
||||
printed job agree: `{tileOrigin, pages: [{artboard, number, printed, page, imageable}]}` (`[x0, y0, x1, y1]` in document space; every tile when tiling, else
|
||||
each page once, not once per ink).
|
||||
|
||||
Dragging with the tool puts the top-left corner of the first page's imageable area where the pointer is (snapping to
|
||||
the artboard's edges): `print.tiling.set {origin: [x, y], artboard?}` saves it with the print settings as
|
||||
`tileOrigin: {placed: true, x, y}`, measured from the artboard's top-left corner (one undo step). It replaces the
|
||||
placement, and tiles start there (going on left and up as far as the art does). A double click on the canvas, or on
|
||||
the tool's button, runs `print.tiling.set {reset: true}`: the placement places the pages again. In the Print dialog
|
||||
the placement then shows as placed by the tool, with a Reset button; dragging the preview moves the origin.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"view.printTiling","params":{"on":true}}}
|
||||
{"name":"run_command","arguments":{"command":"print.tiling.set","params":{"origin":[-36,-36]}}}
|
||||
{"name":"run_command","arguments":{"command":"print.tiling","params":{"settings":{"scaling":"tileImageable","scale":{"width":300,"height":300}}}}}
|
||||
```
|
||||
Reference in new issue
Block a user