Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8a7695c3a | ||
|
|
66cb7848f3 | ||
|
|
ee16d10fd4 | ||
|
|
82af7a3108 | ||
|
|
46e4262376 | ||
|
|
c16804237e | ||
|
|
4b9f9b9bc2 | ||
|
|
008b5e6d6c | ||
|
|
640a46a76f | ||
|
|
2d12b39261 | ||
|
|
d53074487c |
No files matched your search
+4
-4
@@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob
|
||||
|
||||
This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands.
|
||||
|
||||
_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG windoLine truncated
|
||||
_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG windoLine truncated
|
||||
|
||||
## Where we are
|
||||
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
@@ -250,7 +250,7 @@ lossless SVG/PDF export of live effects with SVG filters, a documented JSON form
|
||||
command, gesture and dialog drivable by agents (MCP, CLI, control channel).
|
||||
|
||||
## Out of scope (by design or by law)
|
||||
- **Native `.ai` private data, beyond its structure:** the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths, clipping groups and layer clipping, object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours), linear and radial gradients, opacity, blend modes, isolation and knockout, embedded images with their alpha channel, guides, and every artboard where it is. An object with several fills or strokes, effects or a brush comes in as its drawn look (a group named after it). Type that shows comes from the page, which draws it, into its text object's place (no group around it); type that doesn't (hidden layers and objects, art off the page) is made from the file's text document (characters, font, size, colour, alignment and where it is, for point type), and area type and type on a path in it are left out with a note. Symbols, pattern fills, placed files and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike); the page is used when they differ by more than 5%. A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so Illustrator-only live objects arrive as appearance, and plain groups arrive ungrouped: that part draws each object on its own and marks only layers, clipping groups and groups with opacity, blending or a mask (those come back). Art outside the artboards isn't in that part at all (Illustrator's PDF part leaves it out), so it doesn't open; the import says so in a note (#472). That part also has no paragraphs: area type is written line by line, and comes back as area type only where its font is installed and lays the lines out with the same breaks, else as a point type object per line (#508). Art off the page that a PDF does draw opens on the pasteboard.
|
||||
- **Native `.ai` private data, beyond its structure:** the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths, clipping groups and layer clipping, object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours), linear and radial gradients, opacity, blend modes, isolation and knockout, embedded images with their alpha channel, guides, and every artboard where it is. An object with several fills or strokes, effects or a brush comes in as its drawn look (a group named after it). Type that shows comes from the page, which draws it, into its text object's place (no group around it); type that doesn't (hidden layers and objects, art off the page) is made from the file's text document (characters, font, size, colour, alignment and where it is, for point type), and area type and type on a path in it are left out with a note. A `.ai` saved without PDF compatibility (a placeholder page) opens from its editing data alone, its point type made from the text document and other type left out with a note. Symbols, pattern fills, placed files and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike); the page is used when they differ by more than 5%. A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so Illustrator-only live objects arrive as appearance, and plain groups arrive ungrouped: that part draws each object on its own and marks only layers, clipping groups and groups with opacity, blending or a mask (those come back). Art outside the artboards isn't in that part at all (Illustrator's PDF part leaves it out), so it doesn't open; the import says so in a note (#472). That part also has no paragraphs: area type is written line by line, and comes back as area type only where its font is installed and lays the lines out with the same breaks, else as a point type object per line (#508). Art off the page that a PDF does draw opens on the pasteboard.
|
||||
- **Adobe cloud services** (Libraries sync, Adobe Fonts, Firefly/generative): these are pluggable provider APIs, not built in.
|
||||
- **Adobe's bundled assets** (swatch/brush/symbol libraries, presets, icons): ours are original.
|
||||
|
||||
|
||||
@@ -150,6 +150,20 @@ fn through_editing_data(bytes: &[u8], opts: &LoadOptions, doc: Document, warning
|
||||
(doc, warnings)
|
||||
}
|
||||
|
||||
/// A `.ai` saved without PDF compatibility (its PDF part is a placeholder page) from the editor's
|
||||
/// own copy of its art alone: `None` when the file has none (or pages are picked, or layers are
|
||||
/// off), an error when it can't be read.
|
||||
fn from_editing_data_alone(bytes: &[u8], opts: &LoadOptions) -> Option<Result<(Document, Vec<String>)>> {
|
||||
if opts.pages.is_some() || !opts.layers {
|
||||
return None;
|
||||
}
|
||||
let private = vectorcraft_pdf::illustrator_data(bytes, opts.password.as_deref())?;
|
||||
Some(
|
||||
vectorcraft_eps::ai_alone(&private)
|
||||
.map_err(|why| err(format!("this file was saved without PDF compatibility, and its editing data can't be read ({why})"))),
|
||||
)
|
||||
}
|
||||
|
||||
/// [`load`] with `document.open` options (the PDF pages, box and password; the DXF options).
|
||||
pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded> {
|
||||
let format = detect(name, bytes).ok_or_else(|| match super::unsupported(&super::extension(name)) {
|
||||
@@ -188,9 +202,13 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
.then(|| vectorcraft_pdf::editing_with(bytes, opts.password.as_deref()))
|
||||
.flatten()
|
||||
.map(|e| (e.intact, move || Some(e.data)));
|
||||
restore_or_import(editing, || {
|
||||
let (doc, warnings) = super::pdfimport::import(bytes, opts)?;
|
||||
Ok(through_editing_data(bytes, opts, doc, warnings))
|
||||
restore_or_import(editing, || match super::pdfimport::import(bytes, opts) {
|
||||
Ok((doc, warnings)) => Ok(through_editing_data(bytes, opts, doc, warnings)),
|
||||
// Saved without its PDF part: the editing copy is all the file has.
|
||||
Err(e) if e.to_string() == vectorcraft_pdf::PdfError::PlaceholderOnly.to_string() => {
|
||||
from_editing_data_alone(bytes, opts).unwrap_or(Err(e))
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
})?
|
||||
}
|
||||
"dxf" => {
|
||||
|
||||
@@ -91,3 +91,20 @@ fn what_the_reader_doesnt_read_opens_as_before_with_a_warning() {
|
||||
let r = open(&mut s, "symbols.ai", &ai::ai(&compressed(symbols.as_bytes()), ai::page_pdf()), json!({})).unwrap();
|
||||
assert!(r["warnings"].to_string().contains("symbols") && r["warnings"].to_string().contains("PDF part"), "{r}");
|
||||
}
|
||||
|
||||
/// A page showing only a placeholder line, as a `.ai` saved without PDF compatibility has.
|
||||
const PLACEHOLDER: &str = "BT /F1 12 Tf 10 50 Td (Saved without its PDF part) Tj ET";
|
||||
|
||||
#[test]
|
||||
fn an_ai_file_saved_without_its_pdf_part_opens_from_its_editing_data() {
|
||||
let mut s = Session::new();
|
||||
let r = open(&mut s, "art.ai", &ai::ai(&compressed(&ai::sample_data()), PLACEHOLDER), json!({})).unwrap();
|
||||
assert_eq!(r["format"], json!("ai"), "{r}");
|
||||
check_sample(&s);
|
||||
// Its editing data damaged: it says so (the placeholder isn't the art).
|
||||
let e = open(&mut s, "damaged.ai", &ai::ai(b"%AI12_CompressedData not zlib", PLACEHOLDER), json!({})).unwrap_err().to_string();
|
||||
assert!(e.contains("without PDF compatibility") && e.contains("zlib"), "{e}");
|
||||
// Without editing data, the placeholder still isn't opened as the art.
|
||||
let e = open(&mut s, "plain.ai", &ai::ai(b"", PLACEHOLDER), json!({})).unwrap_err().to_string();
|
||||
assert!(e.contains("placeholder"), "{e}");
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -61,7 +61,7 @@ use graphics::{GState, Out};
|
||||
use interp::{Fault, Interp};
|
||||
use obj::PsError;
|
||||
|
||||
pub use native::{is_loss, layered_ai};
|
||||
pub use native::{ai_alone, is_loss, layered_ai};
|
||||
pub use text::family_style;
|
||||
|
||||
/// The page of a PostScript file without a bounding box (US Letter).
|
||||
|
||||
@@ -31,6 +31,8 @@ use super::ate::{Story, Texts};
|
||||
|
||||
/// The notes that say the import left out something the file has (see [`is_loss`]).
|
||||
const TEXT_LEFT_OUT: &str = "text objects on hidden layers or hidden objects couldn't be read, so they are left out";
|
||||
/// The same for a file without a page to take shown type from.
|
||||
const TYPE_LEFT_OUT: &str = "text objects of kinds VectorCraft doesn't read from the file's text document yet are left out";
|
||||
const ART_LEFT_OUT: &str = "hidden layers have art this can't read";
|
||||
const LAYERS_UNREAD: &str = "the file's layers weren't read";
|
||||
|
||||
@@ -38,6 +40,7 @@ const LAYERS_UNREAD: &str = "the file's layers weren't read";
|
||||
/// layers that could not be read)? Writing the document over the file would lose it for good.
|
||||
pub fn is_loss(note: &str) -> bool {
|
||||
note.ends_with(TEXT_LEFT_OUT)
|
||||
|| note.ends_with(TYPE_LEFT_OUT)
|
||||
|| [ART_LEFT_OUT, LAYERS_UNREAD, super::graphics::TOO_MUCH, super::graphics::FAR_AWAY].iter().any(|n| note.starts_with(n))
|
||||
}
|
||||
|
||||
@@ -512,7 +515,7 @@ fn plan(
|
||||
};
|
||||
}
|
||||
}
|
||||
if spare.len() != loose.len() {
|
||||
if !spare.is_empty() && spare.len() != loose.len() {
|
||||
warn.push("the text is on the layers in the order the page paints it, so a text object may be in another group than it was".into());
|
||||
}
|
||||
}
|
||||
@@ -600,31 +603,36 @@ struct Frame<'a> {
|
||||
}
|
||||
|
||||
/// The document of the editing copy `data`, with the text (and strokes round it) of `visible`, the
|
||||
/// file read as its page, put in its text objects → the document, its notes and where the page is
|
||||
/// in it.
|
||||
fn build(data: &[u8], visible: &Document, page: Page) -> Result<(Document, Vec<String>, Rect), String> {
|
||||
/// file read as its page, put in its text objects → the document and its notes. Without a page
|
||||
/// (a `.ai` saved without its PDF part), its type is made from the file's text document alone and
|
||||
/// nothing is compared.
|
||||
fn build(data: &[u8], visible: Option<&Document>, page: Page) -> Result<(Document, Vec<String>), String> {
|
||||
let Structure { mut doc, to_doc, bbox, artboard, template, hidden_unread, slot_names, warnings } = ai::read(data)?;
|
||||
let art_box = match page {
|
||||
Page::Art => bbox.map(|[a, b, c, d]| Rect::new(a, b, c, d)).ok_or("its editing data doesn't say where its art is")?,
|
||||
Page::Artboard => artboard.ok_or("its editing data doesn't say where its artboard is")?,
|
||||
};
|
||||
let page_rect = visible.artboards.first().map(|a| a.rect).ok_or("it has no page")?;
|
||||
if (art_box.width() - page_rect.width()).abs() > 0.5 || (art_box.height() - page_rect.height()).abs() > 0.5 {
|
||||
return Err("its editing data is for a different page".into());
|
||||
}
|
||||
// The page's area in the layers' document, and the page's type moved there.
|
||||
let rect = to_doc.transform_rect_bbox(art_box);
|
||||
let shift = Affine::translate(Vec2::new(rect.x0 - page_rect.x0, rect.y0 - page_rect.y0));
|
||||
let (mut shown, mut hidden) = (vec![], vec![]);
|
||||
for l in &visible.layers {
|
||||
if let Some(c) = l.children() {
|
||||
text_objects(c, l.visible, &mut shown, &mut hidden);
|
||||
let mut frame = None;
|
||||
if let Some(visible) = visible {
|
||||
let art_box = match page {
|
||||
Page::Art => bbox.map(|[a, b, c, d]| Rect::new(a, b, c, d)).ok_or("its editing data doesn't say where its art is")?,
|
||||
Page::Artboard => artboard.ok_or("its editing data doesn't say where its artboard is")?,
|
||||
};
|
||||
let page_rect = visible.artboards.first().map(|a| a.rect).ok_or("it has no page")?;
|
||||
if (art_box.width() - page_rect.width()).abs() > 0.5 || (art_box.height() - page_rect.height()).abs() > 0.5 {
|
||||
return Err("its editing data is for a different page".into());
|
||||
}
|
||||
}
|
||||
for object in shown.iter_mut().chain(hidden.iter_mut()) {
|
||||
for n in object.iter_mut() {
|
||||
Arc::make_mut(n).transform(shift, false);
|
||||
// The page's area in the layers' document, and the page's type moved there.
|
||||
let rect = to_doc.transform_rect_bbox(art_box);
|
||||
let shift = Affine::translate(Vec2::new(rect.x0 - page_rect.x0, rect.y0 - page_rect.y0));
|
||||
for l in &visible.layers {
|
||||
if let Some(c) = l.children() {
|
||||
text_objects(c, l.visible, &mut shown, &mut hidden);
|
||||
}
|
||||
}
|
||||
for object in shown.iter_mut().chain(hidden.iter_mut()) {
|
||||
for n in object.iter_mut() {
|
||||
Arc::make_mut(n).transform(shift, false);
|
||||
}
|
||||
}
|
||||
frame = Some(Frame { page: visible, page_rect, rect });
|
||||
}
|
||||
let mut infos = vec![];
|
||||
collect_slots(&doc.layers, true, &mut infos);
|
||||
@@ -656,18 +664,22 @@ fn build(data: &[u8], visible: &Document, page: Page) -> Result<(Document, Vec<S
|
||||
fill_slots(&mut doc, &mut layers, &mut content.into_iter(), &slot_names, &mut empty);
|
||||
doc.layers = layers;
|
||||
if empty > 0 {
|
||||
notes.push(format!("{empty} {TEXT_LEFT_OUT}"));
|
||||
notes.push(format!("{empty} {}", if frame.is_some() { TEXT_LEFT_OUT } else { TYPE_LEFT_OUT }));
|
||||
}
|
||||
if !hidden_unread.is_empty() {
|
||||
notes.push(format!("{ART_LEFT_OUT} ({}), left out of them", few(hidden_unread)));
|
||||
}
|
||||
let frame = Frame { page: visible, page_rect, rect };
|
||||
let unseen = prune_unseen(&frame, &mut doc);
|
||||
if unseen > 0 {
|
||||
notes.push(format!("{unseen} {UNSEEN_TEXT}"));
|
||||
match frame {
|
||||
Some(frame) => {
|
||||
let unseen = prune_unseen(&frame, &mut doc);
|
||||
if unseen > 0 {
|
||||
notes.push(format!("{unseen} {UNSEEN_TEXT}"));
|
||||
}
|
||||
notes.extend(compare(&frame, &doc)?);
|
||||
}
|
||||
None => unmark(&mut doc.layers, true, &mut vec![]),
|
||||
}
|
||||
notes.extend(compare(&frame, &doc)?);
|
||||
Ok((doc, notes, rect))
|
||||
Ok((doc, notes))
|
||||
}
|
||||
|
||||
/// Marks (in a node's name, until [`prune_unseen`] has looked) the text objects made from the file's
|
||||
@@ -765,9 +777,9 @@ fn compare(frame: &Frame<'_>, layered: &Document) -> Result<Option<String>, Stri
|
||||
/// has one that can't be used, `visible` with a warning that says why.
|
||||
pub(super) fn layered(ps: &[u8], visible: Imported) -> Imported {
|
||||
let Some(data) = ai::eps_data(ps) else { return visible };
|
||||
let result = data.and_then(|data| build(&data, &visible.document, Page::Art));
|
||||
let result = data.and_then(|data| build(&data, Some(&visible.document), Page::Art));
|
||||
match result {
|
||||
Ok((document, mut warnings, _)) => {
|
||||
Ok((document, mut warnings)) => {
|
||||
for w in &visible.warnings {
|
||||
if !warnings.contains(w) {
|
||||
warnings.push(w.clone());
|
||||
@@ -792,8 +804,8 @@ pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec<String>) -> (
|
||||
if !data.windows(15).any(|w| w == b"%AI5_BeginLayer") {
|
||||
return (visible, warnings);
|
||||
}
|
||||
match build(&data, &visible, Page::Artboard) {
|
||||
Ok((done, notes, _)) => {
|
||||
match build(&data, Some(&visible), Page::Artboard) {
|
||||
Ok((done, notes)) => {
|
||||
let mut all = warnings;
|
||||
all.extend(notes.into_iter().filter(|n| !all.contains(n)).collect::<Vec<_>>());
|
||||
(done, all)
|
||||
@@ -806,6 +818,13 @@ pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec<String>) -> (
|
||||
}
|
||||
}
|
||||
|
||||
/// A `.ai` saved without its PDF part (its pages show only a placeholder) from its editing copy
|
||||
/// alone (`private`, the joined `AIPrivateData` streams) → the document and the import's notes.
|
||||
pub fn ai_alone(private: &[u8]) -> Result<(Document, Vec<String>), String> {
|
||||
let data = ai::decode_private(private)?;
|
||||
build(&data, None, Page::Artboard)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -521,3 +521,24 @@ fn a_dictionary_left_open_at_the_end_of_a_layer_is_a_damaged_file() {
|
||||
assert_eq!(names(&r.document), ["Layer 1"], "{:?}", r.warnings);
|
||||
assert!(r.warnings.iter().any(|w| crate::is_loss(w)), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ai_file_without_its_pdf_part_reads_its_layers_and_makes_its_type() {
|
||||
// Point type from the text document, shown or not; a story it can't read is left out, with a
|
||||
// note that is a loss.
|
||||
let mut doc = text_document(&[("Hi there\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0), ("Area\r", 0, (8200.0, 8160.0), (0.0, 0.0), 0.0)]);
|
||||
// Story 1 in frame 1, a frame that says more than a matrix.
|
||||
let at = doc.rfind("/1 << /0 [ << /0 0 >> ]").unwrap();
|
||||
doc.replace_range(at..at + 23, "/1 << /0 [ << /0 1 >> ]");
|
||||
let at = doc.rfind("/2 << /2 [ 1 0 0 1 0 0 ] >>").unwrap();
|
||||
doc.replace_range(at..at + 27, "/2 << /2 [ 1 0 0 1 0 0 ] /7 1 >>");
|
||||
let art = native_with_text(&(layer("Back", true, &square(10, 10)) + &layer("Words", true, &(text_object(0) + &text_object(1)))), &doc);
|
||||
let (d, notes) = crate::ai_alone(&private(&art)).unwrap();
|
||||
assert_eq!(names(&d), ["Back", "Words"], "{notes:?}");
|
||||
let texts: Vec<String> = text_of(&d.layers[1]).iter().map(|t| t.plain_text()).collect();
|
||||
assert_eq!(texts, ["Hi there"], "{notes:?}");
|
||||
let left = notes.iter().find(|n| n.contains("left out")).unwrap();
|
||||
assert!(left.starts_with("1 ") && crate::is_loss(left), "{notes:?}");
|
||||
assert!(!notes.iter().any(|n| n.contains("order the page paints")), "{notes:?}");
|
||||
assert!(crate::ai_alone(b"%AI24_ZStandard_Data nothing").is_err());
|
||||
}
|
||||
@@ -34,7 +34,7 @@ mod ps;
|
||||
mod scene;
|
||||
mod tiff;
|
||||
|
||||
pub use import::{Imported, family_style, import, is_loss, layered_ai};
|
||||
pub use import::{Imported, ai_alone, family_style, import, is_loss, layered_ai};
|
||||
pub use print::{PrintJob, PrintPage, print};
|
||||
|
||||
use vectorcraft_doc::Document;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -732,11 +732,13 @@ fn zoom_about(vm: &mut View, rect: egui::Rect, p: Pos2, zoom: f64) {
|
||||
vm.center += before - Xf::new(rect, vm).to_doc(p);
|
||||
}
|
||||
|
||||
/// A dashed marquee around screen rectangle `r`.
|
||||
/// A marquee around screen rectangle `r`: dark dashes over a light line, so it shows on a white
|
||||
/// artboard and on the grey pasteboard alike (#725).
|
||||
fn marquee(p: &egui::Painter, r: egui::Rect) {
|
||||
let pts = [r.left_top(), r.right_top(), r.right_bottom(), r.left_bottom(), r.left_top()];
|
||||
p.add(Shape::line(pts.to_vec(), Stroke::new(1.0, Color32::from_white_alpha(230))));
|
||||
for w in pts.windows(2) {
|
||||
p.extend(Shape::dashed_line(&[w[0], w[1]], Stroke::new(1.0, Color32::from_gray(90)), 3.0, 3.0));
|
||||
p.extend(Shape::dashed_line(&[w[0], w[1]], Stroke::new(1.0, Color32::from_gray(40)), 3.0, 3.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1539,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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
@@ -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
|
||||
@@ -3676,6 +3682,7 @@ axis Both Ambos
|
||||
@msg an image VectorCraft can't read was left out se omitió una imagen que VectorCraft no puede leer
|
||||
@msg an image whose data is cut short was left out se omitió una imagen con datos incompletos
|
||||
@msg its editing data doesn't say where its art is sus datos de edición no dicen dónde está su ilustración
|
||||
@msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) este archivo se guardó sin compatibilidad con PDF y sus datos de edición no se pueden leer ({why})
|
||||
|
||||
# The macOS menu bar (native_menu): the app and Window menus' system items, Settings pages.
|
||||
Services Servicios
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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
|
||||
@@ -3673,6 +3679,7 @@ axis Both Les deux
|
||||
@msg an image VectorCraft can't read was left out une image que VectorCraft ne peut pas lire a été ignorée
|
||||
@msg an image whose data is cut short was left out une image aux données tronquées a été ignorée
|
||||
@msg its editing data doesn't say where its art is ses données d’édition ne disent pas où se trouve son illustration
|
||||
@msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) ce fichier a été enregistré sans compatibilité PDF et ses données d’édition sont illisibles ({why})
|
||||
|
||||
# The macOS menu bar (native_menu): the app and Window menus' system items, Settings pages.
|
||||
Services Services
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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
|
||||
@@ -3676,6 +3682,7 @@ axis Both Entrambi
|
||||
@msg an image VectorCraft can't read was left out un'immagine che VectorCraft non può leggere è stata omessa
|
||||
@msg an image whose data is cut short was left out un'immagine con dati incompleti è stata omessa
|
||||
@msg its editing data doesn't say where its art is i suoi dati di modifica non dicono dove si trova la grafica
|
||||
@msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) questo file è stato salvato senza compatibilità PDF e i suoi dati di modifica non si possono leggere ({why})
|
||||
|
||||
# The macOS menu bar (native_menu): the app and Window menus' system items, Settings pages.
|
||||
Services Servizi
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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.
|
@@ -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.
|
@@ -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 в нём нет страницы
|
||||
@@ -3672,6 +3678,7 @@ axis Both Оба
|
||||
@msg an image VectorCraft can't read was left out изображение, которое VectorCraft не может прочитать, пропущено
|
||||
@msg an image whose data is cut short was left out изображение с обрезанными данными пропущено
|
||||
@msg its editing data doesn't say where its art is в его данных редактирования не указано, где находится графика
|
||||
@msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) этот файл сохранён без совместимости с PDF, и его данные редактирования не удаётся прочитать ({why})
|
||||
|
||||
# The macOS menu bar (native_menu): the app and Window menus' system items, Settings pages.
|
||||
Services Службы
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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.
|
@@ -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.
|
@@ -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)),
|
||||
|
||||
@@ -45,10 +45,14 @@ struct Entry {
|
||||
/// The palette's entries in `lang`. They are built when the language, the installed plug-ins or
|
||||
/// the keyboard shortcuts (shown beside each entry) change, not on every frame while a query is
|
||||
/// typed.
|
||||
/// What the entries depend on: the language, the installed plug-ins and the shortcuts.
|
||||
fn cache_key(lang: crate::i18n::Lang) -> (&'static str, u64, u64) {
|
||||
(lang.code(), menus::plugin_revision(), crate::shortcut_editor::GENERATION.load(std::sync::atomic::Ordering::Relaxed))
|
||||
}
|
||||
|
||||
fn entries(ctx: &egui::Context, lang: crate::i18n::Lang) -> Arc<Vec<Entry>> {
|
||||
type Cached = ((&'static str, u64, u64), Arc<Vec<Entry>>);
|
||||
let shortcuts = crate::shortcut_editor::GENERATION.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let key = (lang.code(), menus::plugin_revision(), shortcuts);
|
||||
let key = cache_key(lang);
|
||||
let id = egui::Id::new("palette-entries");
|
||||
if let Some((k, v)) = ctx.data(|d| d.get_temp::<Cached>(id))
|
||||
&& k == key
|
||||
@@ -121,8 +125,16 @@ mod tests {
|
||||
fn entries_are_built_once_per_language_and_match_english_shown_text_and_ids() {
|
||||
let ctx = egui::Context::default();
|
||||
let zh = Lang::from_code("zh-hant").unwrap();
|
||||
let en = entries(&ctx, Lang::EN);
|
||||
assert!(Arc::ptr_eq(&en, &entries(&ctx, Lang::EN)), "kept while the language stays");
|
||||
// Tests running alongside edit shortcuts and install plug-ins, which rebuilds the entries:
|
||||
// look twice until nothing changed in between.
|
||||
let (en, again) = (0..100)
|
||||
.find_map(|_| {
|
||||
let key = cache_key(Lang::EN);
|
||||
let pair = (entries(&ctx, Lang::EN), entries(&ctx, Lang::EN));
|
||||
(cache_key(Lang::EN) == key).then_some(pair)
|
||||
})
|
||||
.unwrap();
|
||||
assert!(Arc::ptr_eq(&en, &again), "kept while the language stays");
|
||||
let zh_entries = entries(&ctx, zh);
|
||||
assert!(!Arc::ptr_eq(&en, &zh_entries), "rebuilt for another language");
|
||||
// Editing a shortcut rebuilds them, so the palette shows the new one.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
-1
@@ -436,7 +436,9 @@ CMYK too.
|
||||
```
|
||||
|
||||
PostScript files (`.eps`, and `.ai` files saved in older formats or without PDF compatibility) open through the EPS
|
||||
reader (see EPS and PostScript import); an `.ai` whose PDF part is only a placeholder page says it can't be opened.
|
||||
reader (see EPS and PostScript import). An `.ai` saved without PDF compatibility (its PDF part is only a placeholder
|
||||
page) opens from its editing data alone: its type is made from the file's text document where it can be (point type), and
|
||||
what can't be is left out with a warning; without editing data it says it can't be opened.
|
||||
|
||||
What a PDF holds comes in as editable art: soft masks become opacity masks (an alpha mask as a white copy of its art;
|
||||
the backdrop colour gives Clip, an inverting transfer function Invert), transparency groups keep isolation and knockout,
|
||||
|
||||
Reference in new issue
Block a user