Commit Graph
181 Commits
Author SHA1 Message Date
dotCooCoo 1be658c88c Variables: Export Data Sets refuses more data sets or variables than Import Data Sets reads
dataset.export checked only the 1 MiB size, so a document with more than
1000 data sets or 256 variables exported a CSV that dataset.import then
refused. Export now refuses such a document with a message naming the
limit, and the messages have rows in every complete-message catalog.
2026-11-02 12:03:37 +07:00
dotCooCoo adb446049b Variables: Undo and Redo clear a highlight that names no variable
Undo, Redo and a canceled undo group put back an earlier document but
left the Variables panel highlight as it was. After renaming the
highlighted variable and undoing, the highlight still named the new
name: a variable defined later under that name showed highlighted
without a click, and Delete removed it.

Each of these paths now clears a highlight whose variable the restored
document does not define, as Delete Variables and Load Variable Library
already did.
2026-11-02 12:03:36 +07:00
Brandon Thomas d3ef9a6aae Merge pull request #5183 from dotCooCoo/fix/replace-symbol-clears-overrides
Symbols: Replace Symbol clears the old symbol's dynamic overrides
2026-11-01 23:45:26 -05:00
dotCooCoo 589c56da22 PDF export: a white stop linked to a process swatch keeps a spot gradient on its ink
A gradient of one spot ink's tints and paper white is written as a
Separation shading in that ink, with each white stop at 0% of it. A
white stop linked to a swatch that is not a spot color (a global White
swatch, for example) did not count as paper white. In an RGB document
the spot ink was then dropped: a Classic gradient was written in the
stops' own colors, and a Perceptual one was exported as an image in
process colors.

Such a stop now counts as paper white in the Separation and DeviceN
paths and in the checks that pick them, as an unlinked white stop does.
A stop linked to a white spot color is still a stop of that ink.
2026-11-01 16:28:40 -08:00
dotCooCoo 2adf5a9a61 Symbols: Replace Symbol clears the old symbol's dynamic overrides
An override on a dynamic symbol instance names an object of its symbol's
art by path. Replace Symbol kept every override whose path the new
symbol's art also had, so a change made to one object of the old symbol
was applied to an unrelated object of the new one. An instance given a
different symbol now drops its overrides; an instance replaced with its
own symbol keeps them. Undo brings them back.
2026-11-01 16:24:33 -08:00
Brandon Thomas 639c9d6ce9 Merge pull request #5113 from dotCooCoo/feat/recolor-color-sliders
Recolor Artwork: color sliders in a chosen color mode edit the selected row's new color
2026-11-01 15:28:03 -05:00
Brandon Thomas fceaf44fd0 Merge pull request #5138 from dotCooCoo/fix/vars-p6
Variables: a text value on threaded type replaces the whole story
2026-11-01 15:27:58 -05:00
Brandon Thomas 33010204cb Merge pull request #5137 from dotCooCoo/fix/vars-p16
Variables: Lock Variables' description names the refused import
2026-11-01 15:27:53 -05:00
Brandon Thomas 2aa24c1f3a Merge pull request #5136 from dotCooCoo/fix/vars-p5
Variables: a graph variable's data set applies its CSV as graph.setData does
2026-11-01 15:27:48 -05:00
Brandon Thomas 7e5eebc3ed Merge pull request #5135 from dotCooCoo/fix/vars-p15
Variables: Rename reports a missing variable or data set before a taken name
2026-11-01 15:27:45 -05:00
dotCooCoo 39e69e8446 Tests: move the lock params text test next to the lock test 2026-11-01 11:48:41 -08:00
dotCooCoo b104efc53c Graph data: a dropped empty row adds no blank series
Dropping rows with neither a label nor a value read the data padded to
the widest row, so a dropped row wider than the rest (`,,`) left every
kept row one blank cell wider. The graph then had an extra blank series,
and a right-axis assignment to that index was kept. Each kept row now
keeps its own width. This applies to graph.setData and to a graph
variable's data set.
2026-11-01 10:19:45 -08:00
dotCooCoo 637229931f Paste: Paste in Back keeps a clip group's clipping path at the bottom
With the clipping path of a clip group or a clipping layer selected, or a
member of a compound path that is the clipping path, Paste in Back put the
clipboard's objects below it. The bottom pasted object then became the
clipping path. With nothing selected in a clipping layer, Paste in Back did
the same at the bottom of the layer.

