Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8255f2bd5 | ||
|
|
dd1a32bb8e | ||
|
|
257c8ec435 | ||
|
|
ad8458832d | ||
|
|
a01b0dfc7a | ||
|
|
ddd1c8fade | ||
|
|
eefe849e14 | ||
|
|
00344f9b68 | ||
|
|
b8a61e90ce | ||
|
|
f2b35fc039 | ||
|
|
133ad87541 | ||
|
|
f8a7695c3a | ||
|
|
66cb7848f3 |
No files matched your search
+2
-2
@@ -134,7 +134,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- **Pen:** Alt pressed in the middle of dragging out an anchor splits its handles and keeps the curve already shaped into it (only the outgoing handle follows the pointer); Space held while dragging out an anchor moves it, handles and all. A press on the first anchor of the path being drawn closes it whatever the drag then does: the drag shapes the closing curve (symmetric handles, Alt breaks them) and the release closes the path wherever it ends (#501). Clicking the last anchor of the path being drawn retracts its outgoing handle (convert cursor), and dragging from it pulls out a new one on its own. Auto Add/Delete: over a selected path, a click on a segment adds an anchor and a click on an anchor deletes it (add and delete cursors); Shift held, or Preferences › General › Disable Auto Add/Delete, starts a new path instead. Alt held over a handle end or an anchor of a selected path (the one being drawn too) works as the Anchor Point tool, with its convert cursor: dragging a handle moves it alone, clicking a smooth anchor makes it a corner, dragging an anchor pulls out new symmetric handles, dragging a segment reshapes it; drawing then goes on. Cmd (Ctrl) held drags with the selection tool chosen last, Direct Selection until one is (handles, anchors and segments of the path being drawn or another), and the Pen then goes on drawing its path while that stays selected; the pointer shows that tool's while Cmd is held. The same holds for agents (`pointer_gesture` with `mods.cmd`), as the borrowing now lives in the engine (#494).
|
||||
- **Path editing (#494):** the handles shown are the handles that drag: Direct Selection drags those of a path selected as a whole (as the Pen leaves it), the Anchor Point tool and Pen+Alt no longer grab handles that Show handles when multiple anchors are selected hides, a handle shorter than the tolerance leaves its anchor to be picked, and the anchor tools pick within Selection & Anchor Display › Tolerance. A dragged segment keeps a smooth anchor at either end smooth. With Direct Selection, the Pen, the Curvature and the anchor tools, a path selected as a whole shows all its anchors selected, and the Control bar and the Properties panel offer Convert (corner, smooth) for them too; Remove and Cut wait for anchors direct-selected (#504). A handle drag gathers its snap targets once per drag (it took 21 ms per pointer move on a 20,000-path document, now 1 ms; `vectorcraft-cli perf` measures handle and segment drags on a 5,000-anchor path).
|
||||
- **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type.
|
||||
- **Other:** Eyedropper, Gradient annotator, Artboard (Alt-drag duplicates an artboard, and its art with Move/Copy Artwork with Artboard on; Copy, Cut and Paste take the artboard with its art, Paste right of the last artboard, Paste in Place where it was, into this or another document, #446; Object › Artboards › Rearrange All Artboards lays them out in a grid by row or by column, in one row or one column, left to right or right to left, from the menu, the Artboards panel or Properties' Rearrange All, #681), Measure, Type, Hand, Zoom, Rotate View.
|
||||
- **Other:** Eyedropper, Gradient annotator, Artboard (Alt-drag duplicates an artboard, and its art with Move/Copy Artwork with Artboard on; Copy, Cut and Paste take the artboard with its art, Paste right of the last artboard, Paste in Place where it was, into this or another document, #446; Object › Artboards › Rearrange All Artboards lays them out in a grid by row or by column, in one row or one column, left to right or right to left, from the menu, the Artboards panel or Properties' Rearrange All, #681; Scale Artwork with Artboard, in the Control bar and Properties beside Move Artwork with Artboard, resizes the artboard proportionally with the art fully inside it and its guides, strokes and corners per the Scale preferences, #602), Measure, Type, Hand, Zoom, Rotate View.
|
||||
- **Editing type:** double-clicking type with a selection tool switches to the Type tool with the caret where it was clicked.
|
||||
- **Temporary tools:** Cmd with any other tool drags with the selection tool used last (Selection, Direct Selection or Group Selection). Held Space pans and Cmd+Space zooms (Cmd+Alt+Space out) whatever the tool.
|
||||
- **Panel drag and drop:** art dragged off the canvas onto the Symbols panel becomes a symbol, onto the Swatches panel a pattern swatch and onto the Brushes panel an Art brush; a symbol dragged onto the canvas places an instance where it is dropped, and a brush dragged onto a path is applied to it.
|
||||
@@ -165,7 +165,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- **Files (M4):**
|
||||
- `.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 (browser tabs hold their copies with Web Locks too, so a background tab's copies are never taken for a crash's, and a tab repairs copies another removed when it resumes: #367), background save and export. The desktop app writes its warnings and errors (graphics adapter, a lost graphics device, panics the guard recovered from) to standard error and to `logs/vectorcraft.log` next to the preferences, the two previous runs kept and each file capped at 16 MiB, so a launch without a terminal still leaves something to attach to a bug report (#465).
|
||||
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units (SVG type under a transform or `viewBox` scale opens at the size it draws at, the scale moved into its size, leading, baseline shift and strokes, a stretch, skew or rotation kept as its transform: #396; a top-level group with a clip path, such as a clipped Inkscape layer, opens as a layer with a clipping mask, its name kept: #420; hidden objects written hidden on Save and `convert`, and `data-*` attributes kept as the objects' own data: #522; top-level groups open as layers named from `data-name`, else the id, also when one more group (a transform on all the art, a `<switch>`) wraps them, hidden ones hidden, and a file whose art sits loose under a root `<svg>` with an id (or `data-name`), as one-layer exports write it, opens with its one layer named from it; nested groups stay groups, except the sublayers VectorCraft writes, marked `data-vc-layer`, and Inkscape sublayers, which open as sublayers in their layer's colour: #372); PDF and PDF-compatible `.ai` in and out (layers, with hidden ones kept hidden with their art and nested ones as sublayers, on pages of up to 64 million operators, art whose layer can't be told going to a hidden, non-printing layer rather than showing or printing: #419; strokes kept live, a fill and a stroke of one outline as one object; a type object's stroke written as each glyph's outline stroked back on the type, so it stays one editable object; lines wrapped in a frame (each wrapped line ending in a space, one left edge, leading and font) back as one area type object, left aligned or justified with its paragraph spacing, where laying it out breaks the lines where the file does; glyphs set apart by gaps wider than a space (a barcode's digit groups) keeping their places; layers stacked as the pages mark them even when one first paints on a later page, art ending a layer's marked content or a clip closing in a later layer kept in its own layer, and layers with no art kept empty: #508; masks, editable text with type set along a curve coming back as type on a path, each artboard's art on its own page only, an `.ai`'s white page under its layers left out rather than coming in as art, spot colours, presets, security, marks and bleed, ICC output intents, raster effects, subset fonts, PDF/X, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3). CMYK images keep their ink amounts from PDF import (and from placed CMYK TIFFs) to PDF export: a CMYK JPEG passes through unchanged, others stay CMYK (DeviceCMYK, or ICC-based CMYK in PDF/X-3/X-4) unless the Output settings convert them; the canvas shows them through the working CMYK profile (#320). A masked CMYK image (`/SMask`, `/Mask` or a colour key) keeps its inks too, its alpha coming in as a greyscale opacity mask.
|
||||
- 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. EPS import runs a PostScript Level 3 interpreter written from the PostScript Language Reference: resource categories and `resourceforall`, local and global VM (`currentglobal`/`setglobal`/`gcheck`), `clipsave`/`cliprestore`, subarrays and substrings that share storage, the standard dictionaries in `systemdict`, `dictstack`, `internaldict`, executable filters and strings, `load` by any key, `//name` immediate names, `token`, `execform`, user paths, `cshow`, `rootfont`, `cachestatus`, colour rendering and halftone queries, file `status` and data read with `flushfile`, and `pdfmark` marks dropped (Discord feedback, #466, #469, #474). Masked images (`ImageType 3` in its three interleavings) and reusable streams are read, and an image's several data sources are read in turn, as the reference defines (procedures that read one file get their own rows: such images came in banded, #498). `restore` puts dictionaries back as they were at the `save`, so programs that count their saves in a dictionary read to their end (part of #505). Tiling patterns come in as pattern swatches, uncoloured ones too; Type 3 fonts' glyphs as their outlines, spaced by `ashow`/`widthshow`/`xshow`/`kshow`; mesh and function-based shadings (types 1, 4–7) as gradient meshes; sampled functions (type 0) and Flate/LZW predictors are read. It is tested with programs written from the specification in the ways common generators use these features; Inkscape/cairo EPS files open as vectors. When a file's PostScript still can't be read, it opens as its TIFF preview, its Windows metafile preview (as vectors) or its EPSI preview, and the warning (in the status bar, `document.open`/`app.open`/MCP `open_file` replies and `vectorcraft-cli info`) names the error, the operator that raised it and the procedures it ran in. Legacy PostScript `.ai` files (Illustrator 3–8, written with the prolog that defines their operators) keep their groups and nested groups (`u` … `U`), between and inside clipping groups; EPS and EMF/WMF import nest clips (and groups) at most 128 deep, so a hostile file can't exhaust the stack. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole). DXF export writes each fill as its outlines followed by its hatch, so laser and cutter software, which skips hatches, gets filled art and type too (#546).
|
||||
- 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. EPS import runs a PostScript Level 3 interpreter written from the PostScript Language Reference: resource categories and `resourceforall`, local and global VM (`currentglobal`/`setglobal`/`gcheck`), `clipsave`/`cliprestore`, subarrays and substrings that share storage, the standard dictionaries in `systemdict`, `dictstack`, `internaldict`, executable filters and strings, `load` by any key, `//name` immediate names, `token`, `execform`, user paths, `cshow`, `rootfont`, `cachestatus`, colour rendering and halftone queries, file `status` and data read with `flushfile`, and `pdfmark` marks dropped (Discord feedback, #466, #469, #474). Masked images (`ImageType 3` in its three interleavings) and reusable streams are read, and an image's several data sources are read in turn, as the reference defines (procedures that read one file get their own rows: such images came in banded, #498). `restore` puts dictionaries back as they were at the `save`, so programs that count their saves in a dictionary read to their end (part of #505). Tiling patterns come in as pattern swatches, uncoloured ones too; Type 3 fonts' glyphs as their outlines, spaced by `ashow`/`widthshow`/`xshow`/`kshow`; mesh and function-based shadings (types 1, 4–7) as gradient meshes; sampled functions (type 0) and Flate/LZW predictors are read. It is tested with programs written from the specification in the ways common generators use these features; Inkscape/cairo EPS files open as vectors. When a file's PostScript still can't be read, it opens as its TIFF preview, its Windows metafile preview (as vectors) or its EPSI preview, and the warning (in the status bar, `document.open`/`app.open`/MCP `open_file` replies and `vectorcraft-cli info`) names the error, the operator that raised it and the procedures it ran in. Legacy PostScript `.ai` files (Illustrator 3–8, written with the prolog that defines their operators) keep their groups and nested groups (`u` … `U`), between and inside clipping groups; EPS and EMF/WMF import nest clips (and groups) at most 128 deep, so a hostile file can't exhaust the stack. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole). PDF export writes type as real, searchable text in embedded subset fonts by default, as Illustrator's PDF presets do (Convert Text to Outlines is an option; printer's marks stay shapes), so text-heavy PDFs stay small (#733). DXF export writes each fill as its outlines followed by its hatch, so laser and cutter software, which skips hatches, gets filled art and type too (#546).
|
||||
- Affinity documents in (AF.1–AF.3): `.af` (Affinity 3) and `.afdesign`/`.afphoto`/`.afpub` (Affinity 1 and 2, container versions 8–12) open and place as editable art: layers and sublayers, groups (pass-through or isolated), artboards (with their names and backgrounds) and Publisher pages side by side, curves with live corners, parametric shapes (rectangles with corner radii, ellipses, polygons, stars, pies, triangles, trapezoids), compound shapes, solid/linear/elliptical gradient fills and strokes (weight, dashes, alignment, scale-with-object), clipping by nesting, vector and pixel masks, artistic and frame text with fonts, sizes, tracking, leading and alignment, placed images (their original JPEG/PNG) and pixel layers (8/16-bit RGB, CMYK), opacity, visibility, locking and blend modes, at the document's resolution in points. Effects, adjustments, brushes and pressure profiles, transparency gradients, master pages, symbol instances (imported as their cached picture), special shapes (cloud, heart, cog…, imported as ellipses) and Affinity-only blend modes are named in the import warning. A file whose native data can't be read opens as its embedded preview with the reason, and that preview can't be placed. Save never writes back over the Affinity file and there is no Affinity export. 22 public CC0/MIT/Apache-2.0 files, pinned and sha256-verified (`cargo xtask corpus --affinity`), must render within a few levels of the thumbnails Affinity saved in them; [scope, provenance and limits](crates/affinity/README.md).
|
||||
- Placed documents: File › Place of a `.vectorcraft` file links to it by default (`file.place`'s `link`): one locked object showing an artboard of the document (or its art's bounds), read the way placing it without Link reads it, its symbols, patterns and images kept apart from the document's; without Link (or from bytes, not a file) it is an editable copy as before. It stays vectors in every output (SVG, PDF, EPS, EMF, DXF), and so do placed documents inside it, read from their own files up to 8 levels deep (documents that place each other stop there). On screen they draw from bitmaps cached per file and zoom step, shared by every copy and made in the background (a placeholder until ready), so a sheet of them redraws in about a millisecond; exports and prints draw them exactly. Like linked images, a save keeps only a preview of each (JPEG when opaque) unless Include Linked Files; output, and zooming past the preview, read the file again, falling back to the preview with a warning when it is gone or changed. Saves for older versions write them as plain art. Links check, update, relink, Update Links on open, Link Info (the artboard's size), Placement Options, Break Link (also Embed, Object › Expand: the editable copy placing without Link gives, its resources joining the document), Edit Original (the document in a new tab) and the Links panel cover them; saving a document updates the open documents that place it. The native format (v4) keeps data of 8 KB or more after the JSON instead of base64 in it.
|
||||
- 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; files copied in a file manager paste as art (SVG, PDF, EMF/WMF, bitmaps) on every desktop platform, and Linux/Wayland reads the compositor's clipboard (#398); on Linux the Paste menu checks the clipboard off the UI thread, so an X11 owner that never answers can't freeze the window (#510); screenshots and other copied pictures paste as embedded images at the view centre everywhere, the web included (#457).
|
||||
|
||||
@@ -35,3 +35,8 @@ sync_github_name = true
|
||||
[people.dotCooCoo]
|
||||
real_name = "Robert Lee"
|
||||
sync_github_name = true
|
||||
|
||||
[people.AutoHenry]
|
||||
real_name = "Hal Goff"
|
||||
display_name = "Hal Goff, HG Studio"
|
||||
sync_github_name = false
|
||||
@@ -96,7 +96,7 @@ pub use vectorcraft_geom as geom;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vectorcraft_color::{Swatch, SwatchGroup};
|
||||
use vectorcraft_geom::{Point, Rect, Vec2};
|
||||
use vectorcraft_geom::{Affine, Point, Rect, Vec2};
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
pub enum DocError {
|
||||
@@ -1006,6 +1006,13 @@ impl Document {
|
||||
*g = g.moved(d);
|
||||
}
|
||||
}
|
||||
/// Map the guides of artboard `id` through `xf`, an axis-aligned scale and move (Scale Artwork
|
||||
/// with Artboard).
|
||||
pub fn map_artboard_guides(&mut self, id: u32, xf: Affine) {
|
||||
for g in self.guides.iter_mut().filter(|g| g.artboard == Some(id)) {
|
||||
g.pos = if g.vertical { (xf * Point::new(g.pos, 0.0)).x } else { (xf * Point::new(0.0, g.pos)).y };
|
||||
}
|
||||
}
|
||||
/// Copy the guides of artboard `from` onto artboard `to`, `d` away (with a copy of their
|
||||
/// artboard).
|
||||
pub fn copy_artboard_guides(&mut self, from: u32, to: u32, d: Vec2) {
|
||||
|
||||
@@ -109,7 +109,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export PDF",
|
||||
[],
|
||||
None,
|
||||
"{path?, preset?: \"VectorCraft Default\" (built-in or saved: pdf.preset.list), artboard? | artboards?: [i…] | range?: \"1-3, 5\" (1-based; default all, one page each), standard?: none|pdfA2b|pdfX1a (CMYK, grey and spot colours only: colours and images converted to output.destination or the CMYK output intent, untagged; transparency flattened with flattenerPreset; PDF 1.3)|pdfX3 (colours ICC-tagged; transparency flattened; PDF 1.3)|pdfX4 (transparency and layers kept; colours ICC-tagged; PDF 1.6 at most) (PDF/X: a /GTS_PDFX output intent, by default the CMYK profile in effect, embedded; TrimBox, Trapped, GTS_PDFXVersion, title and dates; fonts embedded or outlined), compatibility?: 1.3 (no transparency: it is flattened with flattenerPreset; images keep no see-through pixels)|1.4|1.5|1.6|1.7 (default)|2.0 (choosing a standard sets its version: 1.7 for pdfA2b, 1.6 for pdfX4, 1.3 for pdfX1a and pdfX3, which write 1.4 as 1.3 too; pdfA2b refuses 1.3), preserveEditing? (on in VectorCraft Default: the native document as an embedded file, which document.open restores; choosing a standard turns it off, a standard refuses it), thumbnails? (each page drawn as a /Thumb image, 106 px on its long side), fastWebView? (a linearised file: the first page shows while the rest downloads; it stays linearised when encrypted), viewAfterSaving? (the app opens the written file), createLayers? (each layer and sublayer but template layers as a PDF layer, an optional content group listed under its layer's: hidden layers off, non-printing ones /PrintState /OFF, locked ones locked; needs 1.5 or later; they reopen as layers; choosing pdfX1a or pdfX3 turns it off; they refuse it), includeNonPrinting? (keep layers whose Print option is off; left out by default unless createLayers), flattenerPreset?: \"\" (High Resolution) | high | medium | low | a saved preset (flattener.presets.list): what PDF 1.3 files flatten transparency with, flattener?: {object.flattenTransparency options over it}, compression?: {color?, gray?: {downsample: none|average|subsample|bicubic, ppi: 300, abovePpi: 450, compression: none|zip|jpeg|jpeg2000|auto, quality: minimum|low|medium|high|maximum}, mono?: {downsample, ppi: 1200, abovePpi: 1800, compression: none|ccittG3|ccittG4|zip|runLength} (black-and-white images), compressText?: true} (images above abovePpi are resampled to ppi; auto keeps JPEGs JPEG, the others lossless; JPEG needs opaque images; none, jpeg2000, CCITT and runLength are written as ZIP with a warning), marks?: {trim, registration, colorBars, pageInfo, kind: roman|japanese, weight: 0.25 (trim marks and targets), offset: 6 (pt from the artboard, at least the bleed)} (printer's marks in [Registration], every plate: trim marks, registration targets mid-side, CMYK/spot/black-tint colour bars on top, page information (title, artboard, date UTC) below), bleed?: {useDocument (the document's bleed, document.setup), top, bottom, left, right} (pt; art in the bleed is kept). Each page: TrimBox = artboard, BleedBox = artboard + bleed, MediaBox = that + the marks' room (art clipped to the BleedBox), output?: {conversion: none (default)|destination (every colour into the destination profile's model, CMYK or RGB, also colours of that model in another profile)|preserveNumbers (only colours of the other model and Lab; those in the destination's model keep their numbers) (grey stays grey; images are converted too, printer's marks are not), destination: profile name (edit.colorSettings lists them; default: the document's profile for its colour mode), profiles: none (device colours)|all|destination|taggedSource (documents assigned a profile: edit.assignProfile) (ICC-based colours: CMYK with the destination or document CMYK profile, RGB as sRGB, grey with the sRGB tone curve; PDF/A always), outputIntent: profile name (embedded as the catalog's /GTS_PDFX output intent; a name that isn't a profile is only named, which pdfX1a and pdfX3 allow only with a registry and pdfX4 refuses; pdfX1a needs a CMYK one), outputCondition, outputConditionId (default: outputIntent), registry, trapped (/Trapped /True; /False when an output intent is written)} (PDF/A files keep their own output intent), advanced?: {fontSubsetPercent: 100 (below 100 is accepted with a warning: fonts are always embedded as subsets), outlineText: true (false: type as real, selectable and searchable text in embedded subset fonts with a ToUnicode map; fonts whose licence forbids embedding, or allows only whole fonts, stay outlines with a warning), overprint: preserve (overprinting fills and strokes set /OP /op true, /OPM 1; white ones knock out with document.setup discardWhiteOverprint)|discard (they knock out)}, security?: {openPassword, permissionsPassword, printing: none|low|high, changes: none|pages|forms|comments|any, copy, screenReader, plaintextMetadata}} → {path, bytes, warnings}; no path → {dataBase64, bytes, warnings}. The options apply over the preset (null keeps its value); options ac
Line truncated
|
||||
"{path?, preset?: \"VectorCraft Default\" (built-in or saved: pdf.preset.list), artboard? | artboards?: [i…] | range?: \"1-3, 5\" (1-based; default all, one page each), standard?: none|pdfA2b|pdfX1a (CMYK, grey and spot colours only: colours and images converted to output.destination or the CMYK output intent, untagged; transparency flattened with flattenerPreset; PDF 1.3)|pdfX3 (colours ICC-tagged; transparency flattened; PDF 1.3)|pdfX4 (transparency and layers kept; colours ICC-tagged; PDF 1.6 at most) (PDF/X: a /GTS_PDFX output intent, by default the CMYK profile in effect, embedded; TrimBox, Trapped, GTS_PDFXVersion, title and dates; fonts embedded or outlined), compatibility?: 1.3 (no transparency: it is flattened with flattenerPreset; images keep no see-through pixels)|1.4|1.5|1.6|1.7 (default)|2.0 (choosing a standard sets its version: 1.7 for pdfA2b, 1.6 for pdfX4, 1.3 for pdfX1a and pdfX3, which write 1.4 as 1.3 too; pdfA2b refuses 1.3), preserveEditing? (on in VectorCraft Default: the native document as an embedded file, which document.open restores; choosing a standard turns it off, a standard refuses it), thumbnails? (each page drawn as a /Thumb image, 106 px on its long side), fastWebView? (a linearised file: the first page shows while the rest downloads; it stays linearised when encrypted), viewAfterSaving? (the app opens the written file), createLayers? (each layer and sublayer but template layers as a PDF layer, an optional content group listed under its layer's: hidden layers off, non-printing ones /PrintState /OFF, locked ones locked; needs 1.5 or later; they reopen as layers; choosing pdfX1a or pdfX3 turns it off; they refuse it), includeNonPrinting? (keep layers whose Print option is off; left out by default unless createLayers), flattenerPreset?: \"\" (High Resolution) | high | medium | low | a saved preset (flattener.presets.list): what PDF 1.3 files flatten transparency with, flattener?: {object.flattenTransparency options over it}, compression?: {color?, gray?: {downsample: none|average|subsample|bicubic, ppi: 300, abovePpi: 450, compression: none|zip|jpeg|jpeg2000|auto, quality: minimum|low|medium|high|maximum}, mono?: {downsample, ppi: 1200, abovePpi: 1800, compression: none|ccittG3|ccittG4|zip|runLength} (black-and-white images), compressText?: true} (images above abovePpi are resampled to ppi; auto keeps JPEGs JPEG, the others lossless; JPEG needs opaque images; none, jpeg2000, CCITT and runLength are written as ZIP with a warning), marks?: {trim, registration, colorBars, pageInfo, kind: roman|japanese, weight: 0.25 (trim marks and targets), offset: 6 (pt from the artboard, at least the bleed)} (printer's marks in [Registration], every plate: trim marks, registration targets mid-side, CMYK/spot/black-tint colour bars on top, page information (title, artboard, date UTC) below), bleed?: {useDocument (the document's bleed, document.setup), top, bottom, left, right} (pt; art in the bleed is kept). Each page: TrimBox = artboard, BleedBox = artboard + bleed, MediaBox = that + the marks' room (art clipped to the BleedBox), output?: {conversion: none (default)|destination (every colour into the destination profile's model, CMYK or RGB, also colours of that model in another profile)|preserveNumbers (only colours of the other model and Lab; those in the destination's model keep their numbers) (grey stays grey; images are converted too, printer's marks are not), destination: profile name (edit.colorSettings lists them; default: the document's profile for its colour mode), profiles: none (device colours)|all|destination|taggedSource (documents assigned a profile: edit.assignProfile) (ICC-based colours: CMYK with the destination or document CMYK profile, RGB as sRGB, grey with the sRGB tone curve; PDF/A always), outputIntent: profile name (embedded as the catalog's /GTS_PDFX output intent; a name that isn't a profile is only named, which pdfX1a and pdfX3 allow only with a registry and pdfX4 refuses; pdfX1a needs a CMYK one), outputCondition, outputConditionId (default: outputIntent), registry, trapped (/Trapped /True; /False when an output intent is written)} (PDF/A files keep their own output intent), advanced?: {fontSubsetPercent: 100 (below 100 is accepted with a warning: fonts are always embedded as subsets), outlineText: false (type as real, selectable and searchable text in embedded subset fonts with a ToUnicode map; fonts whose licence forbids embedding, or allows only whole fonts, stay outlines with a warning; true: every glyph as an outline), overprint: preserve (overprinting fills and strokes set /OP /op true, /OPM 1; white ones knock out with document.setup discardWhiteOverprint)|discard (they knock out)}, security?: {openPassword, permissionsPassword, printing: none|low|high, changes: none|pages|forms|comments|any, copy, screenReader, plaintextMetadata}} → {path, bytes, warnings}; no path → {dataBase64, bytes, warnings}. The options apply over the preset (null k
Line truncated
|
||||
has_doc,
|
||||
export_pdf
|
||||
),
|
||||
|
||||
@@ -10,8 +10,8 @@ use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Artboard, Document, LAYER_COLORS, LayerColor, Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::Rect;
|
||||
use vectorcraft_doc::{Artboard, Document, LAYER_COLORS, LayerColor, Node, NodeId, NodeKind, Scaling};
|
||||
use vectorcraft_geom::{Affine, Rect};
|
||||
|
||||
use super::*;
|
||||
use crate::EngineError;
|
||||
@@ -135,7 +135,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Artboard Options…",
|
||||
["Window", "Artboards"],
|
||||
None,
|
||||
"{index, name?, x?, y?, width?, height?}",
|
||||
"{index, name?, x?, y?, width?, height?, scaleArt?: bool (Scale Artwork with Artboard: resized, the artboard takes the art fully inside it (locked and hidden art only with lockedAndHidden?: bool, default prefs moveLockedWithArtboard) and its guides from its old rectangle onto the new one, each side by its own ratio; strokes/corners/patterns as object.scale)} → null, or with scaleArt {scaled: the art scaled}",
|
||||
has_doc,
|
||||
artboard_set
|
||||
),
|
||||
@@ -752,21 +752,62 @@ fn artboard_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn artboard_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = p.get("index").and_then(Value::as_u64).unwrap_or(0) as usize;
|
||||
let was = s.doc()?.doc.artboards.get(i).map(|a| a.rect).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
let now = rect_from(p, was);
|
||||
let resized = (now.width() - was.width()).abs() > 1e-6 || (now.height() - was.height()).abs() > 1e-6;
|
||||
// Scale Artwork with Artboard: resized, it takes the art fully inside it (as Move Artwork with
|
||||
// Artboard gathers it) and its guides from the old rectangle onto the new one.
|
||||
let scale_art = bool_or(p, "scaleArt", false);
|
||||
let fit = if scale_art && resized { rect_map(was, now) } else { None };
|
||||
let art = if fit.is_some() {
|
||||
// Locked and hidden art too? Noted in the journal, so a replay scales the same objects
|
||||
// whatever the preference is then.
|
||||
let all = bool_or(p, "lockedAndHidden", s.prefs.move_locked_with_artboard);
|
||||
s.note_journal("lockedAndHidden", json!(all));
|
||||
s.doc()?.doc.art_on_artboard(was, all)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let mut sc = match fit {
|
||||
Some(xf) if Scaling::factor(xf).is_some() => super::object::scaling(s, p),
|
||||
_ => Scaling::default(),
|
||||
};
|
||||
sc.patterns = super::object::transform_patterns(s, p, &art)?;
|
||||
s.edit("Artboard Options", |d, _| {
|
||||
let a = d.artboards.get_mut(i).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
let was = a.rect;
|
||||
a.rect = rect_from(p, a.rect);
|
||||
a.rect = now;
|
||||
if let Some(n) = str_param(p, "name") {
|
||||
a.name = n.to_string();
|
||||
}
|
||||
// Moved (not resized), it takes its guides along.
|
||||
let (id, now) = (a.id, a.rect);
|
||||
if (now.width() - was.width()).abs() <= 1e-6 && (now.height() - was.height()).abs() <= 1e-6 {
|
||||
let id = a.id;
|
||||
if let Some(xf) = fit {
|
||||
d.map_artboard_guides(id, xf);
|
||||
for id in &art {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
n.transform(xf, sc);
|
||||
}
|
||||
}
|
||||
} else if !resized {
|
||||
// Moved (not resized), it takes its guides along.
|
||||
d.move_artboard_guides(id, now.origin() - was.origin());
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
if !scale_art {
|
||||
return ok();
|
||||
}
|
||||
Ok(json!({ "scaled": art.iter().map(|i| i.0).collect::<Vec<_>>() }))
|
||||
}
|
||||
|
||||
/// The map taking artboard rectangle `from` onto `to`, each side scaled by its own ratio. None
|
||||
/// when `from` has no area or the map isn't finite (a size out of range).
|
||||
fn rect_map(from: Rect, to: Rect) -> Option<Affine> {
|
||||
if from.width() <= 1e-9 || from.height() <= 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let (sx, sy) = (to.width() / from.width(), to.height() / from.height());
|
||||
let xf = Affine::translate(to.origin().to_vec2()) * Affine::scale_non_uniform(sx, sy) * Affine::translate(-from.origin().to_vec2());
|
||||
xf.is_finite().then_some(xf)
|
||||
}
|
||||
|
||||
fn artboard_fit_art(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -209,3 +209,26 @@ fn rotated_box_rect_matches_page_bounds_when_upright() {
|
||||
sel(&mut s, &[a]);
|
||||
assert_eq!(selection_box(&s), OrientedBox::aligned(Rect::new(10.0, 20.0, 40.0, 60.0)));
|
||||
}
|
||||
|
||||
/// The Selection tool says when a drag moves, scales or rotates the selection (the canvas hides the
|
||||
/// bounding box meanwhile, #712), and not on a press that hasn't moved or a marquee.
|
||||
#[test]
|
||||
fn the_selection_tool_reports_a_transforming_drag() {
|
||||
let mut s = session();
|
||||
let id = id_of(&s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 50})).unwrap());
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("selection", v).unwrap();
|
||||
let ev = |kind, x: f64, y: f64| PointerEvent::new(kind, x, y).with_mods(Mods::default());
|
||||
s.pointer(&ev(PointerKind::Down, 150.0, 125.0), v).unwrap();
|
||||
assert!(!s.tool_transforming(), "a press that hasn't moved");
|
||||
s.pointer(&ev(PointerKind::Drag, 190.0, 160.0), v).unwrap();
|
||||
assert!(s.tool_transforming(), "moving");
|
||||
s.pointer(&ev(PointerKind::Up, 190.0, 160.0), v).unwrap();
|
||||
assert!(!s.tool_transforming());
|
||||
// A marquee on empty canvas isn't a transform.
|
||||
s.pointer(&ev(PointerKind::Down, 500.0, 400.0), v).unwrap();
|
||||
s.pointer(&ev(PointerKind::Drag, 550.0, 450.0), v).unwrap();
|
||||
assert!(s.tool_busy() && !s.tool_transforming());
|
||||
s.pointer(&ev(PointerKind::Up, 550.0, 450.0), v).unwrap();
|
||||
}
|
||||
@@ -316,6 +316,106 @@ fn artboard_move_copy_reports_the_copies_and_follows_move_art() {
|
||||
assert_eq!(ids.len(), 3, "every artboard has an id of its own");
|
||||
}
|
||||
|
||||
fn stroke_width(s: &Session, id: NodeId) -> f64 {
|
||||
s.doc().unwrap().doc.node(id).unwrap().appearance.stroke().unwrap().width
|
||||
}
|
||||
|
||||
fn guide_positions(s: &Session) -> Vec<f64> {
|
||||
s.doc().unwrap().doc.guides.iter().map(|g| g.pos).collect()
|
||||
}
|
||||
|
||||
/// Scale Artwork with Artboard (#602): `artboard.setProps {scaleArt}` takes the art fully inside the
|
||||
/// artboard and its guides from the old rectangle onto the new one, each side by its own ratio, in
|
||||
/// one undo step; art partly outside and canvas guides stay.
|
||||
#[test]
|
||||
fn artboard_set_props_scales_the_art_and_guides_with_the_artboard() {
|
||||
let mut s = session();
|
||||
let inside = rect(&mut s, 100.0, 100.0, 200.0, 100.0);
|
||||
s.execute("stroke.set", &json!({"weight": 4})).unwrap();
|
||||
let partly = rect(&mut s, 700.0, 500.0, 200.0, 200.0);
|
||||
for (vertical, pos, artboard) in [(true, 400.0, json!(0)), (false, 300.0, json!(0)), (true, 400.0, json!(null))] {
|
||||
s.execute("guide.add", &json!({"vertical": vertical, "pos": pos, "artboard": artboard})).unwrap();
|
||||
}
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
// Half the size: half the art, half the stroke (Scale Strokes & Effects).
|
||||
let out = s.execute("artboard.setProps", &json!({"index": 0, "width": 400, "height": 300, "scaleArt": true, "strokes": true})).unwrap();
|
||||
assert_eq!(out["scaled"], json!([inside.0]));
|
||||
assert_eq!(s.doc().unwrap().doc.artboards[0].rect, Rect::new(0.0, 0.0, 400.0, 300.0));
|
||||
assert_eq!(bounds(&s, inside), Rect::new(50.0, 50.0, 150.0, 100.0));
|
||||
assert_eq!(stroke_width(&s, inside), 2.0);
|
||||
assert_eq!(bounds(&s, partly), Rect::new(700.0, 500.0, 900.0, 700.0), "art partly outside stays");
|
||||
assert_eq!(guide_positions(&s), vec![200.0, 150.0, 400.0], "the artboard's guides scale, the canvas guide stays");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(*s.doc().unwrap().doc, *before, "one undo step");
|
||||
// Moved and stretched (left edge to -100, twice as wide), strokes kept: the art follows.
|
||||
s.execute("artboard.setProps", &json!({"index": 0, "x": -100, "width": 1600, "scaleArt": true, "strokes": false})).unwrap();
|
||||
assert_eq!(bounds(&s, inside), Rect::new(100.0, 100.0, 500.0, 200.0));
|
||||
assert_eq!(stroke_width(&s, inside), 4.0);
|
||||
assert_eq!(guide_positions(&s), vec![700.0, 300.0, 400.0]);
|
||||
}
|
||||
|
||||
/// Without `scaleArt` a resize leaves the art and guides alone (and returns nothing, as before);
|
||||
/// with it, a pure move scales nothing (guides move along, as they always have).
|
||||
#[test]
|
||||
fn artboard_set_props_scales_art_only_when_asked_and_resized() {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s, 100.0, 100.0, 200.0, 100.0);
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 400, "artboard": 0})).unwrap();
|
||||
assert_eq!(s.execute("artboard.setProps", &json!({"index": 0, "width": 400})).unwrap(), Value::Null);
|
||||
assert_eq!(bounds(&s, r), Rect::new(100.0, 100.0, 300.0, 200.0));
|
||||
assert_eq!(guide_positions(&s), vec![400.0]);
|
||||
let out = s.execute("artboard.setProps", &json!({"index": 0, "x": 50, "scaleArt": true})).unwrap();
|
||||
assert_eq!(out["scaled"], json!([]));
|
||||
assert_eq!(bounds(&s, r), Rect::new(100.0, 100.0, 300.0, 200.0));
|
||||
assert_eq!(guide_positions(&s), vec![450.0]);
|
||||
assert!(s.execute("artboard.setProps", &json!({"index": 9, "width": 10, "scaleArt": true})).is_err());
|
||||
}
|
||||
|
||||
/// Locked and hidden art scales only with Move Locked and Hidden Artwork with Artboard, as it
|
||||
/// moves only with it.
|
||||
#[test]
|
||||
fn scale_artwork_with_artboard_leaves_locked_art_unless_the_preference_says() {
|
||||
let mut s = session();
|
||||
let locked = rect(&mut s, 100.0, 100.0, 200.0, 100.0);
|
||||
s.execute("object.lock", &json!({})).unwrap();
|
||||
let scaled = |s: &mut Session| {
|
||||
let out = s.execute("artboard.setProps", &json!({"index": 0, "width": 400, "height": 300, "scaleArt": true})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
out["scaled"].clone()
|
||||
};
|
||||
assert_eq!(scaled(&mut s), json!([]));
|
||||
s.execute("prefs.set", &json!({"key": "moveLockedWithArtboard", "value": true})).unwrap();
|
||||
assert_eq!(scaled(&mut s), json!([locked.0]));
|
||||
// The choice is journaled: replayed with the preference off, the same art scales.
|
||||
s.execute("artboard.setProps", &json!({"index": 0, "width": 400, "height": 300, "scaleArt": true})).unwrap();
|
||||
assert_eq!(s.journal.last().unwrap().1["lockedAndHidden"], true);
|
||||
let (id, p) = s.journal.last().unwrap().clone();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("prefs.set", &json!({"key": "moveLockedWithArtboard", "value": false})).unwrap();
|
||||
assert_eq!(s.execute(&id, &p).unwrap()["scaled"], json!([locked.0]));
|
||||
}
|
||||
|
||||
/// The Artboard tool with its `scaleArt` option on resizes proportionally and the art scales with
|
||||
/// the artboard, in one undo step.
|
||||
#[test]
|
||||
fn artboard_tool_scales_art_with_the_artboard() {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s, 100.0, 100.0, 200.0, 100.0);
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
s.set_tool_options(Some("artboard"), json!({"scaleArt": true}).as_object().unwrap());
|
||||
// The right handle 100 pt out: 9/8 as wide, and as tall about the middle of the left side.
|
||||
gesture(&mut s, "artboard", &[(800.0, 300.0), (850.0, 300.0), (900.0, 300.0), (900.0, 300.0)], Mods::default());
|
||||
assert_eq!(s.doc().unwrap().doc.artboards[0].rect, Rect::new(0.0, -37.5, 900.0, 637.5));
|
||||
assert_eq!(bounds(&s, r), Rect::new(112.5, 75.0, 337.5, 187.5));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(*s.doc().unwrap().doc, *before, "one undo step");
|
||||
// Off again (the option is remembered like Move Artwork with Artboard): the art stays.
|
||||
s.set_tool_options(Some("artboard"), json!({"scaleArt": false}).as_object().unwrap());
|
||||
gesture(&mut s, "artboard", &[(800.0, 300.0), (850.0, 300.0), (900.0, 300.0), (900.0, 300.0)], Mods::default());
|
||||
assert_eq!(s.doc().unwrap().doc.artboards[0].rect, Rect::new(0.0, 0.0, 900.0, 600.0));
|
||||
assert_eq!(bounds(&s, r), Rect::new(100.0, 100.0, 300.0, 200.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magic_wand_selects_same_fill() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -358,6 +358,11 @@ impl Session {
|
||||
self.tool.busy()
|
||||
}
|
||||
|
||||
/// Is the active tool moving, scaling or rotating the selection with a drag?
|
||||
pub fn tool_transforming(&self) -> bool {
|
||||
self.tool.transforming()
|
||||
}
|
||||
|
||||
/// Does the active tool take `key` ahead of the shortcuts bound to it (see `Tool::claims_key`)?
|
||||
pub fn tool_claims_key(&mut self, key: ToolKey, view: ViewInfo) -> bool {
|
||||
self.plane_key(key).is_some() || self.with_tool_cx(view, |t, cx| t.claims_key(cx, key))
|
||||
|
||||
@@ -272,6 +272,8 @@ fn structured_junk() {
|
||||
("object.setBounds", json!({"width": -10, "height": 1e308, "reference": 99})),
|
||||
("object.distributeSpacing", json!({"axis": "horizontal", "spacing": -1e308})),
|
||||
("artboard.setProps", json!({"index": 0, "width": -1, "height": 0})),
|
||||
("artboard.setProps", json!({"index": 0, "width": 1e300, "height": 1e-300, "scaleArt": true, "strokes": true, "corners": true})),
|
||||
("artboard.setProps", json!({"index": 0, "x": -1e308, "width": 0, "height": 1, "scaleArt": true, "patterns": true})),
|
||||
("artboard.new", json!({"width": 0, "height": -1})),
|
||||
("artboard.delete", json!({"index": 0})),
|
||||
("artboard.move", json!({"index": 0, "dx": 1e308, "dy": 0, "moveArt": true})),
|
||||
|
||||
@@ -270,10 +270,12 @@ impl Writer {
|
||||
// The drawing space the page shows (the marks' area).
|
||||
let shown = sheet.view.inverse().transform_rect_bbox(paper);
|
||||
let knockout = std::mem::replace(&mut ex.knockout, false);
|
||||
// Marks keep their inks: they aren't converted to the destination.
|
||||
// Marks keep their inks: they aren't converted to the destination. The page
|
||||
// information is drawn as glyph shapes, like the other marks, not as text.
|
||||
let convert = std::mem::replace(&mut ex.convert, false);
|
||||
let outline = std::mem::replace(&mut ex.outline_text, true);
|
||||
ex.node(&mut s, art, sheet.window.map_or(shown, |r| r.intersect(shown)), true);
|
||||
(ex.knockout, ex.convert) = (knockout, convert);
|
||||
(ex.knockout, ex.convert, ex.outline_text) = (knockout, convert, outline);
|
||||
}
|
||||
if window.is_some() {
|
||||
s.pop();
|
||||
|
||||
@@ -24,6 +24,9 @@ pub(crate) struct Placement {
|
||||
pub size: f64,
|
||||
/// Width of the em ÷ its height, in percent.
|
||||
pub h_scale: f64,
|
||||
/// How far the em leans along the baseline per unit of its height (a text matrix with a
|
||||
/// shear: faux italic, or type slanted as a whole); 0 when it stands square on the baseline.
|
||||
pub slant: f64,
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
@@ -33,14 +36,23 @@ impl Placement {
|
||||
let origin = m * Point::ORIGIN;
|
||||
let ex = m * Point::new(1000.0, 0.0) - origin;
|
||||
let ey = m * Point::new(0.0, 1000.0) - origin;
|
||||
let (w, size) = (ex.hypot(), ey.hypot());
|
||||
let w = ex.hypot();
|
||||
// Upright in y-down document space: x to the right of up.
|
||||
let upright = ex.cross(ey) < 0.0;
|
||||
(origin.is_finite() && w.is_finite() && size.is_finite() && w > 0.01 && size > 0.01 && size < 1e5 && upright).then(|| Self {
|
||||
if !(w.is_finite() && w > 0.01) {
|
||||
return None;
|
||||
}
|
||||
let dir = ex / w;
|
||||
// The em's height is across the baseline; what's left of `ey` along it is a slant.
|
||||
let up = Vec2::new(dir.y, -dir.x);
|
||||
let size = ey.dot(up);
|
||||
let slant = ey.dot(dir) / size;
|
||||
(origin.is_finite() && size.is_finite() && slant.is_finite() && size > 0.01 && size < 1e5 && upright).then(|| Self {
|
||||
origin,
|
||||
dir: ex / w,
|
||||
dir,
|
||||
size,
|
||||
h_scale: w / size * 100.0,
|
||||
slant,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -211,6 +223,7 @@ impl TextLine {
|
||||
let gap = (at.origin - self.next).dot(self.last_dir);
|
||||
let on_line = opacity == self.opacity
|
||||
&& at.dir.dot(self.at.dir) > 0.9995
|
||||
&& (at.slant - self.at.slant).abs() < 1e-3
|
||||
&& (at.origin - self.at.origin).dot(self.at.up()).abs() < size * 0.15
|
||||
&& gap > -size * 0.3
|
||||
&& gap < size * 3.0;
|
||||
@@ -218,6 +231,7 @@ impl TextLine {
|
||||
// that one ends.
|
||||
let on_curve = !on_line
|
||||
&& opacity == self.opacity
|
||||
&& (at.slant - self.at.slant).abs() < 1e-3
|
||||
&& at.dir.dot(self.last_dir) > CURVE_TURN_COS
|
||||
&& at.dir.dot(self.last_dir) < 0.99999
|
||||
&& (at.origin - self.next).hypot() < size * 0.6;
|
||||
@@ -309,7 +323,8 @@ impl TextLine {
|
||||
to_logical(&mut t);
|
||||
}
|
||||
let angle = self.at.dir.atan2();
|
||||
t.xf = Affine::translate(self.at.origin.to_vec2()) * Affine::rotate(angle);
|
||||
// The glyphs' tops lean along the baseline (+x), up being -y in the type's own space.
|
||||
t.xf = Affine::translate(self.at.origin.to_vec2()) * Affine::rotate(angle) * Affine::skew(-self.at.slant, 0.0);
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
if let Some(path) = path {
|
||||
t.kind = TextKind::OnPath { path: vectorcraft_geom::PathData::from_bezpath(&path), start: 0.0, end: None };
|
||||
|
||||
@@ -373,7 +373,7 @@ pub struct AdvancedSettings {
|
||||
|
||||
impl Default for AdvancedSettings {
|
||||
fn default() -> Self {
|
||||
Self { font_subset_percent: 100.0, outline_text: true, overprint: Overprint::Preserve }
|
||||
Self { font_subset_percent: 100.0, outline_text: false, overprint: Overprint::Preserve }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -342,9 +342,13 @@ fn text_exported_as_outlines() {
|
||||
let mut d = doc(300.0, 100.0);
|
||||
let t = TextObject::point(Point::new(10.0, 50.0), "Hello", CharStyle { size: 24.0, ..CharStyle::default() });
|
||||
add(&mut d, Node::new(NodeId(0), NodeKind::Text(Box::new(t))));
|
||||
let s = uncompressed(&d);
|
||||
let mut outlined = PdfOptions::uncompressed();
|
||||
outlined.settings.advanced.outline_text = true;
|
||||
let s = String::from_utf8_lossy(&export(&d, &outlined).unwrap()).into_owned();
|
||||
assert!(!s.contains("/Font"), "no fonts when outlining text");
|
||||
let r = import_with_report(&export(&d, &PdfOptions::default()).unwrap(), &ImportOptions::default()).unwrap();
|
||||
// Real text in an embedded font by default, as a PDF from Illustrator has it (#733).
|
||||
assert!(uncompressed(&d).contains("/FontFile"), "a font embedded by default");
|
||||
let r = import_with_report(&export(&d, &outlined).unwrap(), &ImportOptions::default()).unwrap();
|
||||
let l = leaves(&r.document);
|
||||
assert!(!l.is_empty());
|
||||
assert!(l.iter().all(|n| matches!(n.kind, NodeKind::Path { .. })));
|
||||
|
||||
@@ -331,9 +331,11 @@ fn pdf_a_overprint_mode_is_0() {
|
||||
#[test]
|
||||
fn overprinting_art_imports_as_it_was_drawn() {
|
||||
let d = overprinting(RED);
|
||||
let plain = leaves(&import(&export(&d, json!({"advanced": {"overprint": "discard"}})).bytes).unwrap());
|
||||
// What is drawn (a form's bounds may come back as an unpainted clipping path).
|
||||
let painted = |bytes: &[u8]| leaves(&import(bytes).unwrap()).into_iter().filter(|l| l.2.is_some()).collect::<Vec<_>>();
|
||||
let plain = painted(&export(&d, json!({"advanced": {"overprint": "discard"}})).bytes);
|
||||
for v in [json!({}), json!({"createLayers": true})] {
|
||||
assert_eq!(leaves(&import(&export(&d, v.clone()).bytes).unwrap()), plain, "{v}: the marks draw nothing");
|
||||
assert_eq!(painted(&export(&d, v.clone()).bytes), plain, "{v}: the marks draw nothing");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ fn a_single_upright_glyph_comes_in_as_vertical_type_and_takes_no_horizontal_glyp
|
||||
fill: Some(vectorcraft_color::Paint::solid(vectorcraft_color::Color::rgb(0.0, 0.0, 0.0))),
|
||||
stroke: None,
|
||||
};
|
||||
let at = |x: f64, y: f64| Placement { origin: Point::new(x, y), dir: Vec2::new(1.0, 0.0), size: 20.0, h_scale: 100.0 };
|
||||
let at = |x: f64, y: f64| Placement { origin: Point::new(x, y), dir: Vec2::new(1.0, 0.0), size: 20.0, h_scale: 100.0, slant: 0.0 };
|
||||
let mut line = TextLine::new(at(100.0, 100.0), 1.0);
|
||||
assert!(line.push_upright(&look, at(100.0, 100.0), 1.0, Upright { top: Point::new(110.0, 82.0) }, "§"));
|
||||
assert!(!line.push(&look, at(130.0, 100.0), 1.0, 10.0, "a"), "a horizontal glyph starts another line");
|
||||
@@ -288,7 +288,7 @@ fn letter_spaced_vertical_type_keeps_its_characters_together() {
|
||||
stroke: None,
|
||||
};
|
||||
// Glyphs set down a column 1.5 em apart (tracking 500): no spaces come in between them.
|
||||
let at = |y: f64| Placement { origin: Point::new(100.0, y), dir: Vec2::new(1.0, 0.0), size: 20.0, h_scale: 100.0 };
|
||||
let at = |y: f64| Placement { origin: Point::new(100.0, y), dir: Vec2::new(1.0, 0.0), size: 20.0, h_scale: 100.0, slant: 0.0 };
|
||||
let mut line = TextLine::new(at(100.0), 1.0);
|
||||
for (i, c) in ["§", "§", "§"].iter().enumerate() {
|
||||
let y = 100.0 + 30.0 * i as f64;
|
||||
|
||||
@@ -274,3 +274,19 @@ fn an_embedded_fonts_own_cmap_names_its_glyphs() {
|
||||
}
|
||||
assert!(crate::import::font_chars(b"not a font").is_none());
|
||||
}
|
||||
|
||||
/// Type whose text matrix shears its glyphs (slanted as a whole, as Illustrator writes type
|
||||
/// turned and skewed) reopens as type that still leans: drawn, it covers what its outlines do.
|
||||
#[test]
|
||||
fn slanted_type_reopens_as_type_that_still_leans() {
|
||||
for (turn, lean) in [(0.0, 0.25), (-0.2, 0.25), (0.3, -0.15)] {
|
||||
let mut t = TextObject::point(Point::new(80.0, 150.0), "LEAN", style(60.0));
|
||||
t.xf *= Affine::rotate(turn) * Affine::skew(lean, 0.0);
|
||||
let d = doc(vec![t]);
|
||||
let reopened = import_as(&pdf(&d, false).bytes, TextAs::Text);
|
||||
assert_eq!(texts(&reopened), ["LEAN"], "{turn} {lean}: reopened as type");
|
||||
let (a, b) = (ink(&import_as(&pdf(&reopened, true).bytes, TextAs::Outlines)), ink(&import_as(&pdf(&d, true).bytes, TextAs::Outlines)));
|
||||
let off = [a.x0 - b.x0, a.y0 - b.y0, a.x1 - b.x1, a.y1 - b.y1].iter().fold(0.0f64, |m, v| m.max(v.abs()));
|
||||
assert!(off < 0.5, "{turn} {lean}: {a:?} vs {b:?}");
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ fn options_not_applied_yet_come_back_as_warnings() {
|
||||
json!({"output": {"outputIntent": vectorcraft_color::cms::GENERIC_CMYK, "outputCondition": "Press", "registry": "r", "trapped": true}}),
|
||||
json!({"output": {"outputConditionId": "CGATS TR 001"}}),
|
||||
json!({"advanced": {"outlineText": false}}),
|
||||
json!({"advanced": {"fontSubsetPercent": 35}}),
|
||||
json!({"advanced": {"outlineText": true, "fontSubsetPercent": 35}}),
|
||||
json!({"bleed": {"useDocument": true, "top": 9}}),
|
||||
json!({"bleed": {"top": 9}, "marks": {"trim": true, "registration": true, "colorBars": true, "pageInfo": true}}),
|
||||
json!({"includeNonPrinting": true}),
|
||||
|
||||
@@ -85,7 +85,8 @@ pub struct RenderOptions {
|
||||
pub overprint_preview: bool,
|
||||
/// Trim View: clip the artwork to the artboards (nothing on the pasteboard is drawn).
|
||||
pub trim: bool,
|
||||
/// Leave template layers out (exports and thumbnails: templates are guides, not artwork).
|
||||
/// Leave template layers and guides out (exports and thumbnails: they're aids to drawing, not
|
||||
/// artwork).
|
||||
pub skip_templates: bool,
|
||||
/// Pattern editing mode's tile edge and swatch bounds colour (Object → Pattern → Tile Edge
|
||||
/// Color), RGB.
|
||||
@@ -725,9 +726,9 @@ impl Renderer {
|
||||
Some(p)
|
||||
}
|
||||
|
||||
/// Whether `a` is left out of this frame: hidden, a skipped template, or culled.
|
||||
/// Whether `a` is left out of this frame: hidden, a skipped template or guide, or culled.
|
||||
fn skipped(&mut self, f: &Frame, a: &Arc<Node>) -> bool {
|
||||
let skipped_template = f.opts.skip_templates && matches!(a.kind, NodeKind::Layer { template: true, .. });
|
||||
let skipped_template = f.opts.skip_templates && matches!(a.kind, NodeKind::Layer { template: true, .. } | NodeKind::Path { guide: true, .. });
|
||||
if !a.visible || skipped_template || f.opts.hidden.contains(&a.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -66,3 +66,22 @@ fn dim_images_fades_only_the_images() {
|
||||
assert_eq!(img.pixel(25, 25), [255, 0, 0, 255], "vector art stays");
|
||||
assert_eq!(render(&d, true).pixel(65, 25), [255, 0, 0, 255], "exports are not dimmed");
|
||||
}
|
||||
|
||||
/// A guide path draws on screen only: exports leave it out, as they leave templates out (and as
|
||||
/// the SVG, PSD and PDF exports and printing already do).
|
||||
#[test]
|
||||
fn guides_draw_on_screen_only() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let l = d.layers[0].id;
|
||||
let mut g = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(70.0, 70.0, 90.0, 90.0)), Appearance::default());
|
||||
if let NodeKind::Path { guide, .. } = &mut g.kind {
|
||||
*guide = true;
|
||||
}
|
||||
d.insert(Some(l), 0, g).unwrap();
|
||||
let drawn = |export: bool| {
|
||||
let r = render(&d, export);
|
||||
(60..100).flat_map(|y| (60..100).map(move |x| (x, y))).filter(|&(x, y)| r.pixel(x, y) != [255, 255, 255, 255]).count()
|
||||
};
|
||||
assert!(drawn(false) > 0, "on screen the guide shows");
|
||||
assert_eq!(drawn(true), 0, "exports leave it out");
|
||||
}
|
||||
@@ -517,6 +517,11 @@ pub trait Tool: Send {
|
||||
fn busy(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Is a drag moving, scaling or rotating the selection (the canvas then hides the bounding box,
|
||||
/// so only the art is seen going with the pointer)?
|
||||
fn transforming(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Does the tool take `key` now, ahead of the command shortcuts bound to it (the Gradient tool
|
||||
/// with a stop selected takes Delete and the arrows)?
|
||||
fn claims_key(&self, _cx: &ToolContext, _key: ToolKey) -> bool {
|
||||
|
||||
@@ -163,6 +163,10 @@ impl Tool for SelectionTool {
|
||||
!matches!(self.state, State::Idle) || self.guide.busy()
|
||||
}
|
||||
|
||||
fn transforming(&self) -> bool {
|
||||
matches!(self.state, State::Moving { began: true, .. } | State::Scaling { .. } | State::Rotating { .. })
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
if let Some(out) = self.guide.pointer(cx, ev) {
|
||||
return out;
|
||||
|
||||
@@ -34,7 +34,7 @@ const ROWS: &[Row] = &[
|
||||
(&["polygon"], None, &["sides"]),
|
||||
(&["star"], None, &["points"]),
|
||||
(&["freeTransform"], None, &["constrain"]),
|
||||
(&["artboard"], None, &["moveArt"]),
|
||||
(&["artboard"], None, &["moveArt", "scaleArt"]),
|
||||
];
|
||||
|
||||
/// The stores `tool`'s persistent options live in, each with the option keys it holds.
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
//! Click an artboard to make it active (dashed bounds, 8 handles and its name). Drag inside moves
|
||||
//! it (with its artwork when the `moveArt` option is on, Shift constrains; Alt moves a copy and
|
||||
//! leaves the artboard where it was), drag a handle resizes it
|
||||
//! (Shift proportional, Alt from centre), drag on the pasteboard draws a new artboard, Delete removes
|
||||
//! (Shift proportional, Alt from centre; with the `scaleArt` option on, Scale Artwork with Artboard,
|
||||
//! always proportional, and the art fully inside scales with it), drag on the pasteboard draws a new artboard, Delete removes
|
||||
//! the active one (Copy, Cut and Paste take it with its art: `artboard.copy`) and Escape returns to
|
||||
//! the Selection tool. Moving and resizing snap like drawing
|
||||
//! does: to whole pixels, to the grid, or with Smart Guides to other artboards, their bleed and
|
||||
//! objects (never to the dragged artboard or the art moving with it); the artboard's own bleed edges
|
||||
//! objects (never to the dragged artboard or the art moving or scaling with it); the artboard's own bleed edges
|
||||
//! snap too. A new artboard's corners snap as drawn points do ([`DrawSnap`]).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_geom::{Point, Rect, Vec2};
|
||||
use vectorcraft_geom::{Affine, Point, Rect, Vec2};
|
||||
|
||||
use super::{BLUE, polygon, rect_corners};
|
||||
use crate::bbox::{Handle, hit_handle, move_delta, scale_for_drag};
|
||||
@@ -49,6 +50,9 @@ pub struct ArtboardTool {
|
||||
pub active: usize,
|
||||
/// Move artwork with the artboard.
|
||||
pub move_art: bool,
|
||||
/// Scale artwork with the artboard: a resize takes the art fully inside it along, and keeps the
|
||||
/// artboard's proportions.
|
||||
pub scale_art: bool,
|
||||
drag: Option<Drag>,
|
||||
preview: Option<Rect>,
|
||||
/// Smart Guide targets, gathered when a move or resize starts.
|
||||
@@ -63,7 +67,17 @@ pub struct ArtboardTool {
|
||||
|
||||
impl Default for ArtboardTool {
|
||||
fn default() -> Self {
|
||||
Self { active: 0, move_art: true, drag: None, preview: None, targets: None, copy_targets: None, guides: vec![], draw: DrawSnap::default() }
|
||||
Self {
|
||||
active: 0,
|
||||
move_art: true,
|
||||
scale_art: false,
|
||||
drag: None,
|
||||
preview: None,
|
||||
targets: None,
|
||||
copy_targets: None,
|
||||
guides: vec![],
|
||||
draw: DrawSnap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +154,20 @@ impl ArtboardTool {
|
||||
}
|
||||
}
|
||||
|
||||
/// An artboard's shortest side (as `artboard.setProps` keeps it).
|
||||
const MIN_SIDE: f64 = 1.0;
|
||||
|
||||
/// `r` grown about `anchor` until both sides are at least [`MIN_SIDE`], by one factor, so a
|
||||
/// proportional resize keeps its proportions where the command would lengthen only the short side.
|
||||
fn keep_min_side(r: Rect, anchor: Point) -> Rect {
|
||||
let short = r.width().min(r.height());
|
||||
if short >= MIN_SIDE || short <= 0.0 || !short.is_finite() {
|
||||
return r;
|
||||
}
|
||||
let k = MIN_SIDE / short;
|
||||
(Affine::translate(anchor.to_vec2()) * Affine::scale(k) * Affine::translate(-anchor.to_vec2())).transform_rect_bbox(r)
|
||||
}
|
||||
|
||||
/// Where the bleed edge lies beyond a handle, along the axes the handle moves.
|
||||
fn bleed_offset(cx: &ToolContext, rect: Rect, handle: Handle) -> Vec2 {
|
||||
let d = handle.pos(cx.doc.setup.bleed_rect(rect)) - handle.pos(rect);
|
||||
@@ -206,13 +234,21 @@ impl Tool for ArtboardTool {
|
||||
let mut out = vec![];
|
||||
if !began {
|
||||
out.push(Action::Begin("Resize Artboard".into()));
|
||||
self.begin_snapping(cx, index, false);
|
||||
self.begin_snapping(cx, index, self.scale_art);
|
||||
}
|
||||
self.drag = Some(Drag::Resize { index, handle, rect, grab, began: true });
|
||||
let h = self.snap_handle(cx, p + grab, bleed_offset(cx, rect, handle));
|
||||
let nr = scale_for_drag(rect, handle, h, m.shift, m.alt).transform_rect_bbox(rect);
|
||||
let proportional = m.shift || self.scale_art;
|
||||
let mut nr = scale_for_drag(rect, handle, h, proportional, m.alt).transform_rect_bbox(rect);
|
||||
if proportional {
|
||||
nr = keep_min_side(nr, if m.alt { rect.center() } else { handle.opposite().pos(rect) });
|
||||
}
|
||||
self.preview = Some(nr);
|
||||
out.push(Action::Preview("artboard.setProps".into(), rect_json(index, nr)));
|
||||
let mut v = rect_json(index, nr);
|
||||
if self.scale_art {
|
||||
v["scaleArt"] = json!(true);
|
||||
}
|
||||
out.push(Action::Preview("artboard.setProps".into(), v));
|
||||
out
|
||||
}
|
||||
(PointerKind::Drag, Some(Drag::Create { start, .. })) => {
|
||||
@@ -338,13 +374,14 @@ impl Tool for ArtboardTool {
|
||||
}
|
||||
|
||||
fn options(&self) -> Value {
|
||||
json!({ "active": self.active, "moveArt": self.move_art })
|
||||
json!({ "active": self.active, "moveArt": self.move_art, "scaleArt": self.scale_art })
|
||||
}
|
||||
|
||||
fn set_option(&mut self, key: &str, value: &Value) {
|
||||
match key {
|
||||
"active" => self.active = value.as_u64().unwrap_or(0) as usize,
|
||||
"moveArt" => self.move_art = value.as_bool().unwrap_or(true),
|
||||
"scaleArt" => self.scale_art = value.as_bool().unwrap_or(false),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -483,6 +520,49 @@ mod tests {
|
||||
assert_eq!(v["width"].as_f64(), Some(210.0));
|
||||
}
|
||||
|
||||
/// Scale Artwork with Artboard (#602): the resize asks for the art to scale, keeps the artboard's
|
||||
/// proportions, and never snaps to the art scaling with it.
|
||||
#[test]
|
||||
fn scale_art_resizes_proportionally_without_snapping_to_its_art() {
|
||||
let mut d = two_boards();
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let art = vectorcraft_geom::shapes::rectangle(Rect::new(700.0, 50.0, 710.0, 60.0));
|
||||
d.insert(Some(l), 0, vectorcraft_doc::Node::path(id, art, vectorcraft_doc::Appearance::default_art())).unwrap();
|
||||
let (s, p) = (Selection::default(), paint());
|
||||
let c = cx(&d, &s, &p);
|
||||
// Off: the right handle 3 pt past the art's right edge (710) lands on it; the height stays.
|
||||
let mut t = ArtboardTool { active: 1, ..Default::default() };
|
||||
t.pointer(&c, &ev(PointerKind::Down, 800.0, 100.0));
|
||||
let v = preview_params(&t.pointer(&c, &ev(PointerKind::Drag, 713.0, 100.0)));
|
||||
assert_eq!((v["width"].as_f64(), v["height"].as_f64(), v.get("scaleArt")), (Some(110.0), Some(200.0), None));
|
||||
t.pointer(&c, &ev(PointerKind::Up, 713.0, 100.0));
|
||||
// On: the art scales along, so it's no target, and the height follows the width.
|
||||
t.set_option("scaleArt", &json!(true));
|
||||
assert_eq!(t.options()["scaleArt"], true);
|
||||
t.pointer(&c, &ev(PointerKind::Down, 800.0, 100.0));
|
||||
let v = preview_params(&t.pointer(&c, &ev(PointerKind::Drag, 713.0, 100.0)));
|
||||
assert_eq!((v["width"].as_f64(), v["scaleArt"].as_bool()), (Some(113.0), Some(true)));
|
||||
assert!((v["height"].as_f64().unwrap() - 113.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// A proportional resize never asks for a side under 1 pt, which the command would lengthen
|
||||
/// alone (scaling the art unevenly): both sides grow by one factor instead.
|
||||
#[test]
|
||||
fn a_proportional_resize_keeps_its_proportions_at_the_smallest_size() {
|
||||
let mut d = two_boards();
|
||||
d.artboards[1].rect = Rect::new(600.0, 0.0, 800.0, 20.0);
|
||||
let (s, p) = (Selection::default(), paint());
|
||||
let mut c = cx(&d, &s, &p);
|
||||
c.smart_guides = false;
|
||||
let mut t = ArtboardTool { active: 1, scale_art: true, ..Default::default() };
|
||||
// The right handle to 2.5 % of the width: 5 × 0.5, grown to 10 × 1 about the left middle.
|
||||
t.pointer(&c, &ev(PointerKind::Down, 800.0, 10.0));
|
||||
let v = preview_params(&t.pointer(&c, &ev(PointerKind::Drag, 605.0, 10.0)));
|
||||
let got = ["x", "y", "width", "height"].map(|k| v[k].as_f64().unwrap());
|
||||
assert!(got.iter().zip([600.0, 9.5, 10.0, 1.0]).all(|(a, b)| (a - b).abs() < 1e-9), "{got:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bleed_edges_snap_to_bleed_edges() {
|
||||
let mut d = two_boards();
|
||||
|
||||
@@ -1541,8 +1541,10 @@ fn print_tiling_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
||||
const PRINT_TILING: Color32 = Color32::from_gray(96);
|
||||
|
||||
fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
||||
// The Selection tool's bounding box (rotated with the objects after a rotation).
|
||||
// The Selection tool's bounding box (rotated with the objects after a rotation). It hides while
|
||||
// a drag moves or resizes the selection, so only the art is seen going with the pointer (#712).
|
||||
let show_box = app.session.tool_id() == "selection"
|
||||
&& !app.session.tool_transforming()
|
||||
&& app.ui.view.bounding_box
|
||||
&& app.session.active().is_some_and(|st| !st.selection.is_empty() && st.selection.anchors.is_empty());
|
||||
let bbox = if show_box { app.selection_box() } else { None };
|
||||
|
||||
@@ -323,26 +323,55 @@ fn tab_title(d: &vectorcraft_engine::DocState, zoom: f64, outline: bool) -> Stri
|
||||
/// Linux and the web).
|
||||
const CLOSE_BEFORE_TITLE: bool = cfg!(target_os = "macos");
|
||||
|
||||
/// The first of tabs `widths` wide that the strip shows in `room`: the first one, or a later one so
|
||||
/// that the `active` tab fits.
|
||||
fn first_tab_shown(widths: &[f32], active: Option<usize>, room: f32) -> usize {
|
||||
let Some(a) = active else { return 0 };
|
||||
let mut first = 0;
|
||||
while first < a && widths.get(first..=a).map_or(0.0, |w| w.iter().sum::<f32>()) > room {
|
||||
first += 1;
|
||||
}
|
||||
first
|
||||
}
|
||||
|
||||
/// Document tab strip: "Name* @ 66.67% (RGB/Preview)". User Interface › Large Tabs makes the tabs
|
||||
/// taller, with larger titles.
|
||||
/// taller, with larger titles. Tabs that don't fit are reached from a » button at the strip's
|
||||
/// right end, which lists every open document, and the active tab is always in view (#746).
|
||||
pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
/// The width of the » button.
|
||||
const MORE: f32 = 30.0;
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (height, title_size) = if app.session.prefs.large_tabs { (44.0, 14.0) } else { (35.0, 12.5) };
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(ui.available_width(), height), Sense::hover());
|
||||
ui.painter().rect_filled(strip, 0.0, t.tab_strip);
|
||||
ui.painter().line_segment([strip.left_bottom(), strip.right_bottom()], Stroke::new(1.0, t.border));
|
||||
// On the Home screen no tab is the current one.
|
||||
let active = app.session.active_index().filter(|_| app.ui.home.is_none());
|
||||
let tabs: Vec<(String, std::sync::Arc<egui::Galley>)> = app
|
||||
.session
|
||||
.documents()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, d)| {
|
||||
let zoom = app.views.get(i).map(|v| v.zoom).unwrap_or(1.0);
|
||||
let title = tab_title(d, zoom, app.ui.view.outline);
|
||||
let color = if Some(i) == active { t.text_strong } else { t.text_dim };
|
||||
(title.clone(), ui.painter().layout_no_wrap(title, theme::semibold(title_size), color))
|
||||
})
|
||||
.collect();
|
||||
let widths: Vec<f32> = tabs.iter().map(|(_, g)| g.size().x + 50.0).collect();
|
||||
let overflow = widths.iter().sum::<f32>() > strip.width();
|
||||
let room = strip.width() - if overflow { MORE } else { 0.0 };
|
||||
let first = if overflow { first_tab_shown(&widths, active, room) } else { 0 };
|
||||
let mut x = strip.left();
|
||||
let mut activate = None;
|
||||
let mut close = None;
|
||||
let active = app.session.active_index();
|
||||
for (i, d) in app.session.documents().iter().enumerate() {
|
||||
let zoom = app.views.get(i).map(|v| v.zoom).unwrap_or(1.0);
|
||||
let title = tab_title(d, zoom, app.ui.view.outline);
|
||||
// On the Home screen no tab is the current one.
|
||||
let is_active = Some(i) == active && app.ui.home.is_none();
|
||||
let galley = ui.painter().layout_no_wrap(title, theme::semibold(title_size), if is_active { t.text_strong } else { t.text_dim });
|
||||
let w = galley.size().x + 50.0;
|
||||
let r = egui::Rect::from_min_size(egui::pos2(x, strip.top()), vec2(w, strip.height() - 1.0));
|
||||
for (i, ((_, galley), w)) in tabs.iter().zip(&widths).enumerate().skip(first) {
|
||||
if x + w > strip.left() + room && Some(i) != active {
|
||||
break;
|
||||
}
|
||||
let is_active = Some(i) == active;
|
||||
let r = egui::Rect::from_min_size(egui::pos2(x, strip.top()), vec2(*w, strip.height() - 1.0));
|
||||
let resp = ui.interact(r, ui.id().with(("tab", i)), Sense::click());
|
||||
if is_active {
|
||||
ui.painter().rect_filled(r, 0.0, t.panel);
|
||||
@@ -356,7 +385,7 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let xr = egui::Rect::from_center_size(egui::pos2(x_center, r.center().y), vec2(12.0, 12.0));
|
||||
let xresp = ui.interact(xr.expand(3.0), ui.id().with(("tabx", i)), Sense::click());
|
||||
icons::paint(ui, "x", xr, if xresp.hovered() { t.text_strong } else { t.text });
|
||||
ui.painter().galley(egui::pos2(title_left, r.center().y - galley.size().y / 2.0), galley, t.text);
|
||||
ui.painter().galley(egui::pos2(title_left, r.center().y - galley.size().y / 2.0), galley.clone(), t.text);
|
||||
if xresp.clicked() {
|
||||
close = Some(i);
|
||||
} else if resp.clicked() {
|
||||
@@ -364,6 +393,22 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
x += w;
|
||||
}
|
||||
// The » button: every open document, the active one checked.
|
||||
if overflow {
|
||||
let more = egui::Rect::from_min_max(egui::pos2(strip.right() - MORE, strip.top()), egui::pos2(strip.right(), strip.bottom() - 1.0));
|
||||
let resp = ui.interact(more, ui.id().with("tabs-more"), Sense::click());
|
||||
ui.painter().rect_filled(more, 0.0, if resp.hovered() { t.hover.gamma_multiply(0.4) } else { t.tab_strip });
|
||||
icons::paint(ui, "chevrons-right", egui::Rect::from_center_size(more.center(), vec2(14.0, 14.0)), t.text);
|
||||
let resp = resp.on_hover_text(tl!("Show all open documents"));
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
for (i, (title, _)) in tabs.iter().enumerate() {
|
||||
if widgets::menu_item_name(ui, title, true, Some(i) == active) {
|
||||
activate = Some(i);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some(i) = close {
|
||||
if let Err(e) = crate::unsaved::close(app, i) {
|
||||
app.status(e);
|
||||
@@ -764,6 +809,16 @@ pub fn hint_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
/// With more tabs than fit, the strip starts late enough to show the active one (#746).
|
||||
#[test]
|
||||
fn the_active_tab_stays_in_view() {
|
||||
let widths = [100.0; 10];
|
||||
assert_eq!(super::first_tab_shown(&widths, Some(2), 350.0), 0, "it fits from the start");
|
||||
assert_eq!(super::first_tab_shown(&widths, Some(9), 350.0), 7, "the last three tabs shown");
|
||||
assert_eq!(super::first_tab_shown(&widths, None, 350.0), 0);
|
||||
assert_eq!(super::first_tab_shown(&widths, Some(4), 50.0), 4, "a tab wider than the room still shows");
|
||||
}
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
|
||||
@@ -1991,6 +1991,7 @@
|
||||
Arrange by Row Organizar en una fila
|
||||
Arrange by Column Organizar en una columna
|
||||
Move Artwork with Artboard Mover la ilustración con la mesa de trabajo
|
||||
Scale Artwork with Artboard Escalar la ilustración con la mesa de trabajo
|
||||
Artboards: {n} Mesas de trabajo: {n}
|
||||
Rearrange All Artboards… Reorganizar todas las mesas de trabajo…
|
||||
Recall Selection Recuperar selección
|
||||
@@ -2647,6 +2648,7 @@
|
||||
Swatch Tiles Mosaicos de muestras
|
||||
Swatches Muestras
|
||||
Switch workspace Cambiar espacio de trabajo
|
||||
Show all open documents Mostrar todos los documentos abiertos
|
||||
Symbol Instance Instancia de símbolo
|
||||
Symbol Libraries Bibliotecas de símbolos
|
||||
Symbol Libraries (on the roadmap) Bibliotecas de símbolos (en la hoja de ruta)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1988,6 +1988,7 @@
|
||||
Arrange by Row Disposer en ligne
|
||||
Arrange by Column Disposer en colonne
|
||||
Move Artwork with Artboard Déplacer l’illustration avec le plan de travail
|
||||
Scale Artwork with Artboard Mettre à l’échelle l’illustration avec le plan de travail
|
||||
Artboards: {n} Plans de travail : {n}
|
||||
Rearrange All Artboards… Réorganiser tous les plans de travail…
|
||||
Recall Selection Rappeler la sélection
|
||||
@@ -2644,6 +2645,7 @@
|
||||
Swatch Tiles Carreaux de nuances
|
||||
Swatches Nuancier
|
||||
Switch workspace Changer d’espace de travail
|
||||
Show all open documents Afficher tous les documents ouverts
|
||||
Symbol Instance Occurrence de symbole
|
||||
Symbol Libraries Bibliothèques de symboles
|
||||
Symbol Libraries (on the roadmap) Bibliothèques de symboles (prévu dans la feuille de route)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1991,6 +1991,7 @@
|
||||
Arrange by Row Disponi in una riga
|
||||
Arrange by Column Disponi in una colonna
|
||||
Move Artwork with Artboard Sposta la grafica con la tavola da disegno
|
||||
Scale Artwork with Artboard Scala la grafica con la tavola da disegno
|
||||
Artboards: {n} Tavole da disegno: {n}
|
||||
Rearrange All Artboards… Ridisponi tutte le tavole da disegno…
|
||||
Recall Selection Richiama selezione
|
||||
@@ -2647,6 +2648,7 @@
|
||||
Swatch Tiles Riquadri campioni
|
||||
Swatches Campioni
|
||||
Switch workspace Cambia area di lavoro
|
||||
Show all open documents Mostra tutti i documenti aperti
|
||||
Symbol Instance Istanza di simbolo
|
||||
Symbol Libraries Librerie simboli
|
||||
Symbol Libraries (on the roadmap) Librerie simboli (in programma)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2006,6 +2006,7 @@
|
||||
Arrange by Row 1 行に配置
|
||||
Arrange by Column 1 列に配置
|
||||
Move Artwork with Artboard アートボードと一緒にアートワークを移動
|
||||
Scale Artwork with Artboard アートボードと一緒にアートワークを拡大・縮小
|
||||
Artboards: {n} アートボード: {n}
|
||||
Recall Selection 選択範囲を呼び出し
|
||||
Recent Colors 最近使用したカラー
|
||||
@@ -2421,6 +2422,7 @@
|
||||
Swatch Options… スウォッチオプション…
|
||||
Swatch Tiles スウォッチタイル
|
||||
Switch workspace ワークスペースを切り替え
|
||||
Show all open documents 開いているすべてのドキュメントを表示
|
||||
Symbol Libraries (on the roadmap) シンボルライブラリ (今後対応予定)
|
||||
Symbol Screener Tool シンボルスクリーナーツール
|
||||
Symbol Scruncher Tool シンボルスクランチャーツール
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1760,6 +1760,7 @@
|
||||
Arrange by Row Organizar em Linha
|
||||
Arrange by Column Organizar em Coluna
|
||||
Move Artwork with Artboard Mover Arte com a Mesa de Trabalho
|
||||
Scale Artwork with Artboard Dimensionar Arte com a Mesa de Trabalho
|
||||
Artboards: {n} Mesas de Trabalho: {n}
|
||||
Rearrange All Artboards… Reorganizar Todas as Mesas de Trabalho…
|
||||
Recall Selection Relembrar Seleção
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1987,6 +1987,7 @@
|
||||
Arrange by Row Расположить в строку
|
||||
Arrange by Column Расположить в столбец
|
||||
Move Artwork with Artboard Перемещать иллюстрацию вместе с монтажной областью
|
||||
Scale Artwork with Artboard Масштабировать иллюстрацию вместе с монтажной областью
|
||||
Artboards: {n} Монтажные области: {n}
|
||||
Rearrange All Artboards… Переставить все монтажные области…
|
||||
Recall Selection Восстановить выделение
|
||||
@@ -2643,6 +2644,7 @@
|
||||
Swatch Tiles Плитки образцов
|
||||
Swatches Образцы
|
||||
Switch workspace Переключить рабочую среду
|
||||
Show all open documents Показать все открытые документы
|
||||
Symbol Instance Экземпляр символа
|
||||
Symbol Libraries Библиотеки символов
|
||||
Symbol Libraries (on the roadmap) Библиотеки символов (в плане развития)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1945,6 +1945,7 @@
|
||||
Arrange by Row 排成一行
|
||||
Arrange by Column 排成一列
|
||||
Move Artwork with Artboard 随画板移动图稿
|
||||
Scale Artwork with Artboard 随画板缩放图稿
|
||||
Artboards: {n} 画板:{n}
|
||||
Rearrange All Artboards… 重新排列所有画板…
|
||||
Recall Selection 召回选区
|
||||
@@ -2564,6 +2565,7 @@
|
||||
Swatch Tiles 色板拼贴
|
||||
Swatches 色板
|
||||
Switch workspace 切换工作区
|
||||
Show all open documents 显示所有打开的文档
|
||||
Symbol Instance 符号实例
|
||||
Symbol Libraries 符号库
|
||||
Symbol Libraries (on the roadmap) 符号库 (已列入路线图)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1994,6 +1994,7 @@
|
||||
Arrange by Row 排成一列
|
||||
Arrange by Column 排成一欄
|
||||
Move Artwork with Artboard 隨工作區域移動圖稿
|
||||
Scale Artwork with Artboard 隨工作區域縮放圖稿
|
||||
Artboards: {n} 工作區域:{n}
|
||||
Rearrange All Artboards… 重新排列所有工作區域…
|
||||
Recall Selection 叫出選取範圍
|
||||
@@ -2650,6 +2651,7 @@
|
||||
Swatch Tiles 色票拼貼
|
||||
Swatches 色票
|
||||
Switch workspace 切換工作區
|
||||
Show all open documents 顯示所有開啟的文件
|
||||
Symbol Instance 符號範例
|
||||
Symbol Libraries 符號資料庫
|
||||
Symbol Libraries (on the roadmap) 符號資料庫 (已列入路線圖)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -12,6 +12,24 @@ use crate::theme::Tokens;
|
||||
use crate::widgets::{self, menu_item};
|
||||
use crate::{VectorcraftApp, icons};
|
||||
|
||||
/// The Artboard tool's Move Artwork with Artboard and Scale Artwork with Artboard options (#602) as
|
||||
/// check boxes: the Control bar and Properties show them while the tool is in use.
|
||||
pub(crate) fn art_options(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let opts = app.session.tool_options();
|
||||
for (key, label, default) in [("moveArt", tl!("Move Artwork with Artboard"), true), ("scaleArt", tl!("Scale Artwork with Artboard"), false)] {
|
||||
let on = opts[key].as_bool().unwrap_or(default);
|
||||
if widgets::check(ui, label, on, true) {
|
||||
app.run("tool.setOption", json!({ "key": key, "value": !on })).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether artboards resized while the Artboard tool is in use take their art along (its Scale
|
||||
/// Artwork with Artboard option): the panels' sizes pass it to `artboard.setProps` as `scaleArt`.
|
||||
pub(crate) fn scale_art(app: &VectorcraftApp) -> bool {
|
||||
app.session.tool_id() == "artboard" && app.session.tool_options()["scaleArt"].as_bool().unwrap_or(false)
|
||||
}
|
||||
|
||||
/// The active artboard (of `n`).
|
||||
pub(crate) fn selected(app: &VectorcraftApp, n: usize) -> usize {
|
||||
app.view().map_or(0, |v| v.artboard).min(n.saturating_sub(1))
|
||||
|
||||
@@ -198,12 +198,14 @@ fn multi_color(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool {
|
||||
}
|
||||
|
||||
/// Edit Artboards (the Artboard tool): the active artboard's name, position and size, its preset and
|
||||
/// orientation (#671), New Artboard and Delete Artboard, and Quick Actions: Rearrange All (#681) and
|
||||
/// orientation (#671) (with Scale Artwork with Artboard on, a new size scales its art, #602), Move
|
||||
/// and Scale Artwork with Artboard, New Artboard and Delete Artboard, and Quick Actions: Rearrange All (#681) and
|
||||
/// Exit back to the Selection tool (#530).
|
||||
fn artboard_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let units = app.session.general_unit();
|
||||
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
|
||||
let i = super::artboards::selected(app, n);
|
||||
let scale_art = super::artboards::scale_art(app);
|
||||
if let Some(ab) = app.session.active().and_then(|d| d.doc.artboards.get(i)).cloned() {
|
||||
// Its name as it is (names are never translated).
|
||||
ui.label(egui::RichText::new(&ab.name).size(13.0).color(Tokens::get(ui.ctx()).text));
|
||||
@@ -216,13 +218,15 @@ fn artboard_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
for (label, key, v) in [(l1, k1, v1), (l2, k2, v2)] {
|
||||
dim_label(ui, label);
|
||||
if let Some(v) = widgets::num_field(ui, ("ab", key, row), Some(v), units, fw) {
|
||||
app.run("artboard.setProps", json!({"index": i, key: v})).ok();
|
||||
app.run("artboard.setProps", json!({"index": i, key: v, "scaleArt": scale_art})).ok();
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
artboard_size_row(app, ui, i, ab.rect.width(), ab.rect.height());
|
||||
ui.add_space(2.0);
|
||||
super::artboards::art_options(app, ui);
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
let w = (ui.available_width() - 6.0) / 2.0;
|
||||
@@ -296,7 +300,8 @@ fn artboard_size_row(app: &mut VectorcraftApp, ui: &mut Ui, i: usize, w: f64, h:
|
||||
}
|
||||
});
|
||||
if let Some((width, height)) = size {
|
||||
app.run("artboard.setProps", json!({"index": i, "width": width, "height": height})).ok();
|
||||
let scale_art = super::artboards::scale_art(app);
|
||||
app.run("artboard.setProps", json!({"index": i, "width": width, "height": height, "scaleArt": scale_art})).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,4 +680,43 @@ mod tests {
|
||||
app.run("artboard.setProps", json!({"index": 0, "width": 333})).unwrap();
|
||||
assert!(frame(&mut app, &ctx, vec![]).iter().any(|(t, _)| t == "Custom"));
|
||||
}
|
||||
|
||||
/// #602: Edit Artboards shows Move and Scale Artwork with Artboard; with Scale on, a new size
|
||||
/// (here the other orientation) takes the art on the artboard along.
|
||||
#[test]
|
||||
fn edit_artboards_scales_the_art_with_scale_artwork_with_artboard() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
let r = app.run("shape.rectangle", json!({"x": 20, "y": 20, "width": 40, "height": 40})).unwrap()["id"].as_u64().unwrap();
|
||||
app.select_tool("artboard");
|
||||
let ctx = egui::Context::default();
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
assert!(texts.iter().any(|(t, _)| t == "Move Artwork with Artboard"), "{texts:?}");
|
||||
assert_eq!(app.session.tool_options()["scaleArt"], false);
|
||||
click(&mut app, &ctx, &texts, "Scale Artwork with Artboard");
|
||||
assert_eq!(app.session.tool_options()["scaleArt"], true);
|
||||
// Portrait: 100 × 200, the art scaled by ½ across and 2 down.
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
let row = texts.iter().find(|(t, _)| t == "Preset:").map(|(_, r)| *r).unwrap();
|
||||
let portrait = ctx.viewport(|vp| {
|
||||
let mut r: Vec<Rect> = vp
|
||||
.prev_pass
|
||||
.widgets
|
||||
.layers()
|
||||
.flat_map(|(_, w)| w.iter())
|
||||
.filter(|w| w.sense.senses_click() && w.rect.y_range().contains(row.center().y) && w.rect.left() > row.right())
|
||||
.map(|w| w.rect)
|
||||
.collect();
|
||||
r.sort_by(|a, b| a.left().total_cmp(&b.left()));
|
||||
r[r.len() - 2].center()
|
||||
});
|
||||
let press = |pressed| Event::PointerButton { pos: portrait, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(portrait), press(true)]);
|
||||
frame(&mut app, &ctx, vec![press(false)]);
|
||||
let d = &app.session.active().unwrap().doc;
|
||||
let ab = d.artboards[0].rect;
|
||||
assert_eq!((ab.width(), ab.height()), (100.0, 200.0));
|
||||
let b = d.node(vectorcraft_doc::NodeId(r)).unwrap().geometric_bounds().unwrap();
|
||||
assert_eq!((b.x0, b.y0, b.width(), b.height()), (10.0, 40.0, 20.0, 80.0));
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,8 @@ fn artboard_fields(app: &mut VectorcraftApp, ui: &mut Ui, index: usize, r: Rect)
|
||||
let rp = vectorcraft_geom::reference_point(r, refi);
|
||||
let set = |app: &mut VectorcraftApp, x, y, w, h| {
|
||||
let n = artboard_rect(r, refi, x, y, w, h, link);
|
||||
let p = json!({"index": index, "x": n.x0, "y": n.y0, "width": n.width(), "height": n.height()});
|
||||
let scale_art = crate::panels::artboards::scale_art(app);
|
||||
let p = json!({"index": index, "x": n.x0, "y": n.y0, "width": n.width(), "height": n.height(), "scaleArt": scale_art});
|
||||
if let Err(e) = app.run("artboard.setProps", p) {
|
||||
app.status(e);
|
||||
}
|
||||
|
||||
@@ -338,7 +338,8 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::
|
||||
}
|
||||
|
||||
/// The active tool's options in the Control bar: Mirror & Cut's axis and the side it keeps, Puppet
|
||||
/// Warp's mesh and pins (set through `tool.setOption`).
|
||||
/// Warp's mesh and pins, the Artboard tool's Move and Scale Artwork with Artboard (set through
|
||||
/// `tool.setOption`).
|
||||
pub fn control_bar_options(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
/// (value, label) of each choice.
|
||||
type Choices = &'static [(&'static str, &'static str)];
|
||||
@@ -349,6 +350,11 @@ pub fn control_bar_options(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if app.session.tool_id() == "puppetWarp" {
|
||||
return puppet_warp_options(app, ui);
|
||||
}
|
||||
if app.session.tool_id() == "artboard" {
|
||||
crate::panels::artboards::art_options(app, ui);
|
||||
ui.separator();
|
||||
return;
|
||||
}
|
||||
if app.session.tool_id() != "mirrorCut" {
|
||||
return;
|
||||
}
|
||||
|
||||
+17
-5
@@ -629,7 +629,7 @@ repeated names, and ids the document doesn't have are dropped.
|
||||
Ruler guides are numbered in the order they were made. `guide.add {vertical, pos, artboard?}` makes one (the x of a
|
||||
vertical guide, the y of a horizontal one, in points) → `{index}`. With `artboard` (an artboard's index) it is an
|
||||
artboard guide: it runs across that artboard only, moves with it (`artboard.move`, a pure move in
|
||||
`artboard.setProps`, `artboard.rearrange`), is copied with it (`artboard.duplicate`, `artboard.move {copy}`) and is
|
||||
`artboard.setProps`, `artboard.rearrange`), scales with it (`artboard.setProps {scaleArt}`), is copied with it (`artboard.duplicate`, `artboard.move {copy}`) and is
|
||||
deleted with it (`artboard.delete`); without, a canvas guide runs across the whole canvas. `guide.list` →
|
||||
`[{index, vertical, pos, selected, artboard?}…]` (`artboard` only for artboard guides).
|
||||
`guide.select {indexes: [index…], toggle?}` selects guides on their own (the art is deselected; `toggle` adds or
|
||||
@@ -1184,9 +1184,9 @@ for its colour mode). `profiles` (`all`, `destination`, or `taggedSource` for a
|
||||
`edit.assignProfile`) writes colours in ICC-based spaces with their profiles embedded: CMYK with the destination's or
|
||||
the document's CMYK profile, RGB as sRGB, grey with the sRGB tone curve. `outputIntent` embeds a profile as the file's
|
||||
`/GTS_PDFX` output intent with `outputCondition`, `outputConditionId` and `registry`, and `trapped` sets `/Trapped`
|
||||
(PDF/A files keep their own output intent). `advanced: {outlineText: false}` writes type as selectable, searchable
|
||||
text in embedded subset fonts with a ToUnicode map; fonts whose licence forbids embedding stay outlines, with a
|
||||
warning.
|
||||
(PDF/A files keep their own output intent). Type is written as selectable, searchable text in embedded subset fonts
|
||||
with a ToUnicode map (fonts whose licence forbids embedding stay outlines, with a warning); `advanced: {outlineText:
|
||||
true}` writes every glyph as an outline instead.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"document.exportPdf","params":{"path":"/tmp/press.pdf","output":{"conversion":"preserveNumbers","destination":"VectorCraft Generic CMYK (SWOP-like)","profiles":"destination","outputIntent":"VectorCraft Generic CMYK (SWOP-like)","trapped":true},"advanced":{"outlineText":false}}}}
|
||||
@@ -1268,7 +1268,8 @@ The Selection & Anchor Display and General preferences apply to `pointer_gesture
|
||||
point a selection is dragged by, a drawn point and a transform tool's reference point land on an anchor or a ruler
|
||||
guide that near.
|
||||
- `moveLockedWithArtboard`: `artboard.move {moveArt: true}`, the Artboard tool and `artboard.rearrange` move locked and
|
||||
hidden art with the artboard too; off (the default) it stays where it is.
|
||||
hidden art with the artboard too, and `artboard.setProps {scaleArt: true}` scales it too; off (the default) it stays
|
||||
where it is.
|
||||
- `penRubberBand`, `curvatureRubberBand` (on by default): off, the Pen and Curvature tools draw no preview segment to
|
||||
the pointer.
|
||||
- `showHandlesMultipleAnchors` (on by default): off, Direct Selection (and the Anchor Point tool, and the Pen with Alt)
|
||||
@@ -1551,6 +1552,17 @@ pastes only the art), in this document or another; the result's `artboard` is th
|
||||
does the same in one step → `{index, ids}`, and `artboard.move {copy: true, moveArt: true}` is the Artboard tool's
|
||||
Alt-drag.
|
||||
|
||||
Scale Artwork with Artboard (#602): `artboard.setProps {index, x?, y?, width?, height?, scaleArt: true}` resizes an
|
||||
artboard and scales the art fully inside it, and its artboard guides, from the old rectangle onto the new one, each side
|
||||
by its own ratio (keep the proportions for an even scale), in one undo step → `{scaled: [id…]}`. Locked and hidden art
|
||||
scales only with `lockedAndHidden: true` (default: prefs `moveLockedWithArtboard`); strokes, effects and corners scale
|
||||
as with `object.scale` (`strokes?`, `corners?`, default: Scale Strokes & Effects and Scale Corners), pattern tiles with
|
||||
`patterns?` (default: Transform Pattern Tiles). A move that keeps the size scales nothing (art moves with
|
||||
`artboard.move {moveArt}`). The Artboard tool's `scaleArt` option (`tool.setOption {tool: "artboard", key: "scaleArt",
|
||||
value: true}`; the Control bar's and Properties' Scale Artwork with Artboard check box, beside Move Artwork with
|
||||
Artboard) makes its handle drags proportional and passes `scaleArt`, as do Properties' and the Transform panel's
|
||||
artboard size fields while the tool is in use.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"artboard.copy","params":{"index":0}}}
|
||||
{"name":"run_command","arguments":{"command":"edit.paste","params":{}}}
|
||||
|
||||
Reference in new issue
Block a user