Compare commits

..
Author SHA1 Message Date
bflatasticandClaude Opus 5.5 00344f9b68 Fix #733: PDF export keeps type as text in embedded subset fonts
The PDF writer defaulted to Convert Text to Outlines, so every glyph of
every page became a path: a text-heavy PDF grew several times larger on
export (a 7-page document went from 27 KB to 226 KB) and its text could
no longer be selected or searched. Illustrator's PDF presets embed font
subsets and keep live text; that is now the default too
(advanced.outlineText: false). Outlining stays available as an option,
fonts that can't be embedded still become outlines with a warning, and
printer's marks (page information) are drawn as shapes as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:06:38 +07:00
BFlat 133ad87541 Merge pull request #737 from storytold/fix/712-hide-box-dragging
Fix #712: the bounding box hides while a drag moves, scales or rotates
2026-10-09 17:46:53 +07:00
bflatasticandClaude Opus 5.5 f8a7695c3a Fix #712: the bounding box hides while a drag moves, scales or rotates
During a Selection-tool drag the bounding box and its handles stayed on
screen while the art moved, so the content looked as if it slid inside
a box of its own. The box now hides once a drag moves, scales or
rotates the selection, as in Illustrator, and comes back on release. A
press that hasn't moved, and a marquee, keep it. Tools gain
`transforming()` (Session::tool_transforming) for this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:39:12 +07:00
BFlat 66cb7848f3 Merge pull request #736 from storytold/feat/724-bold-italic
Fix #724: Type › Bold and Italic, Cmd+Shift+B / Cmd+Shift+I while typing
2026-10-09 17:26:35 +07:00
bflatasticandClaude Opus 5.5 ee16d10fd4 Fix #724: Type > Bold and Italic, Cmd+Shift+B / Cmd+Shift+I while typing
Designers coming from page layout apps expect Cmd+Shift+B to bold the
text they are editing. Type > Bold and Type > Italic switch the selected
type (or the Type tool's selection) to its family's own Bold or Italic
face, and back to the regular weight or upright face when it already is,
keeping the face's other words (a Narrow face stays Narrow). There is no
fake bold or slant: a family without the face is left as it is and the
status bar says so. While the Type tool edits text, Cmd+Shift+B and
Cmd+Shift+I apply them; elsewhere Cmd+Shift+B still hides the bounding
box. Both can be given other shortcuts in Edit > Keyboard Shortcuts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:20:58 +07:00
BFlat 82af7a3108 Merge pull request #730 from storytold/feat/637-ai-without-pdf
Part of #637: .ai files saved without PDF compatibility open from their editing data
2026-10-09 16:50:03 +07:00
BFlat 46e4262376 Merge pull request #731 from storytold/fix/729-tabs-above-text
Fix #729: the Tabs panel's Above Text puts its ruler over the text
2026-10-09 16:41:55 +07:00
bflatasticandClaude Opus 5.5 c16804237e Fix #729: the Tabs panel's Above Text puts its ruler over the text
The Tabs panel's ruler was a fixed strip in the panel, at no relation to
the text on the canvas, so stops couldn't be placed by eye. Above Text
(Position Panel Above Text) floats the panel just above the selected
text, as wide as its ruler needs to put 0 on the text's left edge and
its marks at the canvas zoom: each stop sits over the column it sets.
Floating panel groups keep a width they are given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:38:49 +07:00
28 changed files with 352 additions and 28 deletions

No files matched your search

+3 -3
View File
@@ -145,7 +145,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
- **Geometry and effects:**
- Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below. Object › Path › Remove Anchor Points (`path.removeAnchors`) removes the direct-selected anchors without opening their paths (the Delete key still deletes them with their segments): the neighbours keep their handle directions and their facing handles are refitted so one cubic follows the two old segments, and straight sides stay straight. The Delete Anchor Point tool uses the same refit. The Control bar, the contextual task bar, the Properties quick actions and the canvas context menu offer it when anchors are selected. With anchors selected, the Control bar and the Properties panel also convert them to corner or smooth (`path.convertAnchors`), connect end points (Join) and cut the path at them (`path.cutAtAnchors`, also on the task bar and the context menu): a closed path opens there and an open one becomes one path per piece, each cut leaving one of its two coincident anchors selected so a drag pulls the path apart.
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters).
- **Type:** Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. Variation axis sliders are not done. Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `<use>` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458).
- **Type:** Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. Variation axis sliders are not done. Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `<use>` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458). Type › Bold and Italic (beyond Illustrator, as page layout apps have them; Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text) switch the selected type to its family's own Bold or Italic face and back, keeping its width (a Narrow face stays Narrow); a family without that face says so (#724).
- **Interface languages, vertical type:** VectorCraft ▸ Language (or Preferences ▸ User Interface) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese, Spanish (neutral international Spanish, for every `es-*` locale), French (every `fr-*` locale), Italian (every `it-*` locale) and Russian (every `ru-*` locale) cover the whole interface, Czech and Brazilian Portuguese every menu label, Brazilian Portuguese the shell's `tl!` literals too (tested); Spanish, French, Italian and Russian also translate status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech, Spanish, French, Italian and Russian ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku applies to horizontal and vertical Japanese (Paragraph panel › Kinsoku Set, `text.setFormat {kinsoku}`: Hard, the default, keeps closing marks, iteration marks, the prolonged sound mark and small kana off the line start and opening brackets off its end; Soft lets 々, ー and small kana start a line as JLREQ's level 3 rules do; None turns it off), and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a line flush with its start (a paragraph's first line at its first-line indent, JIS X 4051's principle in JLREQ 3.1.5), and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline (new type in a Japanese interface starts top to top, with em box centre character alignment, #432); Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline, the em box top/right, centre or bottom/left, or the ICF (ideographic character face) top/right or bottom/left, from the font's BASE table or, without one, the average ink box of some ideographs and kana; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines.
- **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `<mask>` and PDF soft masks.
- **Image Trace Flat Logo mode:** a fourth mode beside Black and White, Grayscale and Color, for flat-colour logos. Each pixel is read as a blend of the logo's colours (found automatically or named), the blend is interpolated smoothly and every outline is placed where two colours are equally present, so edges are accurate to a fraction of a pixel. Outlines are then cleaned: corners the blur rounded are put back, near-straight runs and near-circular arcs become exact lines and arcs, the rest is fitted with as few Béziers as the tolerance allows, touching colours overlap by a hair so no gap opens, and an outline that strays from the raw contour is fitted plainly instead. Gradients and photos are refused with advice to use Color. *Measured* (a local experiment, not run in CI): a flat-colour logo's vector original was rendered down to a soft 199 × 100 px transparent PNG, traced, and the result compared with the original at 1600 px wide. The built-in colour presets (6 Colors, 16 Colors, High Fidelity Photo, Low Fidelity Photo) had a mean edge error of 2.5 to 3.4 px and region overlap of 0.88 to 0.91; Flat Logo had 0.78 px and 0.966 (0.48 px and 0.975 from a crisp source), in about 0.1 s. Limits: flat-colour art only; needle-thin sharp tips are shortened by about 1.7 source pixels because the blur erases them; it works at no more than 1024 px on the long side (larger images are averaged down first); only one logo family has been scored against a vector original.
@@ -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).
@@ -184,7 +184,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out, EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, Asset Export, clipboard flavours, File Info; AF.1–AF.3 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish | 3–6 (existing scope; Affinity export unestimated) |
| M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 |
| M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open) | 3–6 |
| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Character/Paragraph Styles, per-paragraph paragraph attributes, Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import, variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel, inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; tab leaders, spell check, vertical type pending | 45–60 |
| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Character/Paragraph Styles, per-paragraph paragraph attributes, Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import, variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel (Above Text floats it over the selected text, its ruler at the canvas zoom so stops line up with the columns, #729), inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; tab leaders, spell check, vertical type pending | 45–60 |
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ✅ done (M8.1–M8.20 on 2026-10-06, fidelity pass): Liquify Tool Options with one shared brush, pen pressure, hold-to-apply, a safe scope and incremental strokes; tool options that last; Puppet Warp rotation, multi-select, Control bar and a rest-shape session that follows Undo; Width tool on compound paths; Envelope dialogs with preview, Reset with Warp/Mesh, full Envelope Options, type/images/symbols/appearance/gradients/patterns distorted on canvas and in every export, warps that keep their frame, Edit Contents, mesh handles and Control bar; Blend Options, anchor-targeted Blend tool, editable spine, keys picked on the canvas, faithful interpolation and knockout; Perspective Grid with Define Grid, presets, lock/snap/rulers, widgets, the 1–4 plane switch, movable planes, Perspective Selection scale/copy/perpendicular/Transform Again, and type and symbols truly projected with Edit Text. Left: Anti-Alias and Preserve Shape output, warp-envelope point editing, brush interpolation in blends, Free Transform/Scale/Reflect along a rotated box | 2–4 |
| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder and Color Adjustments; 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 |
+1 -1
View File
@@ -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
),
+23
View File
@@ -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();
}
+5
View File
@@ -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))
+4 -2
View File
@@ -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();
+1 -1
View File
@@ -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 }
}
}
+6 -2
View File
@@ -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 { .. })));
+4 -2
View File
@@ -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");
}
}
+1 -1
View File
@@ -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}),
+5
View File
@@ -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 {
+4
View File
@@ -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;
+3 -1
View File
@@ -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 };
+4 -3
View File
@@ -129,7 +129,7 @@ pub fn float(ui: &mut UiState, ids: &[&str], active: &str, pos: Pos2) {
}
let panels: Vec<String> = ids.iter().map(|id| id.to_string()).collect();
let active = panels.iter().position(|p| p == active).unwrap_or(0);
ui.floating_panels.push(FloatingPanels { panels, active, pos: [pos.x, pos.y] });
ui.floating_panels.push(FloatingPanels { panels, active, pos: [pos.x, pos.y], width: None });
// The tabbed group shows another of its tabs when the one it showed floats.
if let Some(tab) = shown_tab(ui) {
ui.dock_tab = tab;
@@ -312,6 +312,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
let ids = ids_of(g);
let Some(&first) = ids.first() else { continue };
let active = ids.get(g.active).copied().unwrap_or(first);
let given = g.width.map(|w| w.min(screen.width() - 16.0));
let area = area_id(first);
let size = ctx.memory(|m| m.area_rect(area)).map_or(vec2(dock::panel_width(active), 200.0), |r| r.size());
let pos = clamp(g.pos, size, screen);
@@ -333,7 +334,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
})
.collect();
let tabs_width: f32 = tabs.iter().map(|g| g.size().x + 24.0).sum();
let width = ids.iter().map(|id| dock::panel_width(id)).fold(tabs_width + 32.0, f32::max);
let width = ids.iter().map(|id| dock::panel_width(id)).fold(tabs_width + 32.0, f32::max).max(given.unwrap_or(0.0));
ui.set_width(width);
// Title bar: a grip that moves the group, and the × that docks it.
let (bar, _) = ui.allocate_exact_size(vec2(width, TITLE), Sense::hover());
@@ -763,7 +764,7 @@ mod tests {
}))
.unwrap();
let edited = edited.sanitized();
assert_eq!(edited.floating_panels, [FloatingPanels { panels: vec!["layers".into()], active: 0, pos: [1.0, 2.0] }]);
assert_eq!(edited.floating_panels, [FloatingPanels { panels: vec!["layers".into()], active: 0, pos: [1.0, 2.0], width: None }]);
h.settle();
let g = h.group("layers");
let screen = Rect::from_min_size(Pos2::ZERO, SCREEN);
+2
View File
@@ -285,6 +285,8 @@
Fill with Placeholder Text Vyplnit zástupným textem
Insert Inline Symbol Vložit symbol do textu
Show Hidden Characters Zobrazit skryté znaky
Bold Tučné
Italic Kurzíva
Hide Hidden Characters Skrýt skryté znaky
Type Orientation Orientace textu
Horizontal Vodorovně
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -339,6 +339,8 @@
Clean Up… Limpiar…
Clear Borrar
Clear All Tabs Borrar todas las tabulaciones
Above Text Sobre el texto
Position Panel Above Text Colocar el panel sobre el texto
Clear Appearance Borrar apariencia
Clear Guides Borrar guías
Clear Overrides Borrar modificaciones
@@ -2444,6 +2446,8 @@
Show Guides Mostrar guías
Show handles when multiple anchors are selected Mostrar manejadores al seleccionar varias anclas
Show Hidden Characters Mostrar caracteres ocultos
Bold Negrita
Italic Cursiva
Show Images in Outline Mode Mostrar imágenes en modo de contorno
Show in Folder Mostrar en carpeta
Show Indic Options Mostrar opciones índicas
@@ -3124,6 +3128,8 @@ axis Both Ambos
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
# (`complete_languages_translate_every_message` lists any that are missing). Command ids, parameter
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg select some type first selecciona primero algún texto
@msg {_1} has no {_2} style {_1} no tiene el estilo {_2}
@msg a layer couldn't be read no se pudo leer una capa
@msg it has more than one artboard tiene más de una mesa de trabajo
@msg it has no page no tiene página
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -340,6 +340,8 @@
Clean Up… Nettoyer…
Clear Effacer
Clear All Tabs Effacer toutes les tabulations
Above Text Au-dessus du texte
Position Panel Above Text Placer le panneau au-dessus du texte
Clear Appearance Effacer l’aspect
Clear Guides Effacer les repères
Clear Overrides Effacer les remplacements
@@ -2441,6 +2443,8 @@
Show Guides Afficher les repères
Show handles when multiple anchors are selected Afficher les poignées lorsque plusieurs points d’ancrage sont sélectionnés
Show Hidden Characters Afficher les caractères masqués
Bold Gras
Italic Italique
Show Images in Outline Mode Afficher les images en mode Tracés
Show in Folder Afficher dans le dossier
Show Indic Options Afficher les options indiennes
@@ -3121,6 +3125,8 @@ axis Both Les deux
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
# (`complete_languages_translate_every_message` lists any that are missing). Command ids, parameter
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg select some type first sélectionnez d’abord du texte
@msg {_1} has no {_2} style {_1} n’a pas de style {_2}
@msg a layer couldn't be read un calque n’a pas pu être lu
@msg it has more than one artboard il a plus d'un plan de travail
@msg it has no page il n’a pas de page
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -339,6 +339,8 @@
Clean Up… Pulisci…
Clear Cancella
Clear All Tabs Cancella tutte le tabulazioni
Above Text Sopra il testo
Position Panel Above Text Posiziona il pannello sopra il testo
Clear Appearance Cancella aspetto
Clear Guides Cancella guide
Clear Overrides Cancella sostituzioni
@@ -2444,6 +2446,8 @@
Show Guides Mostra guide
Show handles when multiple anchors are selected Mostra le maniglie quando sono selezionati più punti di ancoraggio
Show Hidden Characters Mostra caratteri nascosti
Bold Grassetto
Italic Corsivo
Show Images in Outline Mode Mostra immagini in modalità Contorno
Show in Folder Mostra nella cartella
Show Indic Options Mostra opzioni per lingue indiane
@@ -3124,6 +3128,8 @@ axis Both Entrambi
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
# (`complete_languages_translate_every_message` lists any that are missing). Command ids, parameter
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg select some type first seleziona prima del testo
@msg {_1} has no {_2} style {_1} non ha lo stile {_2}
@msg a layer couldn't be read non è stato possibile leggere un livello
@msg it has more than one artboard ha più di una tavola da disegno
@msg it has no page non ha pagine
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -326,6 +326,8 @@
Fill with Placeholder Text サンプルテキストの割り付け
Insert Inline Symbol インラインシンボルを挿入
Show Hidden Characters 制御文字を表示
Bold 太字
Italic 斜体
Type Orientation 組み方向
Horizontal 横書き
Vertical 縦書き
@@ -882,6 +884,8 @@
Choose… 選択…
Circle 円
Clear All Tabs すべてのタブを消去
Above Text テキストの上
Position Panel Above Text テキストの上にパネルを配置
Clear Appearance アピアランスを消去
Clear Overrides オーバーライドを消去
Click クリック
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2134,6 +2134,8 @@
Show Grid (⌘') Mostrar Grade (⌘')
Show Guides Mostrar Guias
Show Hidden Characters Mostrar Caracteres Ocultos
Bold Negrito
Italic Itálico
Show Images in Outline Mode Mostrar Imagens no Modo de Contorno
Show Indic Options Mostrar Opções do Índico
Show Link Info Mostrar Informações do Link
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -337,6 +337,8 @@
Clean Up… Очистить…
Clear Очистить
Clear All Tabs Удалить все табуляции
Above Text Над текстом
Position Panel Above Text Расположить панель над текстом
Clear Appearance Очистить оформление
Clear Guides Удалить направляющие
Clear Overrides Очистить переопределения
@@ -2440,6 +2442,8 @@
Show Guides Показать направляющие
Show handles when multiple anchors are selected Показывать маркеры при выделении нескольких опорных точек
Show Hidden Characters Показать скрытые символы
Bold Полужирный
Italic Курсив
Show Images in Outline Mode Показывать изображения в режиме контуров
Show in Folder Показать в папке
Show Indic Options Показать параметры индийских языков
@@ -3120,6 +3124,8 @@ axis Both Оба
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
# (`complete_languages_translate_every_message` lists any that are missing). Command ids, parameter
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg select some type first сначала выделите текст
@msg {_1} has no {_2} style у {_1} нет начертания {_2}
@msg a layer couldn't be read не удалось прочитать слой
@msg it has more than one artboard в нём несколько монтажных областей
@msg it has no page в нём нет страницы
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -325,6 +325,8 @@
Clean Up… 清理…
Clear 清除
Clear All Tabs 清除全部制表位
Above Text 文本上方
Position Panel Above Text 将面板置于文本上方
Clear Appearance 清除外观
Clear Guides 清除参考线
Clear Overrides 清除覆盖
@@ -2362,6 +2364,8 @@
Show Guides 显示参考线
Show handles when multiple anchors are selected 选择多个锚点时显示手柄
Show Hidden Characters 显示隐藏字符
Bold 粗体
Italic 斜体
Show Images in Outline Mode 以轮廓模式显示图像
Show in Folder 在文件夹中显示
Show Indic Options 显示印度文字选项
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -337,6 +337,8 @@
Clean Up… 清除…
Clear 清除
Clear All Tabs 清除所有定位點
Above Text 文字上方
Position Panel Above Text 將面板置於文字上方
Clear Appearance 清除外觀
Clear Guides 清除參考線
Clear Overrides 清除覆寫
@@ -2447,6 +2449,8 @@
Show Guides 顯示參考線
Show handles when multiple anchors are selected 選取多個錨點時顯示控制把手
Show Hidden Characters 顯示隱藏字元
Bold 粗體
Italic 斜體
Show Images in Outline Mode 在外框模式中顯示影像
Show in Folder 在資料夾中顯示
Show Indic Options 顯示印度文字選項
Can't render this file because it contains an unexpected character in line 6 and column 47.
+18
View File
@@ -11,6 +11,7 @@ use vectorcraft_engine::cmd::fileio::SaveMode;
use crate::VectorcraftApp;
use crate::io;
use crate::panels::character::Face;
use crate::state::{DockTab, next_zoom};
use crate::theme::{self, Brightness, Tokens};
use crate::widgets;
@@ -175,6 +176,18 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
("view.textThreads", "Hide Text Threads", "Cmd+Shift+Y", "{} toggle the thread lines between threaded text frames"),
("view.gradientAnnotator", "Hide Gradient Annotator", "Cmd+Alt+G", "{} toggle the Gradient tool's annotator"),
("type.hiddenCharacters", "Show Hidden Characters", "Cmd+Alt+I", "{} toggle markers for spaces, paragraph ends and story ends"),
(
"type.bold",
"Bold",
"",
"{} the selected type (or the Type tool's selection) in its family's Bold face, or back to the regular face when it is bold; keeps italics. A family without that face is left as it is, with a message. While the Type tool edits text, Cmd+Shift+B",
),
(
"type.italic",
"Italic",
"",
"{} the selected type (or the Type tool's selection) in its family's Italic (or Oblique) face, or back upright when it is italic; keeps the weight. A family without that face is left as it is, with a message. While the Type tool edits text, Cmd+Shift+I",
),
("effect.last", "Last Effect…", "Cmd+Alt+Shift+E", "{} open the dialog of the last effect applied"),
("view.zoomIn", "Zoom In", "Cmd+=", "{}"),
("view.zoomOut", "Zoom Out", "Cmd+-", "{}"),
@@ -930,6 +943,8 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
"view.snapToPixel" => flag(&mut app.ui.view.snap_to_pixel),
"view.textThreads" => flag(&mut app.ui.view.text_threads),
"type.hiddenCharacters" => flag(&mut app.ui.view.hidden_chars),
"type.bold" => crate::panels::character::toggle_face(app, Face::Bold),
"type.italic" => crate::panels::character::toggle_face(app, Face::Italic),
"view.gradientAnnotator" => flag(&mut app.ui.view.gradient_annotator),
"effect.last" => match app.last_effect.clone() {
Some((e, params)) => {
@@ -1801,6 +1816,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
match id {
// Save is off for a clean document that already has its own file.
"file.save" => app.session.active().is_some_and(|d| d.path.is_none() || d.converted || d.is_dirty()),
"type.bold" | "type.italic" => crate::panels::character::text_style(app).is_some(),
"file.reveal" => app.services.reveal.is_some() && app.session.active().is_some_and(|d| d.path.is_some()),
"file.place"
| "file.export.svg"
@@ -2220,6 +2236,8 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
sub("Font", font_items()),
sub("Recent Fonts", RECENT_FONT_IDS.iter().map(|id| c("Recent Font", id)).collect()),
sub("Size", TYPE_SIZES.iter().map(|(l, n)| cp(l, "text.setStyle", json!({ "size": n }))).collect()),
c("Bold", "type.bold"),
c("Italic", "type.italic"),
Sep,
panel("Glyphs", "glyphs"),
sub("Insert Special Character", insert_items(INSERT_SPECIAL)),
+124
View File
@@ -111,6 +111,90 @@ fn char_cmd(app: &mut VectorcraftApp, cmd: &str, p: Value) {
range_style(app, p);
}
/// The faces Type › Bold and Type › Italic switch to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Face {
Bold,
Italic,
}
/// Words of a style name that set its weight: a style without any is the family's regular weight.
const WEIGHTS: [&str; 16] = [
"thin",
"hairline",
"extralight",
"ultralight",
"light",
"book",
"medium",
"semibold",
"demibold",
"demi",
"bold",
"extrabold",
"ultrabold",
"heavy",
"black",
"ultra",
];
/// Words that name the regular weight, upright: not part of what Bold and Italic keep.
const PLAIN: [&str; 5] = ["regular", "normal", "roman", "plain", "upright"];
/// A style name's (bold, regular weight, italic, the other words): `Bold Italic` is (true, false,
/// true, []), `Narrow Italic` (false, true, true, ["narrow"]), `SemiBold` (false, false, false, []).
fn face_of(style: &str) -> (bool, bool, bool, Vec<String>) {
let words: Vec<String> = style.split([' ', '-', '_']).filter(|w| !w.is_empty()).map(str::to_ascii_lowercase).collect();
let italic = words.iter().any(|w| w == "italic" || w == "oblique");
let bold = words.iter().any(|w| w == "bold");
let regular = !words.iter().any(|w| WEIGHTS.contains(&w.as_str()));
let mut rest: Vec<String> =
words.into_iter().filter(|w| !WEIGHTS.contains(&w.as_str()) && !PLAIN.contains(&w.as_str()) && w != "italic" && w != "oblique").collect();
rest.sort();
(bold, regular, italic, rest)
}
/// The style of `styles` that is bold (else of regular weight) and italic (else upright), with the
/// same other words as style `like` (a Narrow face stays Narrow, a plain one plain).
fn pick_face<'a>(styles: &'a [String], bold: bool, italic: bool, like: &str) -> Option<&'a str> {
let (.., keep) = face_of(like);
styles
.iter()
.map(String::as_str)
.filter(|s| {
let (b, r, i, rest) = face_of(s);
i == italic && (if bold { b } else { r }) && rest == keep
})
// `Regular` before `Regular Text`-like doubles: the plainest name is the family's own face.
.min_by_key(|s| s.len())
}
/// Type › Bold and Type › Italic (Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text, as in
/// page layout apps): the family's own Bold or Italic face for the text the panels act on, or back
/// to its regular weight (upright) when it already is bold (italic). No fake bold or slant: a family
/// without that face is left as it is, and the message says so (#724).
pub(crate) fn toggle_face(app: &mut VectorcraftApp, face: Face) -> Result<Value, String> {
let (s, _) = text_style(app).ok_or("select some type first")?;
let styles = vectorcraft_text::FontDb::global().styles(&s.font_family);
let (bold, _, italic, _) = face_of(&s.font_style);
let (want_bold, want_italic) = match face {
Face::Bold => (!bold, italic),
Face::Italic => (bold, !italic),
};
let Some(name) = pick_face(&styles, want_bold, want_italic, &s.font_style) else {
let wanted = match (want_bold, want_italic) {
(true, true) => "Bold Italic",
(true, false) => "Bold",
(false, true) => "Italic",
(false, false) => "Regular",
};
return Err(format!("{} has no {} style", s.font_family, wanted));
};
let name = name.to_string();
style(app, json!({ "style": name }));
Ok(json!({ "style": name }))
}
/// `text.setRangeStyle` with `p` on the range the Type tool has selected (the whole text when
/// nothing is selected). Does nothing unless the Type tool edits text.
pub(crate) fn range_style(app: &mut VectorcraftApp, p: Value) {
@@ -213,6 +297,7 @@ enum TextInput {
Copy,
Cut,
Paste(Option<String>),
Face(Face),
}
/// Route editing keys (with modifiers), clipboard events and Cmd+C/X/V to the Type tool (Cmd+A is
@@ -254,6 +339,13 @@ pub(crate) fn route_type_input(app: &mut VectorcraftApp, ctx: &egui::Context) {
}
return false;
}
// Type › Bold and Italic, as page layout apps have them while text is edited.
if m.command && m.shift && !m.alt && matches!(key, Key::B | Key::I) {
if *pressed {
todo.push(TextInput::Face(if *key == Key::B { Face::Bold } else { Face::Italic }));
}
return false;
}
if m.command && !m.shift && !m.alt && matches!(key, Key::C | Key::X | Key::V) {
if *pressed {
todo.push(match key {
@@ -281,6 +373,11 @@ pub(crate) fn route_type_input(app: &mut VectorcraftApp, ctx: &egui::Context) {
}
}
TextInput::Paste(s) => text_paste(app, s),
TextInput::Face(f) => {
if let Err(e) = toggle_face(app, f) {
app.status(e);
}
}
}
}
}
@@ -549,6 +646,33 @@ mod tests {
use super::*;
use vectorcraft_engine::Session;
/// Bold and Italic pick the family's own faces by name, and toggle back (#724).
#[test]
fn bold_and_italic_pick_the_familys_own_faces() {
let styles: Vec<String> =
["Light", "Narrow", "Regular", "Italic", "Narrow Italic", "SemiBold", "Bold", "Bold Condensed", "Narrow Bold", "Bold Italic", "Black"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(pick_face(&styles, true, false, "Regular"), Some("Bold"), "not Bold Condensed, Narrow Bold or SemiBold");
assert_eq!(pick_face(&styles, true, true, "Italic"), Some("Bold Italic"));
assert_eq!(pick_face(&styles, false, true, "Regular"), Some("Italic"));
assert_eq!(pick_face(&styles, false, false, "Italic"), Some("Regular"), "upright again, not Narrow");
assert_eq!(pick_face(&styles, true, false, "Narrow"), Some("Narrow Bold"), "a Narrow face stays Narrow");
assert_eq!(pick_face(&styles, false, true, "Narrow"), Some("Narrow Italic"));
let oblique: Vec<String> = ["Book", "Oblique", "Bold-Oblique"].iter().map(|s| s.to_string()).collect();
assert_eq!(pick_face(&oblique, true, true, "Oblique"), Some("Bold-Oblique"));
assert_eq!(pick_face(&oblique, false, false, "Oblique"), None, "Book has a weight word: no regular face");
assert_eq!(face_of("Bold Italic"), (true, false, true, vec![]));
assert_eq!(face_of("SemiBold"), (false, false, false, vec![]));
assert_eq!(face_of("Narrow Italic"), (false, true, true, vec!["narrow".to_string()]));
// No type selected: a message, nothing changed.
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
assert!(toggle_face(&mut app, Face::Bold).is_err());
assert!(!crate::menus::enabled(&app, "type.bold"));
}
/// Character Alignment is in the panel menu with the East Asian options only (as Mojikumi Set
/// and Top-to-Top Leading are in the Paragraph panel).
#[test]
+75 -9
View File
@@ -3,7 +3,7 @@
use egui::{Sense, Stroke, Ui, vec2};
use serde_json::{Value, json};
use vectorcraft_doc::{NodeKind, TabAlign, TabStop};
use vectorcraft_doc::{Node, NodeKind, TabAlign, TabStop, TextKind};
use super::{first_selected, pstate, set_pstate};
use crate::VectorcraftApp;
@@ -26,9 +26,13 @@ fn align_id(a: TabAlign) -> &'static str {
}
}
/// The stops of the caret's paragraph while the Type tool edits text (else the first selected text
/// object's first paragraph) and the ruler span in points (the frame width for area type).
fn current(app: &VectorcraftApp) -> Option<(Vec<TabStop>, f64)> {
/// The panel body's margin and the ruler's own inset: the ruler's 0 is this far right of the panel.
const RULER_LEFT: f32 = 10.0 + 4.0;
/// The text object, the stops of the caret's paragraph while the Type tool edits text (else the
/// first selected text object's first paragraph) and the ruler span in points (the frame width for
/// area type).
fn current(app: &VectorcraftApp) -> Option<(Node, Vec<TabStop>, f64)> {
let editing = super::character::text_editing(app);
let n = match editing {
Some((id, _, _)) => app.session.active()?.doc.node(id)?.clone(),
@@ -37,10 +41,39 @@ fn current(app: &VectorcraftApp) -> Option<(Vec<TabStop>, f64)> {
let NodeKind::Text(t) = &n.kind else { return None };
let para = editing.map_or(0, |(_, a, _)| t.paragraphs_in(a, a).start);
let span = match &t.kind {
vectorcraft_doc::TextKind::Area { frame } => frame.bounds().map_or(360.0, |b| b.width()),
TextKind::Area { frame } => frame.bounds().map_or(360.0, |b| b.width()),
_ => 360.0,
};
Some((t.para_at(para).tabs.clone(), span.max(72.0)))
let tabs = t.para_at(para).tabs.clone();
Some((n, tabs, span.max(72.0)))
}
/// Position Panel Above Text: float the panel just above text `n`, as wide as its ruler needs to
/// put 0 on the text's left edge and its marks at the canvas zoom, so each stop sits over the
/// column it sets (#729). `span`: the ruler's length in the text's points.
fn above_text(app: &mut VectorcraftApp, ctx: &egui::Context, n: &Node, span: f64) -> Option<()> {
let xf = crate::canvas::Xf::new(app.canvas_rect?, app.view()?);
let on_screen = xf.rect_to_screen(n.geometric_bounds()?);
// Screen points per point of the text: across the frame for area type (so a scaled frame
// lines up too), the zoom for point type.
let per = match &n.kind {
NodeKind::Text(t) if matches!(t.kind, TextKind::Area { .. }) => on_screen.width() / span as f32,
_ => xf.zoom as f32,
};
if !per.is_finite() || per <= 0.0 {
return None;
}
let width = span as f32 * per + RULER_LEFT * 2.0;
let height = ctx.memory(|m| m.area_rect(crate::floating::area_id("tabs"))).map_or(240.0, |r| r.height());
let pos = egui::pos2(on_screen.left() - RULER_LEFT, on_screen.top() - height - 6.0);
if crate::floating::group_of(&app.ui, "tabs").is_none() {
crate::floating::float(&mut app.ui, &["tabs"], "tabs", pos);
}
let gi = crate::floating::group_of(&app.ui, "tabs")?;
let g = app.ui.floating_panels.get_mut(gi)?;
g.pos = [pos.x, pos.y];
g.width = Some(width);
Some(())
}
fn stops_json(stops: &[TabStop]) -> Value {
@@ -63,7 +96,7 @@ fn apply(app: &mut VectorcraftApp, stops: &[TabStop]) {
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
let Some((mut stops, span)) = current(app) else {
let Some((node, mut stops, span)) = current(app) else {
super::empty_state(ui, "pilcrow", tl!("No text selected"), tl!("Select a text object to set its tab stops."));
return;
};
@@ -89,6 +122,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
}
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button(tl!("Above Text")).on_hover_text(tl!("Position Panel Above Text")).clicked() {
above_text(app, ui.ctx(), &node, span);
}
});
});
ui.add_space(4.0);
egui::Grid::new("tabs-grid").num_columns(2).spacing([6.0, 4.0]).show(ui, |ui| {
@@ -209,7 +247,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
let Some((mut stops, _)) = current(app) else {
let Some((_, mut stops, _)) = current(app) else {
ui.add_enabled(false, egui::Button::new(tl!("Select text")).frame(false));
return;
};
@@ -259,11 +297,39 @@ mod tests {
frame(&mut app);
app.session.execute("text.tabs.set", &json!({"stops": [{"position": 50}, {"position": 120, "align": "decimal"}]})).unwrap();
frame(&mut app);
let (stops, span) = current(&app).unwrap();
let (_, stops, span) = current(&app).unwrap();
assert_eq!(stops.len(), 2);
assert_eq!(span, 300.0);
// The stops survive a JSON round trip through the panel's encoding.
let back: Vec<serde_json::Value> = stops_json(&stops).as_array().unwrap().clone();
assert_eq!(back[1]["align"], json!("decimal"));
}
/// Position Panel Above Text floats the panel over the text, its ruler's 0 on the frame's left
/// edge and a stop's mark over the column it sets, at any zoom (#729).
#[test]
fn above_text_puts_the_ruler_over_the_text_at_the_zoom() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 600, "height": 400})).unwrap();
app.session.execute("text.create", &json!({"x": 40, "y": 200, "text": "a b", "area": {"width": 300, "height": 100}})).unwrap();
app.canvas_rect = Some(egui::Rect::from_min_size(egui::pos2(50.0, 40.0), vec2(1000.0, 700.0)));
for zoom in [1.0, 2.0] {
if let Some(v) = app.view_mut() {
v.zoom = zoom;
}
let (node, _, span) = current(&app).unwrap();
let ctx = egui::Context::default();
above_text(&mut app, &ctx, &node, span).unwrap();
let g = &app.ui.floating_panels[crate::floating::group_of(&app.ui, "tabs").unwrap()];
let xf = crate::canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
let frame = xf.rect_to_screen(node.geometric_bounds().unwrap());
assert!((g.pos[0] + RULER_LEFT - frame.left()).abs() < 0.01, "0 on the frame's left edge");
assert!(g.pos[1] < frame.top(), "above the text");
// The ruler (the panel less its margins) spans the frame at this zoom.
let ruler = g.width.unwrap() - RULER_LEFT * 2.0;
assert!((ruler - frame.width()).abs() < 0.01, "{ruler} {}", frame.width());
assert!((ruler - 300.0 * zoom as f32).abs() < 0.01);
}
assert_eq!(app.ui.floating_panels.len(), 1, "placed again, not floated twice");
}
}
+25
View File
@@ -466,6 +466,31 @@ mod tests {
out
}
/// Cmd+Shift+B while the Type tool edits text is Type › Bold, not Hide Bounding Box (#724).
#[test]
fn cmd_shift_b_while_typing_is_bold_not_the_bounding_box() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
app.select_tool("type");
let view = app.view_info();
for kind in [vectorcraft_tools::PointerKind::Down, vectorcraft_tools::PointerKind::Up] {
app.session.pointer(&vectorcraft_tools::PointerEvent::new(kind, 50.0, 50.0), view).unwrap();
}
frame(&mut app, vec![egui::Event::Text("Bold".into())]);
assert!(app.session.tool_wants_text());
let chord =
egui::Event::Key { key: Key::B, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND | Modifiers::SHIFT };
frame(&mut app, vec![chord.clone()]);
assert!(app.ui.view.bounding_box, "the bounding box stays");
// The chord reached Bold: the text is in a bold face, or its family has none and says so.
let face = crate::panels::character::text_style(&app).map(|(s, _)| s.font_style).unwrap_or_default();
assert!(face.contains("Bold") || app.ui.status.contains("has no Bold style"), "{face:?} {:?}", app.ui.status);
// Out of the text, the chord hides the bounding box as before.
app.select_tool("selection");
frame(&mut app, vec![chord]);
assert!(!app.ui.view.bounding_box);
}
/// Tab shows and hides the panels, and in the Type tool it types a tab, without moving the
/// keyboard focus on to a field: the keys after it (a tool letter, the next letter typed) still
/// work.
+3
View File
@@ -425,6 +425,9 @@ pub struct FloatingPanels {
pub active: usize,
/// Top-left corner in screen points.
pub pos: [f32; 2],
/// A width the group was given (the Tabs panel sized over its text), else its panels' own.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub width: Option<f32>,
}
impl FloatingPanels {
+3 -3
View File
@@ -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}}}}