Paste in Back now puts the objects right above the clipping path, as Send to
Back keeps art above it.
2026-11-01 10:18:06 -08:00
dotCooCoo be20929877 Variables: a text value on threaded type replaces the whole story
Applying a data set wrote a text value into the bound frame only, and the
thread's reflow then joined it with the other frames' old text. The art no
longer matched the row it had just applied, a captured row applied back lost
text, and a value bound to a later frame deleted the rest of the story.

- A text value bound to any frame of threaded text replaces the whole story,
  which then flows through the frames again. `overflow` names the story's
  last frame.
- Capture Data Set, Update Data Set and the Variables panel's match check
  read such a frame as the whole story, frames joined in thread order.
- When any frame of the story is locked, the text value is skipped and
  counted in `skipped`, and the story is not read.
- Two text variables bound in one story apply in definition order, as for
  two on one object: the one defined last wins.

Docs: docs/mcp.md.
2026-11-01 08:58:35 -08:00
dotCooCoo e49e102032 Variables: Lock Variables' description names the refused import
variable.lock's params text and the doc comment on Variables::locked said every
dataset command still runs while the variables are locked. dataset.import is
refused when its header would define a variable; both texts now say so, as
docs/mcp.md and dataset.import's params already do.
2026-11-01 08:43:56 -08:00
dotCooCoo 07282d95e0 Variables: a graph variable's data set applies its CSV as graph.setData does
Choosing a data set gave a graph variable's CSV to the graph without the
two steps graph.setData takes after new data: rows with neither a label
nor a value were kept and drawn as empty slots, and a right-axis series
the new data no longer had stayed assigned, so a series added by a later
row started on the right axis. The data set now goes through the same
steps.
2026-11-01 08:43:16 -08:00
dotCooCoo f98b086c5f Variables: Rename reports a missing variable or data set before a taken name
variable.rename and dataset.rename checked whether newName was taken before
checking that name exists, so renaming a missing name onto a taken one said
the new name already exists. Both now report the missing name first.
2026-11-01 08:23:40 -08:00
dotCooCoo 827e137b5c Paste: in front or back of a compound path's member, other art goes beside the compound path
With a member of a compound path selected, Paste in Front and Paste in Back
put the clipboard's objects next to that member, inside the compound path,
whatever they were. A pasted group, compound path or type object then sat
inside a compound path, which holds only paths. The model-based test
`random_command_sequences` stops on this with "compound #N has a non-path
child".

When the clipboard holds anything other than paths, the objects now go
right above or below the compound path. Paths still go next to the member,
inside the compound path.
2026-11-01 06:42:33 -08:00
dotCooCoo 9b853004d5 Recolor Artwork: a slider edit changes the color mode of its own row only
The color sliders now mark the row they edit with `model: "new"`
instead of a dialog-wide flag, and `recolor.apply` reads a row's own
`model` before the command's. Rows the sliders did not edit keep each
color's model, so with RGB art and a CMYK color group, a slider edit of
one row no longer turns the other rows' art CMYK. Another edit of the
row's new color, Reset and rows reduced again remove the mark, and
`recolor.randomize` moves it with its new color.
2026-11-01 02:06:46 -08:00
dotCooCoo f242b9caa6 Recolor Artwork: a row's tint stays with its spot color
A wheel drag or a harmony rule on the Edit tab now removes a row's tint
when it gives the row another new color, as the color sliders, the hex
field and the Color Picker already do. With Limit to Library, an edit
that snaps back to the row's own color keeps the tint.

