main
63
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3374d0778d |
Scripts: Check Script… reports what a .jsx script needs and does, without running it
VectorCraft does not run scripts written for the reference app, and File › Scripts held only the disabled Other Script…. File › Scripts › Check Script… picks a .jsx, .jsxinc or .jsxbin file, reads it without running any of it, and shows a report: what the script needs (the script language's own objects, the document model, files, menu commands, dialogs, or features only the reference app has), what it would do outside the document, the menu-command strings it passes, the document-model member chains and type names it uses, its directives, the files it includes, and the problems that would stop it loading. Copy Report copies the report as JSON. File › Open and drag-and-drop show the report for those files too. - vectorcraft-scriptcheck, a new Standalone crate with no dependencies, holds the checker: a lexer for ECMAScript 3 with E4X literals and the #name and //@name directive lines, and an analysis of the tokens that follows variables, function scopes and the files a script includes. It never recurses: open brackets, the levels of an XML literal and the function bodies being read are kept on stacks capped at 4,096. It reads at most 8 MiB of a file and 32 MiB with the included files, follows 64 included files 16 deep, looks up at most 1,024 paths for them, keeps 1,000 entries in a list, tracks 1,000 variables holding member chains and 1,000 holding File objects, and cuts echoed text to 120 characters. A compiled script is reported as compiled and isn't read. - script.check (engine, a query) takes path, data or dataBase64. With a path and includes: true, it reads the files the #include lines name from inside the script's folder, through the automation read root, and when the script or one of those files assigns a member chain or a File object to a variable, the check treats the variable as holding it in every file. It doesn't follow absolute names, names, folders or links that lead out of the folder, names or folders longer than 4,096 bytes, or JSINCLUDE, and it reads only regular files. - ui.checkScript shows the report (dialog scriptCheck). File › Open lists Scripts (.jsx, .jsxinc, .jsxbin) and sends a script's path to script.check, which reads at most 8 MiB of it. The language rules come from ECMA-262 3rd edition and ECMA-357 2nd edition. The directive, platform and document-model root names come from the public JavaScript Tools Guide and scripting guide pages that catalog.rs cites by URL, and executeMenuCommand, doScript, loadAction, unloadAction and selectTool come from the public forum posts cited there; only names are used. Every script in the tests is written from scratch with invented names. Docs: docs/mcp.md, docs/target-app-parity.md. |
||
|
|
f0810ca58b |
Object: Lock and Hide › All Artwork Above go through each top-level layer once
Lock › All Artwork Above and Hide › All Artwork Above went through the objects above each selected object in its top-level layer, one selected object at a time. For each object they looked up in a list whether it was selected or already found, and they computed its bounds again for each selected object below it until one overlapped it. With 100 tiles selected under 25,000 rectangles, a release build of vectorcraft-cli took 6.4 s for Lock, and with all of 5,001 objects selected it took 8.8 s and locked nothing. The commands now go through each top-level layer that holds a selected object once, and check each object that is not selected against the bounds of the selected objects below it. The selected objects and the objects found are kept in sets, and each object's bounds are computed once. The objects they lock or hide, the count, the selection and the undo step are the same as before. On the same documents Lock now takes under 0.1 s. Docs: docs/target-app-parity.md. |
||
|
|
35d6aa0a16 |
Render: pattern tiles at different scales are kept between frames
The renderer kept one tile per pattern name and ink plane, and used it only at the raster scale it was made at: the device scale rounded up to a power of two and capped. Two objects that paint one pattern at scales for which the raster scale differs, such as a pattern scaled to 25% with Transform Pattern Tiles next to the same pattern at 100%, replaced each other's tile, and every frame rasterized both tiles again. Tiles are now kept per pattern name and raster scale, within the tile cache's budget of 128 MiB per thread and ink plane. When the budget fills, the cache drops the least recently used tiles first. A tile is still used only while the pattern's art, tile, tile type and overlap are the ones it was made from. At 760%, with a pattern of 576 stars in a 288 pt tile on a rectangle that fills the view, and the same pattern at 25% on a square, release builds of vectorcraft-cli bench at 2880x1800 took a median of 26.0 ms a frame on main and 8.8 ms with this change, with 8 threads. Docs: docs/target-app-parity.md. |
||
|
|
fb2089aaf5 |
macOS: the app no longer reads the menus at every step of a move, scale or shape drag
On macOS, native_menu::sync reads every row of the menus (each command's enabled and checked state, and each label) whenever state_hash changes, and the macOS backend updates the AppKit items whose label, enabled or checked state changed. The hash included the active document's revision, and Session::preview makes a new revision at each step of a drag, so the app read every row again at each step of a move, scale or shape drag. In a release build on an M2 Max, that took about 1.5 ms a step with one object selected, and 13.8 ms a step with 5,001 objects selected. While an interaction is open, state_hash leaves the revision out. Move, scale and shape drags, dialog previews and the font menu's preview are interactions. The menus are read when an interaction begins and again on the frame it ends: when a drag's button is released, when Escape cancels a drag, when a move is dragged onto a panel, when a dialog's preview is applied or canceled, when the font menu's preview ends, or when a control request such as ui.dialog.cancel ends it. They are still read on a click or a key press during an interaction. docs/development.md says when the menus are updated during an interaction. |
||
|
|
88bd4812e0 |
Part of #1429: path, Pathfinder and live-object commands add and remove many objects in one pass
Offset Path, Outline Stroke, the Pathfinder panel, Clean Up, Make Blend, Rasterize and the other commands that #1429's table marks 3, except Split Into Grid, added, removed or replaced their objects one at a time, and so did Remove Anchor Points, Redefine Symbol and the removal of Shaper groups whose source art is gone. Each insertion or removal moved the later objects of the layer or group. The lookups by id that followed walked the tree, and every fourth walk rebuilt the lookup cache. On 25,000 rectangles a release build of vectorcraft-cli took 29.1 s for Offset Path. These commands now make their edits with Document::wrap, Document::insert_beside and Document::remove_many, in one pass over the children of each layer or group, and Outline Stroke and Expand write each new object into the place of the object it replaces. A list that holds an object twice, or an object and another inside it, and that was edited without an error before, is still edited one object at a time. The documents, selections and undo steps are the same. On the same rectangles Offset Path now takes 1.3 s. Docs: docs/target-app-parity.md. |
||
|
|
6f9e2d5c16 |
Part of #1429: Layers panel and artboard commands on many rows take time linear in the document
These commands removed, moved or copied one row or object at a time: the Layers panel's Delete Selection, Duplicate Selection, Collect in New Layer, row drags, Merge Selected, Flatten Artwork and Release to Layers, and Cut Artboard, Duplicate Artboard and the Artboard tool's Alt-drags that copy art. Each edit moved the later rows of the same layer or group to other indices. The next lookup by id then walked the tree, and after every fourth such walk Document::index_path rebuilt the whole lookup cache. Release to Layers also walked the document twice for each new layer to name and color it. With 25,000 rectangles in one layer, a release build of vectorcraft-cli took 11.9 s for Duplicate Selection and 17.7 s for Release to Layers. The commands now remove, move and copy the rows in one pass over each layer or group, with Document::remove_many and Document::wrap, and with copy_above, the copy pass of duplicate_in, which Duplicate Selection and the artboard copies now share. Release to Layers names its new layers from one walk of the document. Merge Selected checks the target layer's objects against a set of the listed rows, and Delete Selection checks the top-level layers against such a set. In a document whose ids are unique, the rows keep their order, new rows and layers take the same ids, names and colors, and the selection, the undo step and the journal are the same. A release build with this change takes 98 ms for Duplicate Selection and 26 ms for Release to Layers. Docs: docs/target-app-parity.md. |
||
|
|
22e06db73d |
CLI: perf on Windows reports the cores busy and its working set, and the help lists --deep
On Windows, perf printed "load average ?": load_average runs sysctl or reads /proc/loadavg, and Windows has neither. The warning for a busy machine could not print, so a run over budget on a busy machine failed, where macOS and Linux warn and exit 0. perf --deep printed no memory rows, because rss_mb runs ps. --help did not list --deep. On Windows, perf now reads GetSystemTimes twice, one second apart, and its first line gives the cores busy on average over that second where macOS and Linux give the load average. It warns above three quarters of the cores, as on macOS and Linux: with all 32 logical processors of a test machine kept busy, the line read "cores busy 32.0" and a run with three budgets over exited 0. The --deep memory rows give the working set (GetProcessMemoryInfo). Both calls use winsafe 0.0.29, which Cargo.lock already lists for the desktop app; the CLI depends on it on Windows with the kernel and psapi features. The help lists --deep and says that a run over budget on a busy machine exits 0. perf's report on macOS and Linux does not change. |
||
|
|
571289ddb6 |
Part of #1429: Alt-drags with the selection and transform tools copy the objects once per drag
On each pointer move of an Alt-drag with the Selection, Direct Selection, Group Selection, Rotate, Scale, Reflect or Shear tool, Session::preview put the drag's snapshot back and ran object.transform with copy again, and duplicate_in copied every selected object again. The Move, Rotate, Scale, Reflect and Shear dialogs with Copy and Preview on copied every selected object again on each change of a value. The interaction now keeps the untransformed copies that its first preview with copy makes of the snapshot. Each later preview that copies the same objects of the same snapshot transforms only those copies. The copies keep their ids, and the document, the selection, the undo step and the journal are the same as before. With 25,000 rectangles selected, a release build of vectorcraft-cli took a median of 138.5 ms for each Alt-drag move after the first preview and takes 105.7 ms with this change. Docs: docs/target-app-parity.md. |
||
|
|
f4e72fe6a5 |
Render: raster effects are made again when patterns, symbols, images, settings or fonts change
The renderer kept the rasters of an object's drop shadows, outer glows and filtered content (Gaussian Blur, Feather and the Photoshop-style filters), and drew them again while the object, the zoom and rotation and the ink plane stayed the same. The content drawn into a raster also reads things outside the object: patterns, symbols, brushes, images, document, layer and color settings, the knockout a group inherits, the fonts, and the options the frame is drawn with. When one of them changed, a renderer that had drawn the object went on drawing the raster it had kept, made from the old values. Each kept raster now records what its content was drawn from: which of those things the object reads, found by walking the object when the raster is made, and a hash of their values then. The renderer draws the raster only while that hash is the same in the current frame. Once a frame, the renderer hashes the document's patterns, symbols and images. It hashes their art and pixels by the addresses of their shared values, and it keeps weak references to those values while a raster made with them is kept, so no other value can take one of those addresses. A filtered raster also records the color channels its filters split, and under De-Interlace the raster resolution. A temporary release test timed the inputs of 400 kept rasters at about 10 microseconds a frame. |
||
|
|
8754646784 |
Type: frames and paths beyond 1,000,000,000 pt hold no text, and layout limits its search for room
Opening an EPS whose threaded area type frame has a control point at
x = 8e115 failed with an internal error ("capacity overflow").
`eps_text_frames_never_panic` in `import_fuzz` found it with
PROPTEST_CASES=20000. Text layout flattens a frame to within 0.1 pt,
and for such a curve kurbo 0.13.1's `flatten` calls `reserve` for
`usize::MAX` quadratic pieces. Laying out text around a wrap shape with
such a curve failed the same way. Laying out type on a path along such
a curve panicked in kurbo's `solve_itp` ("attempt to shift left with
overflow") in builds with overflow checks, and in a release build it
did not finish within 20 s. Laying out area type in a triangle frame
that comes to a point at its top, at y = 1e20, did not finish either:
adding a lead to a baseline that large leaves the baseline where it is.
Text layout now leaves out a frame, a text wrap shape or the path of
type on a path that has a coordinate beyond 1,000,000,000 pt either
way, control points included, or a NaN coordinate in an anchor point.
Such a frame or path holds no text, and such a wrap shape is ignored.
PDF export uses the same bound (`MAX_COORD` in
`crates/pdf/src/export.rs`).
Text layout also steps a line down a frame cell at most 100,000 times
to find room for it. In a cell taller than 100,000 leads, a step is the
cell's height divided by 100,000; in a shorter cell, a step is one
lead, as before. Before this change, in a dev build, with triangle
frames that come to a point at their top, laying out text in one
2,000,000,000 pt tall took 9.8 s, and laying out a paragraph whose
Space Before is -1e300 pt in one 1,000 pt tall did not finish within
20 s.
|
||
|
|
ffa948dcc3 |
Part of #1429: Edit and Object commands on many objects take time linear in the document
Delete, Cut, Group, Make Compound Path, Make Clipping Mask, Send to Current Layer, Ungroup and Release Compound Path removed, inserted or moved one object at a time. duplicate_in inserted one copy at a time; Duplicate, Copy in the Move, Rotate, Scale, Reflect and Shear dialogs, Transform Again after a copy, Transform Each with Copy, the perspective copies and each move of an Alt-drag call it. Each insertion or removal moved the later objects of the same layer or group to other indices. The next lookup by id then walked the tree, and after every fourth such walk Document::index_path rebuilt the whole lookup cache. Each command did work that grew with the number of objects times the size of the document: with 25,000 rectangles selected, a release build of vectorcraft-cli took 7.2 s to group them and 11.5 s to copy them with object.transform. Document::remove_many, Document::wrap and Document::insert_beside remove nodes, wrap them in a new node, or put nodes directly above or in place of their anchors, in one pass over the children of each parent. Each finds the nodes with one index_paths call, checks every id before it changes anything, and clears the lookup cache once. The commands above use them. The objects keep their order, the copies take their ids in the same order, and the selection, the undo step and the journal are the same. duplicate_in still copies one root at a time when a root is listed twice or lies inside another root. A release build with this change groups the 25,000 rectangles in 27 ms and copies them in 121 ms. Docs: docs/target-app-parity.md. |
||
|
|
412e83c825 |
Render: the placed art of symbol instances and inline graphics is kept between frames
The renderer drew each symbol instance by copying its symbol's art, staining the copy for a stained instance and moving every object in it by the instance's transform, in every frame. The copies were new nodes, so the caches the renderer keys by a node's address (cull bounds, path geometry, glyph layouts, live-effect geometry) missed for every object of every instance, and the copies stayed in those caches until they were pruned. Stroke outlines missed as well, because their cache uses an entry only when the entry holds the node being drawn. The renderer copied and moved the art of inline graphics in type the same way. The renderer now keeps the art it places in a frame cache with a 64 MiB budget, under the address of the symbol's art, the transform, whether strokes scale with it, and the instance's stain. When the art is a group or a layer without an appearance of its own, the renderer copies none of it in a frame whose instances and symbols did not change, and the objects in it hit the caches keyed by their own address (cull bounds and paths, glyph layouts, live-effect geometry). The stroke-outline cache keys a stroke by its address in the appearance. In the art placed for an unstained instance, each object without a gradient fill or stroke shares its appearance with the same object in the symbol's art. When a symbol has two or more unstained instances, the renderer therefore still outlines the strokes of those objects again for each instance in every frame. Moving or restaining an instance, or editing its symbol's art, places that art again, and the other instances keep theirs. Other art is placed again in every frame, as before, and so is art that holds a symbol instance, type with an inline graphic, an image, a placed document, a blend, an envelope, a repeat or a raster effect. Art that holds type is placed again when the fonts change. vectorcraft-cli bench of a symbol of eight paths (three filled and stroked, two only filled, three only stroked) shown by 2,000 unstained instances took 51.6 ms a frame on main and 32.3 ms with this change, at 2880x1800 on 8 threads; the same paths in 2,000 groups took 26.0 ms. The peak working set of a bench run at that size went from 150 MiB to 83 MiB. In the tests, a renderer that keeps placed art draws the same pixels as a new renderer, and instances draw the same pixels as groups of their placed art. |
||
|
|
a9a7a4cddf |
Fix #1257: a dragged object reuses its filter and shadow rasters
Each pointer move of a drag applies the move to the interaction's snapshot, so the dragged object is a new node in every frame. The renderer kept the rasters of an object's drop shadows, outer glows and filtered content (Gaussian Blur, Feather and the Photoshop-style filters) only for the same node, so every frame of a drag ran those effects again. On main, a moved frame of texture_drag_perf (a CMYK rounded rectangle at 190.07% and DPI 1.5 in an 1800 x 1500 view) took 112 to 197 ms in a release build. The canvas now renders with RenderOptions::interacting while a drag or a dialog's preview is in progress. When an object misses its cached raster in such a frame, the renderer takes the raster an earlier frame made for an object with the same id, slot and ink at the same zoom and rotation, made by the same effects from content of the same size (to within 0.000001 pt). It draws the object's content into a raster placed as that one was and compares the pixels byte for byte (only the alpha under a shadow or glow). For Feather it also compares the points of the clip outline, to within 0.000001 pt. When they match, the old raster is drawn at the new place, rounded to a whole pixel, as a pan draws a cached raster. Content under De-Interlace never takes a moved raster, and a raster made over moved rasters counts as moved itself. A frame rendered without interacting never draws such a raster, or a raster made over one: the frame after the drag runs the effects again and draws what a fresh renderer draws. The canvas's cache key includes whether an interaction is in progress. On the desktop, the canvas sends the frame after an interaction to the render worker when the frame shown drew moved rasters. Each cached raster keeps the content it was made from, within the existing 512 MiB budget. The pixels kept with a filtered raster take as many bytes as the raster, and the alpha kept with a shadow or glow takes a quarter as many as the shadow. texture_drag_perf now renders a drag's frames as the canvas does, then the frame after the drag, and that frame once more with every raster cached. A moved frame now takes 20 to 26 ms, close to the 18 to 25 ms of the cached frame; the frame after the drag takes 110 to 189 ms. Docs: docs/target-app-parity.md. |
||
|
|
5d5e7a1dd2 |
PDF export: art beyond 1,000,000,000 pt is left out, so dev and test builds no longer abort
In builds with overflow checks (`cargo run`, `cargo test` and `cargo xtask ci`), PDF export aborted the process when a number it wrote rounded to -2,147,483,648 as an f32. pdf-writer 0.15 writes -2,147,483,648.0 as an integer, and `Limits::register_int` overflows when it takes the absolute value. krilla 0.8.2's `Surface` then fails an assertion in its `Drop` while that panic unwinds. `svg_hostile_markup_never_panics` in `import_fuzz` hit it with type shifted 2,147,483,649 pt down. Paths, gradients, dash offsets, images and artboards that far out hit it too, and so does a group around a stroke wide enough to reach that far. Release builds have no overflow checks. PDF export now leaves out art whose coordinates or transform entries lie beyond 1,000,000,000 in its own space or in the content stream it is drawn into, and glyph lines whose spacing lies beyond 1,000,000,000 thousandths of an em. That covers paths and clipping paths, glyph lines, images, gradients, strokes whose width, caps or miter spikes reach that far, and the art and marks of a page whose artboard lies that far, which is written blank. A dash offset beyond the bound is written as its remainder after whole periods of the pattern. The type's transform is now pushed inside the form that marks a fill or stroke of type to overprint. The exporter checks what that form holds through the type's transform, as it checks the same fill or stroke without overprint. |
||
|
|
44685d8798 |
Align: Object › Align follows Align To, and the choice is kept across restarts
Object › Align ran object.align without to, so it aligned to the key object while there was one and otherwise to the selection. With Align to Artboard chosen it aligned two objects to the selection's bounds, and it left a single selected object where it was, while the align buttons of the Align panel, the Control bar and the Properties panel move that object to the active artboard. The Align To choice was kept in egui's temporary memory, so it went back to Align to Selection at each launch, and ui.inspect did not report it. The choice is now the UiState field align_to, which the desktop app saves with its preferences and ui.inspect reports. A saved value other than "artboard" reads as Selection, and the other preferences still load. Object › Align now sends the to that the align buttons send: the artboard when one object is selected, else the key object while there is one, else the Align To choice. With anchor points selected it sends its parameters as before, and the points align to their own bounds. While one object is selected, the Align To row shows Artboard. Agents' object.align calls without to keep the engine's default. Docs: docs/control-protocol.md, docs/target-app-parity.md. |
||
|
|
ae061eacfb |
Part of #394: art moved whole with direct-selected anchors follows Transform Pattern Tiles
With anchors of one path direct-selected and another object selected whole, the arrow keys (object.nudge), path.moveAnchors and a Direct Selection drag of the selected anchors moved that object without its pattern tiles, whatever General › Transform Pattern Tiles was set to. When the same object was selected on its own, the arrow keys and a Direct Selection drag followed the preference. path.moveAnchors left the tiles in place in that case too. path.moveAnchors now follows the preference for the objects it moves whole, as the transforms, Align and Distribute do. path.moveAnchors and object.nudge also take a `patterns` param for one call. When those objects use a pattern, the journal records the choice, and a recorded action plays back with it. Partly selected paths still move only their selected anchors. Docs: docs/mcp.md (transformPatternTiles), docs/target-app-parity.md. |
||
|
|
307a5a6298 |
Layers: art moved into a hidden or locked layer, or hidden by a data set, is deselected
These commands moved selected objects into a hidden or locked layer, or hid them, and left them selected, with the key object still set when it was one of them: - a row dragged into a hidden layer or group (layer.move); - node.move into a hidden or locked layer or group; - Merge Selected into a hidden or locked layer; - Flatten Artwork into a locked layer; - Collect in New Layer, when the topmost row it collects is in a hidden or locked layer; - applying a data set that hides bound objects (dataset.select, dataset.next, dataset.prev). They now call Selection::deselect_uneditable after the change, as layer.setProps, layer.hideOthers and layer.lockOthers do. Every selected object left hidden or locked is deselected, and the key object is cleared when it is one of them. Undo restores the objects and selects them again with the key object. A row drag into a hidden layer and node.move into a hidden or locked layer still succeed. The params text of layer.move, node.move, layer.merge, layer.flatten, layer.collectInNew and dataset.select describes the deselection. The Layers panel section of docs/mcp.md said that node.move has the same rules as layer.move; it now says that node.move also moves a row into a locked layer or group, which layer.move refuses. Docs: docs/mcp.md, docs/target-app-parity.md. |
||
|
|
1e76e389d2 |
Part of #868: PDFs saved with the reference app's editing data open from it, as .ai files do
A PDF saved with the reference app's editing data keeps that data in streams named AIPDFPrivateData1, AIPDFPrivateData2, ..., where a .ai file uses AIPrivateData1, AIPrivateData2, .... illustrator_data read only the .ai names, so such a PDF opened like any other PDF: plain groups opened ungrouped, and hidden objects and guides were missing. The import gave no warning that the PDF's editing data went unread. illustrator_data now reads the AIPDFPrivateData streams when the file has no AIPrivateData1, and the PDF opens from its editing data as a .ai file does. editingData: false still opens it like any other PDF. has_private_data still looks for AIPrivateData keys only. layered_ai also returns whether it read the layers. When it did, art reaching past a PDF's page opens whole, and through_editing_data drops the PDF importer's note that such art was clipped (PAST_PAGE_NOTE, now public) as it drops the off-artboard note. When it didn't, both notes stay. On main, a .ai whose editing data can't be decoded or has no layers opened from its PDF part without the off-artboard note. testkit::ai::pdf writes the .ai file of testkit::ai::ai with the PDF key names. The editing-data tests open it as a PDF, and the import fuzz test opens the damaged sample as a PDF too. Docs: the document.open params, docs/mcp.md, docs/file-format-parity.md, docs/gaps.md (G9: the export direction of #868 remains), one evidence line in docs/target-app-parity.md, and #868 moved from layer structure to editing data in the File formats row of docs/target-app-parity.md and in docs/roadmap.md. |
||
|
|
be9d05b274 |
EPS: the PostScript reader reads many tiling patterns, spot inks and shadings in linear time
Setting a tiling pattern went through every pattern made so far to find one with the same PaintProc, cell and color, and went through the document's patterns again to name a new one. Each spot ink was looked up in the list of every ink used. When the reading ended, each ink and each pattern became a swatch after a search of all the swatches, and the second object of each clipping group of two was looked up in the list of every shading painted. For a file that makes n of any of these, the searches did work that grows with n squared: a dev-profile build of vectorcraft-cli info took 99 user CPU seconds on a file that makes 160,000 tiling patterns, 35 on one that uses 160,000 spot inks, and 3.2 on one that paints 160,000 shadings through clips. The patterns are now kept in a hash map, under a hash of the PaintProc's storage, the cell, and the color, tint and swatch that an uncolored pattern is set in. A lookup compares the patterns under the same hash, with the comparison it made before. The inks are kept in a map by name, and the shadings are kept in a set. The swatch names go in a set while the swatches are made. The reader's document gets its patterns only from tiling, so the name search always ended at Pattern n+1 after n patterns, and that name is now made directly. A pattern whose cell has a NaN corner never compared equal to another, and it is no longer kept for lookups. The three files now read in 1.3, 0.6 and 0.9 user CPU seconds, and the documents read are the same as before. Tests: a program that makes 200,000 tiling patterns and uses 200,001 spot inks reads within 20 seconds, with the patterns named in order; a pattern made again from its prototype, and an uncolored pattern set again in a color it was set in, are the same pattern. |
||
|
|
76ca890bb4 |
Path: Simplify and Add Anchor Points record no undo step when they change no path
Simplify and Add Anchor Points edited the document whenever the
selection held a path, so a call that changed no path still recorded
an undo step, emptied Redo, marked the document modified and turned
the selected live shapes into plain paths. OK in the Simplify dialog
did the same. Simplify at its default options leaves a rectangle, a
star, a line and a path with one anchor as they are, and Add Anchor
Points leaves a path with one anchor as it is.
Both commands go through edit_paths, which now computes the new paths
before it edits the document. When no path changes, the command
returns without editing the document, and the reply
({ids, before, after}) has the same form and counts as before. When
some paths change, the command replaces only those paths and records
one undo step, so a path that Simplify leaves as it is keeps its live
shape.
One line in the evidence list of docs/target-app-parity.md names the
commands.
|
||
|
|
8b6bfe6ed7 |
Transparency: opacity mask options that change nothing record no undo step
Disable, Enable, Unlink and Link Opacity Mask and Opacity Mask Options (transparency.setOpacityMask) edited the document whenever a target had an opacity mask, even when every mask already had the flags the call set. Such a call recorded an undo step, emptied Redo and marked the document modified. When every mask a call acts on has those flags already, the call now returns without editing the document, whether it acts on the selection, on id or ids, or on the object whose mask is being edited. The reply is still null, and the journal still records the call. A call that changes a flag on any of the masks is still one undo step. One line in the evidence list of docs/target-app-parity.md names the commands. |
||
|
|
ae27cf6c2b |
EPS: loops under a long dash array no longer hang the import
setdash took a dash array of any length, and the graphics state kept a copy of it. gsave, gstate, currentgstate and setgstate copied the whole pattern with the state, currentdash made an array of it, and each stroke and strokepath copied and scanned it. Each of these counted as one operation against the operation budget. Under a pattern of 1,000,000 entries, a pass of gsave grestore took 0.38 ms of user CPU time, and a pass of strokepath on a line took 2.05 ms. Loops of the two would reach the budget after about 1.6 and 2.8 hours. setdash now raises limitcheck for a pattern of more than 65,535 entries, the most elements an array has in a typical PostScript implementation (PostScript Language Reference, third edition, Table B.1). Saved and copied graphics states share the pattern. setdash and each stroke count each entry they read or copy as one operation, and currentdash counts the array it makes against the memory budget. Under a pattern of 65,535 entries, loops of setdash, currentdash, stroke and strokepath now run out of the operation budget, or the memory budget for currentdash, after 0.08 to 0.31 seconds of user CPU time. Tests: a pattern of 65,535 entries is set, and a pattern of 65,536 entries stops the reading with limitcheck. Under a pattern of 65,535 entries, 4,000,000 passes of gsave grestore and 2,000,000 passes of gstate currentgstate setgstate finish, and loops of setdash, currentdash and strokepath run out of a budget. The test of these loops reads each program on a thread of its own and fails when no result arrives within 20 seconds. |
||
|
|
9cbab1383b |
Paint: fill, stroke, gradient and Appearance panel commands that change nothing record no undo step
These commands edited the document even when nothing changed, so the call recorded an undo step, emptied Redo and marked the document modified: - Fill, Stroke, None, Apply Last Color and Apply Last Gradient with the paint the objects have already, also on characters selected with the Type tool. - stroke.set and stroke.setAdvanced with the options the strokes have already. - paint.editGradient, paint.setGradientGeom and the freeform point commands when the gradient comes out as it was. - Invert and Complement on the Appearance panel's selected row when no color changes. - Swap Fill and Stroke and Default Fill and Stroke when every object comes out as it was. - appearance.setItem with the values the item has or an index no object has, and transparency.set with an item and the values that item has; appearance.removeItem with indices past every stack; appearance.moveItem, Reduce to Basic Appearance, Clear Appearance, the Eyedropper (appearance.copyFrom) and appearance.transfer when every object comes out as it was. These commands now return without editing the document. The replies have the same form as before. On a graph's series or labels, a fill or stroke that the art has already still records one undo step when the graph does not keep that paint for them (for example after the Eyedropper painted the art), and the graph then keeps it. The Eyedropper test whose second copy changed nothing gives its target a green fill first, which the copy turns red again. One line in the evidence list of docs/target-app-parity.md names these commands, apart from Invert and Complement. |
||
|
|
d0bb09c16f |
EPS: huge arcs, fine dashes and rectangle arrays in a loop no longer hang the import
The operation budget counted each operator once, however long a path it made or read. arc and arct with a huge radius make up to about a million curve segments, strokepath makes a piece for each dash, and rectfill makes five path elements for each rectangle in an array. pathbbox, clip, clippath, shfill, pathforall, gstate, currentgstate and setgstate read or copy the whole current path or clip path, and in an encoded user path a byte of the operator string can repeat the next operator up to 223 times. In a loop, each pass counted a few operations. vectorcraft-cli info was still reading each of 15 files that run one of these in "loop" when "timeout 300" stopped it, after 28 to 128 seconds of user CPU time. With a radius of 50 instead of 2.9e30, the loop of arc ran out of the budget after 6.9 seconds of user CPU time. Each of these operators now counts the path elements it makes, reads or copies as one operation each. Making or reading an element took up to about as long as an operation in a loop of pop, and copying one took less. strokepath also counts each entry of the dash pattern in each period of the path. An encoded user path counts each entry of its data array, each byte of its operator string and each operator it runs. The 15 loops now end with "the program runs too long" after 0.06 to 2.35 seconds of user CPU time, and the loop of arc with a radius of 50 ends after 1.9 seconds of user CPU time. Tests: a loop of each of these operators runs out of the operation budget. The loops cover arc and arct with a huge radius, strokepath with a fine dash and with dashes of length zero, rectfill with 2,000 rectangles, the eight operators that read or copy a path of 10,001 elements, and uappend on three encoded user paths. The tests read each program on a thread of its own and fail when no result arrives within 20 seconds. |
||
|
|
4a27d897ed |
EPS: shading functions in shared arrays no longer hang the import
A shading's Function can be an array of functions. eval evaluated every
entry of an array and the arrays inside it, with only a depth limit. An
array of 100 entries that all refer to the same array one level down,
nested that way a few levels deep, is small in memory, but each
evaluation visited about 100^levels functions, and a gradient samples
such a function 33 times. vectorcraft-cli info on a file with four
levels of 100 shared entries had used 90 CPU seconds, with a 12 MiB
resident set, when "timeout 120" stopped it. eval also counted nothing
against the operation budget: a loop that paints an axial or radial
shading counted a few operations a pass, however many functions each
pass evaluated. On a file that paints a gradient of one Type 2 function
with N 2 in "{ sh shfill } loop", vectorcraft-cli info ran for
209 CPU seconds, and its resident set reached 4 GB before the program
ran out of the operation budget.
eval now counts the functions one evaluation visits, as linear_points
counts its nodes, and raises limitcheck past 257. The PostScript
Language Reference defines a shading's function as one function
dictionary or an array of one for each color component. The reader
allows 32 components, and the depth limit allows each entry a chain of
8 functions. An array with more entries than the visits left raises
limitcheck before it is copied. Each function visited counts 8
operations against the budget: one visit took as long as 6 to 10
operations, measured with loops of pop and of a name lookup.
Tests: a shading over four levels of 100 shared entries raises
limitcheck, and the square drawn before it stays; a shading over one
array of 256 entries is drawn as a gradient, and one over 257 entries
raises limitcheck; a program that paints 2000 shadings over the
256-entry array runs out of the operation budget. The test reads each
program but the last on a thread of its own and fails when no result
arrives within 20 seconds.
|
||
|
|
d3be78000e |
Select: Select All skips hidden isolated groups, and menu commands stay within what it selects
Select All, All on Active Artboard and Inverse select from selectable: the visible, unlocked objects on visible, unlocked, non-template layers and sublayers, or in isolation mode the visible, unlocked objects in the isolated group or layer and in its visible, unlocked, non-template sublayers. Other Select menu commands did not use it: - Clipping Masks, All Text Objects, Stray Points and Open Paths selected hidden and locked objects and the objects in hidden or locked groups and sublayers. - Point Text Objects, Area Text Objects, Brush Strokes, Bristle Brush Strokes and the Same commands for symbols, fonts and text colors checked an object's own flags and its layer's, but not those of the groups and sublayers between them. - Next Object Above and Below selected the object beside the selected one even when it was hidden, locked or a sublayer. - A saved selection picked from the Select menu selected again the objects hidden or locked since it was saved. - Direction Handles direct-selected the hidden and locked paths in a selected group. - In isolation mode, the commands in the first two bullets, the other Same commands, All on Same Layers and a recalled saved selection selected art outside the isolated group or layer. Next Object Above and Below and Direction Handles did so only when the selection already held something that was not inside the isolated group or layer. In isolation mode selectable checked the flags of the isolated group's or layer's objects and sublayers, but not those of the group or layer itself or of the groups and layers it is in. Select All, All on Active Artboard and Inverse still selected the isolated group's or layer's objects after it, or a group or layer it is in, was hidden or locked. In isolation mode selectable now returns nothing while the isolated group or layer, or a group or layer it is in, is hidden, locked or a template layer. walk_selectable visits the objects selectable returns and the objects inside them, leaving out hidden and locked objects with what they hold. It visits nothing in a hidden, locked or template layer, sublayer or group. The commands in the bullets above now select from it. All on Same Layers takes the objects on the selected objects' layers that selectable returns. Next Object Above and Below pass over the objects walk_selectable leaves out, and leave the selection as it is when none is left in that direction. The params texts of Direction Handles and Recall Selection, docs/mcp.md (Object selection, Saved selections) and one line in the evidence list of docs/target-app-parity.md describe the change. |
||
|
|
a24269eb15 |
Type: Change Case, Smart Punctuation and other commands that change nothing record no undo step
These commands edited the document even when nothing changed, so the call recorded an undo step, emptied Redo and marked the document modified: - Change Case to the case the text has, Smart Punctuation on type with nothing left to change or on a selection with no type in it, and Find and Replace when every match is replaced with the text it is. All three rewrite the text runs through edit_runs, which now edits only when a run changes. - Convert To Area Type with no point type among the objects, and Convert To Point Type with no area type among them. - Type on a Path Options given the values the type has, without Flip: OK in the dialog with nothing changed, or a Type on a Path effect the type has. - Fill with Placeholder Text on type that holds its placeholder text already. - type.insert with an empty text on type that ends with a run of text. These commands now return without editing the document. The replies have the same form as before. A call that changes something still records one undo step. One line in the evidence list of docs/target-app-parity.md names every command above except type.insert. |
||
|
|
db8b9bb04c |
EPS: closepath no longer grows the path past its limit
moveto, lineto and curveto check that the current path holds fewer than 1,048,576 elements before they add one, and they stop the reading with "a path has too many points" when it is full. closepath added its element without that check. In an encoded user path, a byte of 255 runs the next operator 223 times. vectorcraft-cli info on a 622-byte file with one uappend that runs closepath 11,150,000 times peaked at a memory footprint of 601.9 MiB and reported "the file draws nothing". The string operator makes strings of up to 4 MiB, and one uappend of an operator string that long would add about 468 million elements of 56 bytes, about 24 GiB. closepath now makes the same check before it adds its element, in the closepath operator and in the ClosePath arm of append_user. On the file above, the reading now stops with "a path has too many points", and vectorcraft-cli info peaks at 62.0 MiB. A path below the limit closes as before. Tests: a moveto followed by 1,100,000 closepaths, and a uappend that runs closepath 1,059,250 times, are each read between two squares. Each reading stops with "a path has too many points" and keeps only the first square. |
||
|
|
f8f40e7902 |
Artboards: the Artboard tool starts on the active artboard
Each tool switch builds a new tool, and a new Artboard tool started on Artboard 1 whatever artboard was active. With Artboard 2 active, after the tool was chosen with Shift+O, its toolbar button, Edit Artboards in Properties or Document Setup, or the Artboards panel menu's Artboard Options…, the canvas outlined Artboard 1, and Delete, Cut, Copy, the Transform panel and Artboard Options acted on Artboard 1. The first pointer event over the canvas, a hover included, then made Artboard 1 the active artboard for Align to Artboard and Paste in Place too. VectorcraftApp::select_tool now starts a newly chosen Artboard tool on the active artboard, as the Artboards panel row's Options button does. Choosing the tool again while it is in use leaves it as it was, and the artboards Shift-clicked into its selection stay selected. The test makes Artboard 2 active, chooses the tool with tool.select, and checks the tool's active option, Artboard Options, a hover over Artboard 1 and Delete. It then presses Shift+O with two artboards selected and checks that both stay selected. Docs: docs/mcp.md (tool.select), docs/target-app-parity.md. |
||
|
|
b3f832cbcd |
Links: Find in Folder… looks for missing linked files in a folder and its subfolders
Find in Folder… in Linked File Not Found and in the Links panel menu
looks in a folder the user picks, and in its subfolders, for the files
with the names of missing linked files, in any letter case. In the
Links panel menu it looks for the selected linked files when there are
any. It uses the folder search of the Missing Fonts dialog. Find
Linked Files lists each file it looks for with the files found for it.
A file with the size and contents saved with the link comes first. A
file of the saved size comes next when the link saved no hash or the
file is larger than 1 GB, and any other file of the name comes last.
Within each group, a file named in the same letter case comes first.
Then each file's folder names are compared with the saved path's, from
the innermost outward, up to the first one that differs, and a file
with more matching names comes before one with fewer. A file is chosen
when it is the only file found, or the only one with the saved
contents, and the user can choose another file or Don't Relink.
Relink relinks each file chosen, and each relink is one undo step.
With Copy into the document's folder, which shows for a saved
document, each image's file is copied next to the document first. The
file's name, then Name 2.png, Name 3.png and so on, are tried in turn
until one is free, which gets the copy, or is a file with the same
contents, which is used. A placed document is relinked where its file
is. The status line names the first file that fails to relink, and the
other files still relink. When Find Linked Files opens from Linked
File Not Found, the missing files left without a choice, and any that
fail to relink, go back to that dialog. On the web, Find in Folder… is
disabled in the Links panel menu, and links.findFiles can't start a
search.
- links.findFiles searches on separate threads and returns at once;
links.findFiles {} reports the search, and {stop: true} stops it. It
looks for at most 1,000 linked files at once: the active document's
missing ones, or those of the objects it is given. It hashes a file
only when a link of its name saved the file's size and a hash, and
only when the file is 1 GB or smaller. It ends once every linked
file has its same file, and it searches only the folders
text.findFontFiles may search, with the same maxSeconds, depth and
count limits.
- links.relink {copy} copies the file it is given into the saved
document's folder and links images to the copy; a call that includes
a placed document fails. Each name it writes is checked against the
automation write root. text.addFontFiles and links.relink {copy}
make their copies with fileio::copy_new, which never replaces a
file. The text.addFontFiles params text and docs/mcp.md say which
names are tried for a copy.
- vectorcraft-doc adds links::hash_reader, which hashes what a reader
reads as hash_bytes hashes bytes.
- ui.findLinksInFolder is the Links panel's Find in Folder… for agents;
docs/control-protocol.md lists the dialog's fields.
Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md.
|
||
|
|
fd2ab03cd9 |
Layers: Send to Current Layer refuses a hidden or locked current layer
A click on a Layers row runs layer.setCurrent, which makes the row's layer current whether it is hidden or locked and keeps the selection. Object › Arrange › Send to Current Layer then moved the selected objects into that layer, or into a sublayer of a hidden or locked layer, and left them selected. In that case drawing, typing and pasting put new art on the topmost layer that is shown and unlocked, and since #1039 they fail with "the target layer is locked or hidden" when no layer is shown and unlocked. Send to Current Layer now fails with that error when the current layer is hidden or locked, or is inside a hidden or locked layer. It makes the check that DocState::target_parent makes for new art (Document::is_editable) before it moves anything or records an undo step. The objects stay where they are and stay selected. The message already has a row in the cs, de, es, fr, it, ja, pt-br, ru and uk catalogs. Docs: docs/target-app-parity.md. |
||
|
|
ac8d7ebce8 |
Transform: Transform Again repeats the options of the transform it repeats
Transform Again kept only the last transform's matrix and its Copy flag. After a Selection tool handle drag resized area type, Transform Again scaled the type instead of resizing the type area again. After Move, Rotate, Scale, Reflect or Shear with Transform Patterns set differently from General › Transform Pattern Tiles, Transform Again followed the preference, and after a scale whose strokes or corners param differed from Scale Strokes & Effects or Scale Corners, it followed those preferences. A recorded Transform Again played back with the preferences in effect at playback, although its step records the strokes, corners and patterns values it used. last_transform now keeps the options with the matrix, as last_perspective keeps the perspective.transform params. apply_transform stores the copy, typeAreas and patterns values a transform command used, and strokes and corners when it scales. commit_interaction stores the params of a drag's last object.transform preview with the values that preview noted. object.transformAgain passes the options to the transform it repeats, and a true or false value given in its own params under one of those names takes precedence. A recorded step plays back with the values it records. Three engine tests fail without the change: Transform Again after a handle drag on area type, Transform Again after a Move with patterns: false while Transform Pattern Tiles is on and after a Scale with strokes: true while Scale Strokes & Effects is off, and a recorded Transform Again played back with Transform Pattern Tiles on. Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md. |
||
|
|
9ba45990ec |
Part of #394: new and imported documents use the Gridline every and Subdivisions preferences
Preferences › Guides & Grid › Gridline every and Subdivisions set the grid of the documents that were open when either value changed, and of no other document. A document made with File › New, or opened from a file VectorCraft reads no grid from (SVG, PDF, EPS, .ai and the other imported formats), got a 72 pt grid with 8 subdivisions, whatever the preferences were set to. After a restart the saved values applied to no document, because the desktop app restores the preferences at launch, before any document is open. file.new now gives a new document the preferences' grid, and document.open, file.newFromTemplate and file.revert give it to a document read from such a file. A native file, and the native document that an SVG, PDF, .ai or EPS file carries, keep the grid they were saved with. Changing either preference still sets the grid of every open document. Docs: docs/mcp.md (gridlineEvery, gridSubdivisions), docs/target-app-parity.md. |
||
|
|
f769f4ea5d |
Tests: the envelope export test ignores the PDF's XMP dates and file ID
`enveloped_type_exports_distorted_in_every_format` exports each format twice and compares the two files without the lines that contain `CreationDate` or `ModDate`. Two other lines of the PDF also change with the time it was written. The XMP packet is one line that holds `xmp:CreateDate`, `xmp:ModifyDate` and the file ID, and the trailer holds the file ID as `/ID`. The file ID is a hash that krilla computes over the bytes written before it, which include the dates. When the two PDF exports were written in different seconds, these two lines differed, and the test failed at "pdf exports are repeatable". The comparison now also drops the lines that contain `xmp:CreateDate`, `xmp:ModifyDate` or `/ID[`. A new check writes the PDF that `document.export` writes, with its creation time set to Unix seconds 0 and then 1, and asserts that the comparison's filter leaves the two files equal. |
||
|
|
d875efd6e1 |
Tests: the command sweeps and import fuzz tests fail when a command panics
The engine's guard turns a panic inside a command into EngineError::Internal and rolls the document back. The panic checks in command_sweep.rs counted that error like any other error. They include probe, which the empty-params, junk-params and structured-junk sweeps call. The tests in import_fuzz.rs that open, place or paste a file through a command counted it the same way. None of them reported the panic. Each of these checks now passes the command's result through no_panic, which the library file tests in import_fuzz.rs already use, and the change adds a copy of it to command_sweep.rs. A case whose command panics now fails. The panic from no_panic reads "<command> panicked: <message>", and the checks that print a panic's message show it. Each check handles any other error as before. |
||
|
|
58af33aa1d |
Palette: the entry that cancels the key object reads Cancel Key Object
The command palette listed select.key as Set Key Object. A pick runs the command without an id, which cancels the key object (#541). select.key is now labeled Cancel Key Object, the label of the Align panel menu item that also cancels the key object. The palette lists it once, and engine.commands reports the new label. The params text and docs/mcp.md say that select.key {id} makes id the key object, and that select.key {}, or an id that names no selected object, cancels it. The Set Key Object rows are removed from the 10 catalogs that had one. Docs: the select.key params text, docs/mcp.md, docs/target-app-parity.md. # Conflicts: # crates/engine/src/cmd/select.rs # docs/mcp.md # docs/target-app-parity.md |
||
|
|
845aa6848f |
MCP: select.key checks its id, and agents can read the key object
Agents could set and clear the key object but not read it: document.inspect, the control channel's ui.pointer reply and the headless pointer_gesture reply left it out, and select.key returned null. select.key with an id that was not selected, or with a value that is not an id, such as "x", cleared the key without an error. A compound-path member or an object inside a selected group could become the key. select.key now checks its id as select.set, select.add and select.toggle do (#784). It returns an error for an id that is not a selected top-level object and leaves the key as it was. Layers, compound-path members and objects inside another selected object are not top-level objects. select.key {id: null} used to clear the key and is now an error; select.key {} still clears it. select.key returns {key}, and document.inspect and both pointer replies include the key object as "key". In isolation mode a Selection-tool click can pick an object that select.key does not accept as the key: a sublayer inside an isolated layer, a member inside an isolated compound path, or an object in an isolated group that is also selected. The tool no longer sends select.key {id} for such an object. When the object is already the key, the tool still sends select.key {}, which clears the key. A recorded action whose select.key step names an object that is not a selected top-level object when the action plays now fails and is rolled back, as an action with a select.set step naming a deleted object has failed since #784. Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md. |
||
|
|
208195765c |
Align: Align to Key Object makes the topmost selected object the key
With two or more objects selected and no key object, a click on Align to Key Object in the Align panel stored Key as the Align To choice and set no key. The align buttons of the Align panel, the Control bar and the Properties panel then ran object.align with to: "key", which fails with "no key object". Distribute Spacing sent the panel's spacing value and kept the leftmost or top object in place. The stored Key stayed until Align to Selection, Align to Artboard or Cancel Key Object was chosen. The button now runs select.key on the topmost selected object, the last in paint order, whatever order the objects were selected in, and the canvas draws that object with the key-object outline. The align buttons in all three places and Distribute Spacing then use that key, and Cancel Key Object clears it. A key that is already set stays the key. With fewer than two objects selected the button is disabled, as the Distribute buttons are. The button stores no choice, so once the key is cleared by Cancel Key Object, by a click on the key object or by another selection, Align To shows the stored Selection or Artboard choice again. Cancel Key Object no longer sets that choice to Selection. Docs: docs/target-app-parity.md. |
||
|
|
138d9499a7 |
Shape Builder: a line's loose end past a crossing no longer runs out and back in most cases
Since #1035 moved linesweeper to 0.5, the sweep can cut the paths through one crossing at points more than its tolerance (eps) apart. The planar map merged a point only into a vertex within eps of it, so the two passes of an open path could end on two different vertices, and it kept them as two edges with different ends. Past the crossing, shape_builder then returned a line's loose end as one piece that runs out to the line's end and back, and an extra piece of the line it crosses, about 1.4e-6 long. In live_paint, the face around the loose end ran out along it and back, and FaceMerger::merge of the faces returned a second subpath along it. pieces_cut_out_of_their_inputs fails on main for such an input. The map now joins the ends of pieces whose middles and ends lie within 16 eps of each other. The two passes then share their ends and become one edge. The new test is the failing input proptest saved for the property. Lines that slope close to -1/32 in the document are nearly level in the sweep's frame, and the change does not cover all of them. |
||
|
|
b1f3c819b4 |
Part of #394: art moved or duplicated with an artboard follows Transform Pattern Tiles
artboard.move with moveArt (the Artboard tool's drag of one artboard or several and, with copy, its Alt-drag), artboard.duplicate with art and artboard.rearrange with moveArtwork moved the art, or its copies, without their pattern tiles, whatever General › Transform Pattern Tiles was set to. They now follow the preference as the transforms and artboard.setProps do: with it on, pattern fills and strokes move with the art. Each also takes a `patterns` param for one call. When the art uses a pattern, the journal records the choice, and a recorded action plays back with it. Docs: docs/mcp.md (transformPatternTiles, artboard copies), docs/target-app-parity.md. # Conflicts: # crates/engine/src/cmd/menucmds.rs |
||
|
|
cf583304d1 |
Object: Lock, Transform Each, Perspective and other commands that change nothing record no undo step
These commands edited the document even when nothing changed, so the call recorded an undo step, emptied Redo and marked the document modified: - All Artwork Above and Other Layers (Object › Lock and Object › Hide) with nothing to lock or hide: no object above the selection, no other layer, or every one already locked (Lock) or hidden (Hide). - Transform Each with no object to transform, and without Copy when both scales are 100% and there is no move, rotation or reflection. - Rearrange All Artboards when no artboard moves, or with no artboard. - stroke.widthPoint.set and stroke.widthPoint.copy when the profile comes out as it was, and stroke.widthProfile.set with the profile the strokes have already. - Liquify when the stroke reshapes no path, and Puppet Warp given the pins the art holds already (rest: true), or without rest when every pin stays where it is and turns the art by 0° or is free. - Define Perspective Grid with the grid the document has, Attach to Active Plane with no object, Release with Perspective on art that is not in perspective, Move Plane to where the plane is, without copies, and Move Plane to Match Object when the plane is there and active already. - Move, Nudge and Transform in Perspective without Copy when no object moves (1e-9 pt or less along the plane or its normal, Snap to Grid included, or the identity), and Transform Again after one. These commands now return without editing the document. The replies have the same form as before. Transform Each with Copy and no object to transform no longer empties the selection. Puppet Warp without rest, with every pin where it is and turning the art by 0° or free, no longer turns a live shape into a plain path. All Artwork Above still deselects selected objects that are hidden or locked. Width Point counts a change of 1e-9 or less in a point's place or width factor (its width over half the stroke weight) as none. OK in Width Point Edit with the widths it shows, with or without Adjust Adjoining Width Points, records no undo step. After a move in perspective that moved nothing, Transform Again repeats the identity. One line in the evidence list of docs/target-app-parity.md names the commands. |
||
|
|
d79aa607cc |
Lint: clippy -D warnings passes for the wasm32 web build
On main, cargo xtask wasm printed two dead-code warnings in
vectorcraft-ui-egui, and this clippy run reported the same two as
errors:
cargo clippy --target wasm32-unknown-unknown -p vectorcraft-web -- -D warnings
ask_update_changed (dialogs/missing_links.rs) is called only from
tick_link_updates, and is_c_locale (i18n/mod.rs) is called only from
detect_system_lang. Both callers are compiled only for native targets.
Each function now has the same #[cfg(not(target_arch = "wasm32"))]
attribute as its caller. Native builds compile both functions as
before.
|
||
|
|
2b93e8a930 |
Swatches: swatch.library.save counts only the swatches the file holds
swatch.library.save replied with the number of solid colors and gradients it was asked to save, including the gradients the format leaves out. For a new document saved as .ase, count was 50 and the file held 46 colors. The .gpl and .ase writers keep solid colors only, and the CSS writer leaves freeform gradients out. count is now the number of swatches written. The new PaletteFormat::holds returns whether a file of a format keeps a swatch with a given paint, and swatch.library.save counts the swatches for which it returns true. The reply has the same fields as before. Tests: after a freeform gradient is added to a new document's swatches, the test checks that count has these values: 51 for .vcswatches, 46 for .gpl and .ase, and 50 for CSS. Each file holds that many swatches. Docs: docs/mcp.md. |
||
|
|
cd34e4097a |
Object: Lock and Hide › All Artwork Above deselect the objects they lock or hide
Object › Lock › All Artwork Above and Object › Hide › All Artwork Above lock or hide the objects that are above the selection in its layer and overlap it. Such an object can be a group or a sublayer that holds another selected object. The commands left that object selected, the key object included. Align then moved the locked object, or aligned the other selected objects to the hidden key object. object.lock and object.hide clear the selection, and layer.setProps, which the Layers panel's eye and lock columns run, deselects what it hides or locks. Both commands now deselect every selected object that is locked or hidden after the command, as layer.setProps does, and clear the key object when it is one of them. Undo selects the objects again and restores the key object. Docs: the object.lock.above and object.hide.above params text, docs/target-app-parity.md. |
||
|
|
a31cdab606 |
Part of #394: Align and Distribute move pattern tiles when Transform Pattern Tiles is on
object.align, object.distribute and object.distributeSpacing moved objects without their pattern tiles, whatever General › Transform Pattern Tiles was set to. They now follow the preference as Move, Nudge and the Selection tool do: with it on, pattern fills and strokes move with the art. Each also takes a `patterns` param for one call, as the transforms do. When the art uses a pattern, the journal records the choice, and a recorded action plays back with it. Docs: docs/mcp.md (transformPatternTiles), docs/target-app-parity.md. |
||
|
|
975dea056c |
MCP: object.setProps removes the objects it hides or locks from the selection
object.setProps with visible: false or locked: true left the objects it hid or locked selected, the key object included, and kept selected objects inside a group or layer it hid or locked. Align then moved a locked object, and aligned the other objects to a hidden key object. object.lock and object.hide clear the selection, and layer.setProps, which the Layers panel's eye and lock columns run, deselects what it hides or locks. After a call with visible: false or locked: true, object.setProps now deselects every selected object that is then hidden or locked, as layer.setProps does: the selected objects it hides or locks, selected objects inside them, and selected objects that were hidden or locked before the call. It clears the key object when the key is one of them. Undo selects them again and restores the key object. A call with visible: false or locked: true that changes nothing records no undo step and still deselects them, as object.lock and object.hide do. A call that sets neither, such as an opacity change from the Control bar or the Transparency panel, leaves the selection as it is. Docs: the object.setProps params text, docs/mcp.md, docs/target-app-parity.md. |
||
|
|
a2abe63e8d |
Align, Reflect and Shear: object.align needs a direction; unknown choices are errors
object.align with neither horizontal nor vertical returned success and moved nothing. The command palette, and a key bound to it in Keyboard Shortcuts, run it with no params. It now returns "give horizontal or vertical", as object.distribute already did. An action recorded while Align ran from the palette or a bound key contains an object.align step with no params. Playing that action now fails at that step, and command.batch rolls back the whole action. Values outside a parameter's choices were read as another choice or ignored. Align ignored an unknown horizontal or vertical and aligned to the selection for an unknown to value. Distribute read an unknown direction as center. Distribute Spacing and Shear read any axis other than "vertical" as horizontal, and Reflect read any axis other than "horizontal" or a number as vertical. A bounds or to value that was not a string fell back to the default, and a spacing that was not a number gave automatic spacing. The commands now return an error that names such a value, before they move anything or record an undo step. When a to, bounds, axis or spacing value is missing or null, the command uses the parameter's default: Distribute Spacing without an axis spaces the objects horizontally. The run_command description and docs/mcp.md showed object.align with an align param it does not have; they now use horizontal. The new status-bar messages have rows in the de, es, fr, it, ja, ru and uk catalogs. Docs: docs/mcp.md, docs/target-app-parity.md. |
||
|
|
8e4158b364 |
Align: Align to Artboard uses the active artboard
Align to Artboard aligned the selection to the artboard under its center, or to the first artboard when no artboard was under the center. A press with a selection tool inside an artboard makes that artboard the active one (#693). With the Selection tool, a Shift-click on an empty spot inside an artboard also makes it active and keeps the selection. Align to Artboard did not use the active artboard. object.align takes artboard, a 0-based artboard index, and uses it when its to param is "artboard". The app passes its active artboard, the one the Artboards panel highlights, to each object.align it runs that names none, as it does for Paste in Place, Paste in Front, Paste in Back (#693) and Select › All on Active Artboard (#1006). MCP calls in remote mode also get it. Without artboard (headless MCP and the CLI), or with an index the document doesn't have, object.align aligns to the artboard under the center of the selection, or to the first artboard when no artboard is under the center. command.batch takes artboard too, and the app passes it the active artboard. The batch passes it on to each step of these five commands that names none, when the step runs. When the document a step runs in has no artboard at that index (for example, after an earlier step deleted the active artboard when it was the last one), the step gets that document's last artboard. A step in a document that an earlier step opened, created or switched to gets the same index, not that document's own active artboard. With Align to Selection, one selected object aligned from the Align panel, the Control bar or the Properties panel is aligned to the active artboard too, unless it is the key object. Recorded actions leave out the artboard that the app passes to these commands and to the batch. The Actions panel plays an action as a batch. Its Align to Artboard, All on Active Artboard and in-place paste steps that name no artboard use the artboard that is active when it plays, and the default Center on Artboard action now centers on the active artboard. In-place paste steps recorded since #693 and before this change still paste onto the artboard that was active when they were recorded. Tests cover object.align with and without artboard on three artboards; a Shift-click that makes an artboard active, after which Align to Artboard aligns to that artboard but keeps an artboard the call names; a batch that passes its artboard to each step when the step runs, after a step deletes the last artboard and in a new document; recorded actions that leave the artboard out; and recorded Align to Artboard and Paste in Place steps and the default Center on Artboard action, which play on the artboard that is active when they play. Docs: docs/mcp.md (Align to Artboard), docs/target-app-parity.md. |
||
|
|
a8eb9024da |
Align: a key object needs two or more selected objects
Selecting two objects, clicking one to make it the key object and then Shift-clicking the other left the remaining object as the key. The canvas still drew that object with the thick key-object outline. The Align panel aligned the object to itself, so the object did not move. The key is now cleared after any change that leaves no other selected object outside it, such as a Shift-click, or hiding or deleting the other object's row in the Layers panel. On a single selected object, or on a group selected with only its own members, select.key sets no key. With one object selected and Align to Key Object chosen, the Align panel, the Control bar and the Properties panel ran object.align with to: "key", which fails with "no key object" when the selection has no key. All three now align a single selected object to the artboard whatever Align To is set to. Docs: docs/target-app-parity.md. |
||
|
|
05bc89827b |
Drops: libraries, presets and plug-ins open on a drop, as File › Open opens them
With a document open, the desktop app ran File › Place on a swatch or graphic style library, a Libraries panel file, a presets file or a plug-in dropped on it. A desktop drop has no position, and without one the app opened only documents and ran File › Place on every other file. On the web, the app also ran File › Place on these files when they were dropped on the canvas. File › Place can't read them, and the drop failed with "Couldn't place X: ... not a format VectorCraft reads". VectorcraftApp::drop_target now returns Open for these files, with or without a drop position, and they open as File › Open opens them: plug-ins are installed, presets are imported, swatch and graphic style libraries open in the library panel, and a Libraries panel file is added to the Libraries panel as a new library. A dropped library, presets file or plug-in that has a path is added to Open Recent Files when it opens, as File › Open adds it. io::opens_as returns the kind of each of these files from its extension, and drop_target and io::open_bytes both call it. The rules from #472 still apply to documents, pictures and text. On the web the browser gives no file name before the drop, so the app still outlines the canvas while such a file is dragged over it. Tests: drop_target returns Open for each of the eleven extensions, with no position and with a position on the canvas. A .gpl dropped through the desktop drop path loads in the library panel and is added to Open Recent Files, and a .vclibrary dropped the same way becomes a new library in the Libraries panel. Docs: docs/control-protocol.md (file.place), docs/development.md (Drag-and-drop), docs/target-app-parity.md (Evidence, UI). |
||
|
|
1819921e03 |
Align: a compound path or selected group that contains the key object stays in place
Align and Distribute Spacing move a selected compound path or group as one object, and they found the key object only when its own id was among the objects they move. When the key was inside a compound path or a selected group, aligning to the key moved that compound path or group like any other object, and the key moved with it. In that case, Distribute Spacing with a spacing held the first object still instead of the key. The key object can end up inside a selected group or a compound path. Dragging its row into a group in the Layers panel (layer.move) keeps the selection and the key. A Selection-tool Shift-click on that group, when it is unselected and directly in a layer, then adds it to the selection (select.toggle) and keeps the key. Other ways of adding the group keep the key as well: a Shift-click in the Layers panel's selection column or on the group's target circle, a Group Selection click that hits the key itself, select.add, and select.anchors or select.anchorsMany in add or toggle mode. In isolation mode, a Selection-tool click on one of several selected objects in the isolated group makes that object the key. In an isolated compound path, the same click on the outline of a selected member makes that member the key. Align to the key object and Distribute Spacing with a spacing now keep in place the selected object that is the key or contains it. Both commands look for that object among the key and its ancestors, so a group around the key's group also stays in place. Aligning to the key still lines the others up with the key's own bounds. Docs: docs/target-app-parity.md. |
||
|
|
2d95ac1694 |
EPS: arc and arcn with huge angles no longer hang the import
arc raised its second angle by 2π in a loop until it was no longer below the first, and arcn lowered it the same way. For an angle of 1e18 degrees that loop takes about 3e15 steps: vectorcraft-cli info was still reading an EPS file with "newpath 100 100 50 1e18 0 arc stroke" when "timeout 20" stopped it. For 1e308 the loop never ends: from 2^56 radians (about 4.1e18 degrees) up, adding 2π leaves a double unchanged. The import fuzz test eps_hostile_programs_never_panic builds programs from tokens that include arc, arcn, 1e308 and 1e15. arc now raises the second angle by multiples of 360 until it is at least the first, and arcn lowers it until it is at most the first, as the PostScript Language Reference defines them. The code works out both sweeps from the remainders of the two angles after whole turns, and it places the first endpoint at the remainder of the first angle. Ordinary angles give the same arcs as before, apart from rounding in the last digits. One case changes: when raising (arc) or lowering (arcn) the second angle by whole turns makes it equal to the first, as in 60 -300 arc or -60 300 arcn, rounding sometimes made the old loop add another turn and draw a full circle. Those arcs are now empty, as the PostScript Language Reference defines them. Tests: 1e18 0 arc, 1e308 0 arc, 0 1e18 arcn and 0 1e308 arcn draw the arc between the angles' remainders and leave the current point at the second angle. The test reads each program on a thread of its own and fails when no result arrives within 20 seconds. |
||
|
|
9ea392e6ee |
Tests: the command sweeps no longer skip junk scale, spacing and artboard values
The sweeps `every_command_with_junk_params` and
`fuzzed_calls_keep_documents_serializable` skipped every case that set
`object.scale`'s `sx`, `object.transformEach`'s `scaleH` or `scaleV`,
`object.distributeSpacing`'s `spacing`, or `artboard.setProps`'s `x`,
`y`, `width` or `height`. The comment above `KNOWN_BUGS`, the list of
those (command, param) pairs, referred to `#[ignore]`d repro tests, but
`bug_huge_transform_makes_document_unloadable` and
`bug_artboard_huge_props_break_svg` have no `#[ignore]` and run with the
other tests. Since
|
||
|
|
dc6e8a4253 |
Object: Align, Move, Arrange and other commands that change nothing record no undo step
These Object commands edited the document even when nothing changed, so the call recorded an undo step, emptied Redo and marked the document modified: - object.align, object.distributeSpacing and object.distribute (three or more objects) when no object moves. With one object selected, Object › Align aligns it to its own bounds, so it recorded a step every time. Calls on objects that were already aligned or evenly spaced also recorded a step, including objects that an earlier Align or Distribute left a rounding error away from the position the next call computes. - Move, Rotate, Scale, Shear, Transform and Nudge by an identity transform without a copy, Transform Again after one, Set Bounds with the current position and size, and a Selection tool drag that ends where it started. - Nudge by 0 with anchors direct-selected or with only ruler guides selected, and Transform with nothing selected or with ids that name no object, with or without a copy. - Bring to Front and Bring Forward on objects already in front, Send to Back and Send Backward on objects already at the back, and Send to Current Layer on objects already on top of the current layer. - Ungroup, Release Compound Path and Release Clipping Mask with none of their objects selected, and Expand Shape with no live shape selected. - Lock with every selected object already locked, Hide with every one already hidden, Unlock All with nothing locked and Show All with nothing hidden. - object.setProps with the values the objects already have, or with no object. - object.setLiveShape with the values the shape already has: a polygon's sides, angle, radius or equal sides, or a side length that gives exactly its current radius; an ellipse's pie, including Invert Pie on a whole ellipse; and a corner radius or kind that the corners already have. These commands now return without editing the document, and Align and Distribute count a move of 1e-9 pt or less on both axes as none. Such a call records no undo step, keeps the step to redo and leaves the document unmodified. In these cases Ungroup no longer resets the selection, and Release Compound Path, Unlock All and Show All no longer empty it. A Transform with a copy of no object keeps the selection. Lock and Hide still deselect the objects. A Nudge by 0 no longer expands a live shape whose anchors are direct-selected. The replies have the same form as before. An identity transform still sets what Transform Again repeats. The journal still records each call, so the Actions panel records it as before. When every value given to Set Bounds is the current one, it now applies the identity transform instead of the transform computed from those values, which can differ from the identity by a rounding error on a rotated object or with Use Preview Bounds. When a drag comes back to its start, the identity preview no longer edits the document. Session::preview now advances the revision when it undoes the previous preview, so views redraw that drag. The undo and redo row of docs/mcp.md now notes that many calls that change nothing record no undo step, so undo after one undoes the step before it. |
||
|
|
11cf254a07 |
Fonts: a folder search finds .suit suitcases and skips font files over 256 MB
Add Fonts adds only files a folder search found. - macOS declares .suit as the extension of a font suitcase (com.apple.font-suitcase), and the font scan reads a suitcase whatever its name. A search considered only suitcases without an extension. It now also considers files whose extension is .suit, in any letter case, and reads one only when it is a suitcase font. - Add Fonts copies at most 256 MB a file. A search also read larger font files, and a search that found one for a font could end before it found a smaller copy. A search now skips font files over 256 MB. Docs: docs/mcp.md. |
||
|
|
7b7e10e82e |
Fonts: a folder search skips suitcase fonts whose resource fork is over 8 MB
A folder search reads at most 8 MB of each font file. The suitcase reader reads a suitcase font's resource fork whole, up to 256 MB. Before this change, the search applied its limit to each font only after that read. One search thread could read 256 MB from a single suitcase and hold a copy of its fonts as well. The search now reads a suitcase font only when its resource fork is 8 MB or less. The font scan still reads each suitcase font's resource fork whole, up to 256 MB. Docs: docs/mcp.md. |
||
|
|
5709e636a5 |
Fonts: missing fonts are listed when a document opens, with Find Fonts and a folder search
When a document opens with fonts that aren't available, fonts used
only in its symbols included, VectorCraft lists them in a Missing
Fonts dialog after the missing linked file questions, one document at
a time, with Find Fonts and Close. Find Fonts opens Type › Find Font
on the first missing font of the layers' type, to replace it. After
Find Fonts or Close the dialog doesn't open again for that open; when
another dialog replaces it, it opens again once that dialog closes.
Search Fonts Folder (the Fonts folder File › Package writes next to
the document) and Find in Folder… search a folder and its subfolders
for the fonts' files, and Add Fonts copies the files the user chooses
into a Fonts folder next to the preferences. The desktop app and
vectorcraft-cli read that folder with the system's font folders. Type
› Find Font has a Find in Folder… button too when fonts the document
uses are missing. The web app shows no dialog; a status message says
how many fonts aren't available.
- text.missingFonts lists each missing family and style once,
whatever version the document's type names, and leaves out CSS
generic families such as sans-serif. text.findFontFiles searches a
folder on up to 8 threads and returns at once; text.findFontFiles {}
reports the search, and {stop: true} stops it. A search reads only
what the font scan reads of font files (their table directories and
name, fvar and OS/2 tables) and, on macOS, of suitcase fonts, and
lists a file for a font only when the file makes the font resolve
exactly. It does not enter app packages, the Program Files and
ProgramData folders, the system's folders at the root of a volume,
the Shared and Public folders in Users, a home folder's Library,
AppData, Applications, snap and dot folders, a Library folder's app
data folders or the folders the environment names for apps, and it
skips other hidden folders and does not follow links below the
picked folder. It ends after 60 s by default, after 10,000,000
entries listed, with more than 1,000,000 folders waiting, after
10,000 font files read or 1,000 kept, or once every font has a file;
Stop and Close end it too.
- text.addFontFiles copies only files the last search found. Of
those, it copies only regular .ttf, .otf, .ttc, .otc, .woff and
.woff2 files and suitcase fonts with their resource fork, at most
256 MB a file and 1 GB in all. It never replaces a file: a file with
the same contents is kept, and when another file has that name, the
copy gets a number (Name 2.otf). The fonts are then scanned again,
and type set in them redraws.
- ui.missingFontsDialog and ui.findFontsInFolder open the dialog for
agents; docs/control-protocol.md lists its fields.
- File › Package gives a font file whose name has no font extension
the extension that matches its data (.ttf, .otf or .ttc).
- vectorcraft-text adds set_app_font_dir, which system_font_dirs
reads, WantedFonts, which reads a font file's names as the font scan
does, is_font_file and is_suitcase. #762 split its face reader, which
now takes read budgets: file_face_names_within tries the suitcase
reader first, and reader_face_names_within applies the budgets to
each font. vectorcraft-format adds write_new_with.
Docs: docs/mcp.md, docs/control-protocol.md, ROADMAP.
|
||
|
|
975d4975ee |
Swatches: swatch.library.copyToUser copies a loaded library into User Defined
swatch.library.copyToUser {library} copies a library loaded from
another folder into the user library folder of the desktop app. A
.vcswatches, .gpl or .ase file is copied as it is; the swatches of a
document and a library loaded from the data or dataBase64 parameter are
written as a .vcswatches file. The copy gets a name that no file in the
folder has, and when a file with the same extension and bytes is
already there, that file is used. The library is then listed as User
Defined only. Until the app quits, commands given its loaded id use the
copy, unless another library is loaded under that id. Without a user
library folder (the web and headless sessions) the command is disabled.
A file with the same extension and bytes as a User Defined library
opens as that User Defined library. The unchanged original of a copied
.ase file is one such file. Graphic style libraries open the same way.
|
||
|
|
75f2145b92 |
Package: fonts found by any of their names are copied, and skipped fonts are named
File › Package and Document Info › Font Details looked fonts up with FontDb::face and compared the face's family with the name the document stores. A font a document named by another of its names (a PostScript or localized name, as SVG and PDF files do) was reported as not available and left out of the package, although the canvas drew it and exports embedded it. - Package and Font Details resolve fonts with FontDb::resolve, as text.fonts and the renderer do. A style the family lacks is still copied as its closest style; the report now names the style it copies, and Font Details lists the font as substituted, with that style's file and permission. - FontFace::embeddable reads fsType as exports do (Embedding), so fonts that allow bitmap embedding only (bit 9), or that set the restricted bit together with reserved bit 0 (0x0003), are no longer copied. Document Info's permission labels follow the same rule: when a font sets more than one usage bit, the label names the most permissive one. - The Package dialog and its status line name the fonts it didn't copy. |
||
|
|
4b00c2d968 |
Part of #579: macOS fonts registered from other folders are listed too
The font scan read only the macOS font folders and the Additional Fonts Folder, so fonts that apps and font managers register with CoreText from their own folders were missing from the font menus, and exports set such type in the fallback font. CoreText's font manager lists them with the installed fonts. - `mac_fonts.rs` lists the files of the fonts the font manager has available (`CTFontManagerCopyAvailableFontURLs`, through objc2-core-text), keeping only file URLs as `open_documents.rs` does (#433). The desktop app and `vectorcraft-cli` install it as they install the DirectWrite lister on Windows, and the scan reads the files outside the font folders in place. - The scan leaves out faces with no outlines the font engine draws (no glyf, CFF, CFF2 or VARC table), on every platform. CoreText lists Apple's PingFang UI interface font (macOS 27), whose outlines are in an hvgl table; if the scan cataloged it, PingFang SC would count as installed, and Chinese characters in fallback text, Japanese kanji included, would draw blank. - Tests: the list holds the installed fonts by full path, a font registered from another folder is listed while it is registered, and a face without outlines the engine draws isn't cataloged. |
||
|
|
a61d058f1b |
Swatches: Save Swatch Library writes swatch exchange (.ase) files
The Save Swatch Library dialog lists Swatch Exchange (.ase) as a format, and swatch.library.save takes format: "ase" or a path ending in .ase. Each solid color keeps its model (RGB, CMYK, Lab or Gray) and its type (spot, global or process), and color groups stay groups. Gradients and patterns are left out, and a tint swatch is written as the color it shows. Without a path or user: true, the command returns the file as dataBase64. A swatch exchange library opens under the name of its file. When Save Swatch Library or Save Graphic Style Library saves to a file, the save panel suggests the Name from the dialog as the file name, as a save to User Defined does, and the web app downloads the file under that name. A name made only of dots and spaces, such as "...", gives the file name Library (Library.vcswatches) in place of a hidden file. palette_io::write returns the file's bytes. A property test writes generated libraries and reads them back, and an import fuzz test loads mutated copies of a file the writer wrote. |
||
|
|
67af21c6db | Contributors: add dotCooCoo (Robert Lee) | ||
|
|
e0b1f55250 |
Swatches: swatch exchange (.ase) files open as swatch libraries
File › Open, Other Library… and the User Defined library folder read .ase files. Colors keep their model, global and spot colors stay global and spot, and color groups stay groups. The library readers take a file's bytes. On the web, File › Open passes a swatch library file to swatch.library.load as dataBase64 instead of as lossy UTF-8 text. Swatch and group names in a library are now unique, in .gpl files too. Add to Swatches puts each color group of a library into one document group when the document already has a swatch with the group's name. The library fuzz helpers fail when a command panics. |