recolor.randomize moves each row's tint with its new color when it
shuffles the order, and removes the tint from a color that saturation
and brightness changes.
2026-11-01 01:55:46 -07:00
dotCooCoo 46cb088eae Recolor Artwork: color sliders edit the selected row's new color in a chosen mode
The Assign tab gets a Color Mode menu (HSB, RGB, CMYK, Lab, and Tint when
the row's new color is a spot color of the Limit to Library library) and
its sliders under the rows. They edit the first selected row's new color
and are disabled with no row selected. Once they have edited a color in a
mode, the preview and OK send recolor.apply with model "new", so the art
takes the color in that mode.

recolor.apply rows take an optional tint (percent, default 100): art linked
to the row's global or spot library swatch takes that share of its tint.
recolor.randomize keeps a row's tint.
2026-10-31 21:42:42 -07:00
dotCooCoo 38bb213917 PDF export: a Perceptual gradient that mixes spot and process inks keeps its inks
A Perceptual linear or radial gradient that mixes spot colors with each
other or with CMYK or gray stops was written as an image in process
colors. It is now written as a DeviceN shading of its inks, with the
stops on its Perceptual curve: at each one, the inks of the two stops on
either side are mixed at the weight where the two colors mixed in
display RGB have the OKLab lightness of the color on the curve.

- Gradient::ink_mixes gives the curve's stops as weights between stops,
  under the same 4,096-stop limit as ink_tints.
- Perceptual gradients that mix a spot color with RGB or Lab stops,
  dithered fills and freeform gradients stay images, and the warning
  now names those cases.
2026-11-01 11:09:09 +07:00
dotCooCoo ff036557fb Symbols: Edit Symbol opens the symbol in its own editing mode, and every instance follows each edit
Squashed from #5064 onto main.
2026-11-01 11:09:03 +07:00
dotCooCoo 5716a996ab Type: Touch Type moves a character rotated near 90 degrees across its line
Baseline shift now moves a character in the line's frame, outside
Character Rotation, so a positive shift moves a character up from its
line at any rotation, and the character turns about its shifted pen
position. The Touch Type tool splits a drag into its part along the line
(kerning) and its part up from the line (baseline shift) at every
rotation; before, within about 6 degrees of a quarter turn it dropped
the cross-line part.

Characters that already combine a rotation with a baseline shift are
drawn in a new position. Their stored values do not change.

- text: glyph transform order, PositionedGlyph::pivot (the turning
  point), tab leaders in the same order
- tools: Touch Type splits drags against the line's up axis and measures
  rotate-handle turns about the shifted pen position
- engine: text.touchType description; dy is up from the line
- docs: mcp.md Touch Type paragraph, target-app-parity.md progress line
2026-11-01 11:08:57 +07:00
dotCooCoo 94ef9b87c2 Type: Character panel toggles show off for a mixed selection, and text.setFormat checks on/off values
The Character panel's All Caps, Underline and Strikethrough buttons, and
its menu's All Caps, Underline, Strikethrough, Proportional Metrics,
Tate-chu-yoko and No Break, read the first selected character's style.
With only some characters marked they could show on, and choosing one
turned the attribute off for every character. They now show on only
when every selected character has the attribute, and a click on a mixed
selection turns it on for all, as Small Caps and Standard Vertical Roman
Alignment do. type.underline (Cmd+Shift+U) underlines a mixed selection
the same way. The Character Alignment and Digits submenus check no item
while the selected characters differ.

text.setFormat ignored a value that was not true or false for underline,
strikethrough, allCaps, proportionalMetrics, tateChuYoko,
standardVerticalRoman, noBreak, hyphenate, hyphenateCapitalized and
romanHangingPunctuation. It now refuses it with "`<key>` must be true or
false" and changes nothing, as text.setRangeStyle does.
2026-11-01 11:08:56 +07:00
dotCooCoo d72022daf5 Grid tools: F, V, X and C change the divider skews by 10 while a grid is dragged out
Squashed from #5019 onto main.
2026-11-01 08:33:02 +07:00
dotCooCoo ac896cfe3d PDF export: a Perceptual gradient of one spot ink's tints keeps its ink
A Perceptual linear or radial gradient was always written as an image,
so a gradient of spot ink tints lost its plate, and no warning named the
spot ink.

PDF export now writes a Perceptual gradient whose stops are tints of one
spot color (and paper white) as a Separation shading in that ink. Its
tints follow the gradient's curve: at each stop the renderer places on
the curve, the tint with the OKLab lightness of the color there.

Dithered fills, freeform gradients and Perceptual gradients that mix a
spot color with other colors are still images, and the export now warns
when such a gradient has stops linked to a spot swatch that its spot
colors are written in process colors.
2026-11-01 08:32:54 +07:00
Don Omondi 961694cba6 Fix #4918: delete nested picks safely, keep picks per document
symbol.delete {names} expands every instance of the named symbols,
round by round, before it removes any definition, so a symbol and the
one placed inside it delete alike in either order. The Symbols panel's
picks belong to the document they were made in, and a single picked
symbol is deleted only while symbol.unused still lists it.
2026-11-01 07:38:01 +07:00
Don Omondi 5b639169cd Fix #4918: Symbols panel Select All Unused
symbol.unused lists the symbols nothing places: instances in the art
(opacity masks included), graph designs, pattern tiles and brush art
count, and so do instances in a used symbol's art. The Symbols panel
menu's Select All Unused shows them selected, and Delete Symbol asks
once and deletes those still unused in one undo step
(symbol.delete {names}).
2026-11-01 07:38:01 +07:00
dotCooCoo 8805fa8214 Place: Word .doc text with a broken surrogate pair is refused, and fields without a result warn
A 16-bit piece holding half of a UTF-16 surrogate pair was placed with
U+FFFD in its place; the document is now refused. A pair split between
two pieces is decoded as one character.

A field with no result (no separator, never ended, or nested past 64)
is left out with its own warning, and the warning about fields placed
as their results is given only for fields that have one.
2026-11-01 07:16:30 +07:00
dotCooCoo 5fdccdf602 Place: Word 97-2003 .doc files place as area type with their formatting
File > Place reads a Word 97-2003 document (a compound file named .doc,
or one that holds a WordDocument stream) and sets its main text as area
type, as it does for .rtf and .docx files. The text comes from the
piece table, in 16-bit Unicode or compressed 8-bit pieces. Formatting
is resolved from the style sheet's default font, the paragraph style,
the character style and the direct character and paragraph properties,
and keeps the same properties the .docx reader maps.

Table cells are separated by tabs and each row is a paragraph, and a
field places as its result. Hidden and deleted text, pictures, headers,
footers, notes, comments and text boxes are left out, and the reply's
warnings name each kind of content left out or changed once. Word 6.0
and Word 95 documents, encrypted documents and documents last saved
with fast save are refused, and so are structures that lie outside
their streams.

The compound file is read with the cfb crate (MIT), which adds one
package to Cargo.lock.
2026-11-01 07:16:29 +07:00
bflatastic 7d8f1d4e06 Fix #4978: Area Type Options › Text Flow by rows or by columns 2026-11-01 06:11:11 +07:00
bflatastic 8704c15c6d Fix #4980: Pen with Cmd/Ctrl drags the path, not hidden bounding-box handles
Cmd/Ctrl held with the Pen lends the last-used selection tool (Direct
Selection until one is chosen), which already moved anchors, straight
segments and curves of the path being drawn, one undo step each, and
handed the path back to the Pen.

With the Selection tool used last, the lent tool hit-tested the
bounding box even though the canvas hides it while anchors are selected
on their own, as the end anchor of a path being drawn is. Over the
path it showed a resize pointer, and the drag scaled the path by a
handle nobody could see: a flat path did nothing at all.

The Selection tool's handles now take the pointer only while the box is
shown (View > Show Bounding Box, no anchors selected on their own), so
the lent Selection tool moves the path being drawn.

Tests: select::tests::hidden_box_handles_take_no_press_while_anchors_are_selected,
tests::draw2::pen_cmd_drag_edits_anchors_and_segments_and_goes_on_drawing.
2026-11-01 06:11:09 +07:00
dotCooCoo f66b583b5b Tests: mixed spot gradients export as DeviceN shadings when radial or in a Lab ink
- A radial gradient from a spot swatch to a CMYK color is written as a
  DeviceN shading (ShadingType 3) of Yellow and the spot ink.
- A Lab spot color used as a flat fill and in a mixed gradient is
  written as one Separation space with the Lab alternate, which the
  DeviceN space names under /Colorants.
- docs/roadmap.md lists the gradients that still export without the
  spot ink: mixed spot/process gradients in SVG and PDF gradients with
  RGB or Lab stops. docs/target-app-parity.md also notes that a
  Perceptual gradient is written to PDF as an image.
2026-11-01 04:55:11 +07:00
dotCooCoo ba16468710 PDF export: a gradient that mixes a spot color with process colors keeps the spot ink
A linear or radial gradient whose stops mix a spot color with process
colors, or two spot colors, was written in process colors with a
warning. It is now written as a DeviceN shading of the process inks
its stops use and its spot inks, so each spot ink separates onto its
own plate. An off-center midpoint becomes a stop with the inks halfway
between its two stops. A gradient that mixes a spot color with RGB or
Lab colors is still written without the spot ink, with a warning that
says so.
2026-11-01 04:55:11 +07:00
dotCooCoo bbcb886a40 Perspective: a stored projection whose determinant overflows reads back at a w row of 1
A stored projection whose determinant is not finite is read back as the
same map divided by the largest magnitude in its w row, the scale the
overflow branch of conjugated already uses, so points that overflowed at
the stored scale draw: the identity with every entry 1e308 maps (2, 2)
to (2, 2). The overflow branch of inverse scales the map so that the
largest of the products its determinant sums is near 1, so a map with
large entries and a small w row, such as a scale by 1e305 with a move
by -3.9e307, is still inverted and read back.

The two overflow tests in persp_text.rs build their stored projections
with a w row of 1, so they still overflow after the read-back.
2026-11-01 04:55:11 +07:00
dotCooCoo 18510b77af Appearance: effect.apply reports the new effect's index in the object whose row it names
The reply's index came from the last object the effect was added to, while
its item came from the first selected object (or the first target for an
explicit item). When those objects held different numbers of effects on the
row, the reply named no effect of that object. The index is now read from
that object's effect list.
2026-11-01 04:55:10 +07:00
dotCooCoo af86b2c833 Objects on Path: double-clicking the tool opens its options
Double-clicking the Objects on Path tool opens Objects on Path Tool
Options: a nine-point Pivot grid, a Rotate field, Reset (the center and
0) and Save. Save sets the tool's options, which last across tool
switches and are saved with the preferences. The tool sends them with
each attach, and object.objectsOnPath.attach without pivot or rotate
uses them, so they apply to the objects attached next; groups attached
before keep theirs.
2026-11-01 04:53:41 +07:00
bflatastic f22caa6108 Fix #4938: Dynamic symbols keep per-instance appearance linked to the symbol 2026-11-01 04:53:38 +07:00
bflatastic 8c35bacfb3 Global Edit: the menu checks a selected group's kind only; its contents are checked on start 2026-11-01 03:36:43 +07:00
bflatastic 3146ca513b Fix #4939: Global Edit finds and edits groups like the selected one 2026-11-01 03:36:43 +07:00
dotCooCoo dfd07552c8 Cursors: Caps Lock gives the drawing tools the crosshair
While Caps Lock is on, the tools whose pointer General > Use Precise
Cursors changes show the crosshair, and the saved preference does not
change. The desktop app reads the lock state on macOS, Windows and
Linux X11 and passes each change to the new view.capsLock command. A
change to view.capsLock or usePreciseCursors through app.run refreshes
the canvas pointer. The view.preciseCursors command is labeled Use
Precise Cursors, so command search finds it by the preference's name.
2026-11-01 01:59:54 +07:00
dotCooCoo db6c5ea9df Fix #2678: Ungroup no longer multiplies the group's opacity into its members
Ungroup multiplied each member's opacity by the group's, so a member of a
50% group stayed at 50% after the group was gone, and members that
overlapped showed through each other. Each member now keeps its own
opacity, and the group's opacity is removed with the group.
2026-11-01 01:22:08 +07:00
dotCooCoo 5a5a7b9ce8 Mesh tool: a click on a mesh's edge from just outside adds the lines there
A Mesh tool click within the hit tolerance outside a gradient mesh hit the
mesh, but object.mesh.addLine looked for the point inside the outline only
and failed with "point is outside the mesh". The same click on a path's edge
made a 1x1 mesh without the line.

- GradientMesh::locate_within takes a tolerance: a point outside the mesh
  within it of the outline gives the nearest point of the edge.
- object.mesh.addLine and object.mesh.create take an optional `tolerance`
  (0 by default), and the Mesh tool sends its hit tolerance.
2026-11-01 01:22:08 +07:00
dotCooCoo 9896f633da Type: Kerning has an Optical option that spaces characters by their shapes
Squashed from #4825 onto main.
2026-11-01 01:22:04 +07:00
dotCooCoo 012af4fd49 Brushes: each Pattern Brush Options tile can be set to a pattern swatch or None
A menu under each of the five tiles in Pattern Brush Options lists None,
Original and the document's pattern swatches; the corner menus keep the
four automatic corners. A swatch choice copies the swatch's whole art and
its tile frame into the brush, so the brush keeps drawing after the swatch
is deleted, and the menu then shows that art as Original. Side cannot be
None: OK shows "You must choose a side pattern." and keeps the dialog open.

brush.options takes side_tile, outer_corner_tile, inner_corner_tile,
start_tile and end_tile (a pattern name, none, original, the corner auto
ids, or {"pattern": name} for a swatch named like one of those words), and
rejects side_tile "none" and unknown names before editing.
2026-11-01 01:22:03 +07:00
dotCooCoo b1aa576764 Brushes: Delete Brush asks Yes or No for unused brushes, and Remove Strokes or Cancel for brushes in use
The question for brushes no stroke uses reads "Delete the brush selection?" with Yes and No, from the bottom button and the panel menu, and Alt-click (Option-click on macOS) on the button still deletes them without asking. When a stroke uses one of the brushes, the button, Alt-click and the menu item ask with Remove Strokes and Cancel; Remove Strokes deletes the brushes and the strokes that used them become plain strokes.

brush.unused also counts the tile that pattern editing's Cancel puts back, which brush.delete already clears, so the panel asks the in-use question for a brush only that tile uses.
2026-11-01 01:21:54 +07:00
dotCooCoo b46cdc4e4d Isolation mode: Lock, Unlock All, Hide and Show All work on the isolated art
Squashed from #4849 onto main.
2026-11-01 01:21:53 +07:00
dotCooCoo ec8c1cb8d1 Intertwine: a right-click where three or more objects overlap arranges the nearest one
Squashed from #4862 onto main.
2026-11-01 00:36:29 +07:00
dotCooCoo bb6cbfb2b4 Images: SVG and EPS export write colorized grayscale and 1-bit images in their fill color
SVG export ignored an image's colorize paint and wrote the image's own
black, white and gray pixels. EPS export flattened every colorized image
into a process-color raster, so a spot color lost its plate. Both now
write the image in the fill color, with the coverage the canvas and PDF
export use (darkness times alpha), so white and transparent pixels take
no color.

- SVG writes a coverage PNG as a mask over a rectangle in the color,
  shared between repeated images while each keeps its own color. Linked
  mode writes the coverage PNG as a file, and SVG Tiny 1.2 writes a PNG
  in the color. A colorized image whose pixels are missing, or whose
  grayscale pixels can't be decoded, is left out with a warning.
- EPS writes fully covered or uncovered pixels as an imagemask in the
  color's process or Separation ink (Registration on every plate), and a
  grayscale image as ink samples scaled by coverage. Level 3 masks its
  uncovered pixels out and Level 2 writes them white; gray midtones are
  written over white, with the existing warnings.
- EPS flattening no longer counts a solid colorize paint on a grayscale
  or 1-bit image as transparency by itself. Opacity, blending modes,
  opacity masks, effects and overlapping transparent art still flatten
  it, and so does a colorize paint the writer can't draw.

The svg crate's tests turn on resvg's raster-images feature to render
the embedded PNGs.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-01 00:35:27 +07:00