Commit Graph
9906 Commits
Author SHA1 Message Date
ncic 9709c871fc Merge upstream main into pencil-no-guides
# Conflicts:
#	docs/target-app-parity.md
2026-11-13 23:52:26 +01:00
dotCooCoo 2ae9a8f0f0 Graphs: an opacity or blend mode set on a series group is kept
transparency.set and object.setProps with an object opacity or blend mode,
given a series group picked with Group Selection, set the group's own
opacity and blend mode, and the graph drew its series groups again
without them. The graph now stores them for the series (groupOpacity and
groupBlend) and puts them back on the series group when it is drawn
again. Commands that clear the group's own opacity and blend mode
(appearance.clear, graphicStyle.apply, appearance.transfer onto the
group, graphicStyle.redefine of a linked group) remove what the graph
stores.
2026-11-13 14:52:08 -08:00
dotCooCoo 0d26724a7c Graphs: Appearance and effect commands leave a design's art in a series out
A graph draws the art of a column or marker design inside a series again
from the stored design. Rows, effects and graphic styles that a command
gave that art were lost the next time the graph's data, type or size
changed. The paint commands already left the art out.

- The appearance.* row and whole-appearance commands, effect.apply,
  appearance.addEffect and the effect.* commands that edit, move or
  remove an effect, threeD.mapGraphic, graphicStyle.apply,
  graphicStyle.addFromLibrary with apply, transparency.set with a fill
  or stroke item, and appearance.copyFrom (as the target) leave out any
  object inside a series group other than a path directly in it, given
  by id, selected or reached through target: "contents". They edit the
  other objects given, and a command whose objects are all design art
  is an error.
- appearance.transfer with design art as its target, or as its source
  without copy, is an error.
- A Pathfinder effect no longer groups selected design art.
- The paint commands given a design's copy with a fill or stroke item
  leave it out as well.

docs/mcp.md states the rule, and the Graphs line in
docs/target-app-parity.md names it.
2026-11-13 14:51:31 -08:00
dotCooCoo e00e5c9cd9 Guides: removing and editing artboard guides visits each layer once
Deleting ruler guides and moving or scaling an artboard's guides asked
each layer whether its whole subtree held a matching guide, then asked
every sublayer the same question again on the way down. A layer nested
N deep was scanned N times, so a chain of N layers cost N(N+1)/2 layer
scans.

One read walk now records which children match and which sublayers hold
a match, and the edit visits and copies only those layers. The guides
removed, the count returned, the selection, the order the guides are
changed in and the sharing of untouched layers are the same as before.

The same walk replaces the repeated subtree scans in the other guide
edits in this file:

- tidying the ruler guides of an opened file (guides below art, in
  objects, in opacity-mask art, in symbols and patterns)
- saving the guides as a list
- removing an editing layer while keeping its guides
2026-11-13 14:49:19 -08:00
dotCooCoo aa5d142bd8 .ai save: the editing copy keeps text wrap Offset and Invert Wrap
A PDF-compatible .ai saved with Include Editing Data for Other Apps
wrote no text wrap options, so a wrap object opened from the file no
longer wrapped type. An object with only wrap options got no
dictionary at all.

The writer now writes each wrap object's options in its dictionary as
`AI11TextWrapOffset` (`/Real`) and `AI11TextWrapInvert` (`/Bool`, 1 or
0), the entries `text_wrap` in crates/eps/src/import/ai/read/document.rs
reads, beside the object's name, variables and opacity mask.

- The offset is the one in the art's space, so on a large canvas it is
  written at 1/10 and read back at its size. An offset that is not
  finite is written as the default 6 pt, and one past the -1000 to
  1000 pt the reader keeps as the nearer end of that range.
- Art the writer draws for an object (a blend's steps, a symbol
  instance written as its art, a brushed stroke's pieces, pattern
  tiles, an object's drawn look) is written without the options of the
  objects it is made from. The object itself and a symbol's own art
  keep theirs.
- A clipping path written as its fills and its strokes has its options
  once.

Docs: docs/file-format-parity.md, docs/target-app-parity.md.
2026-11-13 14:48:16 -08:00
dotCooCoo ccc9c6b341 Title bar: the app bar places its window buttons by the desktop's button layout
On Linux and the BSDs, craft-window reads the desktop's window-button
layout (`button-layout` in `org.gnome.desktop.wm.preferences`) into
`Frame::App` when the desktop leaves the window's frame to the app, but
the app bar ignored it and always drew Minimize, Maximize and Close at
its right end.

- `CaptionLayout` lists the buttons at each end of the bar;
  `caption_buttons` draws them there in the listed order, first one
  leftmost, and the bar's other controls keep clear of both groups.
- The desktop app copies the layout of an app frame the desktop left to
  it. An app frame turned on by `VECTORCRAFT_TITLEBAR=app` or by
  VectorCraft Title Bar on Every Desktop, a window opened through
  XWayland, macOS and Windows keep all three at the right.

Docs: docs/control-protocol.md, docs/target-app-parity.md.
2026-11-13 14:45:12 -08:00
BFlat 28bd9c9292 Merge pull request #7298 from dotCooCoo/fix/hi-message-rows
i18n: Hindi rows no longer keep English words or garble their sentences
2026-11-14 05:42:51 +07:00
BFlat 517b725be5 Merge pull request #7089 from dotCooCoo/fix/smooth-fidelity-screen-pixels
Smooth: Fidelity is in screen pixels, so a drag smooths as much at any zoom
2026-11-14 05:42:42 +07:00
BFlat 64c6387b84 Merge pull request #7260 from dotCooCoo/fix/tool-options-refuse-invalid
Live Paint and Shape Builder: tool.setOption refuses invalid options
2026-11-14 05:42:33 +07:00
BFlat 0899d4026a Merge pull request #7302 from dotCooCoo/fix/plugin-before-item-3d-context
Effects: a plug-in before a fill's or stroke's own 3D effect is told its fill or stroke
2026-11-14 05:42:24 +07:00
BFlat 39027eb42e Merge pull request #7292 from dotCooCoo/fix/ai-mask-art-live-effect
.ai export: opacity mask art keeps its live effects in the editing data
2026-11-14 05:42:16 +07:00
BFlat 2fe70b2483 Merge pull request #7258 from dotCooCoo/fix/graph-label-appearance-kept
Graphs: Appearance edits on a label are kept for every label of its kind
2026-11-14 05:42:08 +07:00
BFlat c36aaec289 Merge pull request #7296 from dotCooCoo/fix/delete-artboards-one-pass
Artboards: delete several artboards and their guides in one pass
2026-11-14 05:41:59 +07:00
BFlat 1b039350a3 Merge pull request #7297 from dotCooCoo/fix/global-edit-enabled-for-supported-groups
Global Edit: Start Global Edit is offered only for a group it can start on
2026-11-14 05:41:50 +07:00
BFlat 5fd9ef537a Merge pull request #7294 from dotCooCoo/fix/live-shape-unknown-keys
Live Shapes: object.setLiveShape refuses unknown keys and invalid sides or radius
2026-11-14 05:41:41 +07:00
BFlat 7b992a416e Merge pull request #7287 from dotCooCoo/fix/recovery-error-guidance
Data Recovery: a failed copy names the folder in use and how to change it
2026-11-14 05:41:33 +07:00
BFlat 795f01782f Merge pull request #7272 from dotCooCoo/fix/invert-wrap-one-pass
Text Wrap: Invert Wrap intersects a line's spans in one pass
2026-11-14 05:41:20 +07:00
dotCooCoo ac5e9c6618 Graphs: a kept gradient takes the same place on every mark of its series
A graph series kept a gradient with the document coordinates it had on
the one mark it was stored from, and gave that paint unchanged to every
mark when the graph was drawn again. The other marks showed the gradient
placed over the edited mark, and the placement stayed behind when the
data or the graph moved the marks.

The graph now stores a gradient relative to the unit box of the mark it
comes from (its geometric bounds, or its stroke's box for a stroke).
Each path of the series, the legend swatch and a Filled Lines band
included, takes it on its own box. Every store route does this:
paint.setFill and paint.setStroke, the Eyedropper, graphic styles,
Appearance commands, paint.setGradientGeom and the freeform point
edits. A command that writes a mark's paint as the graph drew it leaves
the stored gradient as it is and records no undo step.

The series paints save this as fillUnitBox and strokeUnitBox. A
gradient in a file saved without these flags is drawn where it was
stored. So is a gradient stored from a path with no width or height.

Docs: docs/mcp.md, the paint.setGradientGeom params text and the Graphs
line in docs/target-app-parity.md.
2026-11-13 14:41:12 -08:00
BFlat df581b2521 Merge pull request #7282 from dotCooCoo/feat/banner-hems-grommets
Cutting Tools: Banner Hems and Grommets marks a banner's hems and grommet holes
2026-11-14 05:41:10 +07:00
BFlat 04015f75cc Merge pull request #7277 from dotCooCoo/fix/break-text-thread-lookup
Artwork Cleanup: Break Text and Remove Breaks read the text threads once
2026-11-14 05:41:00 +07:00
BFlat 64b4cb1ad6 Merge pull request #7300 from dotCooCoo/fix/composite-fonts-per-document
Type: documents that define one composite font name differently each draw in their own
2026-11-14 05:40:49 +07:00
BFlat c135ba1f9b Merge pull request #7279 from dotCooCoo/fix/path-type-probe-wider
Type: threaded type on a path aligned Justify All is measured by paragraphs
2026-11-14 05:40:38 +07:00
BFlat 35d7c5d523 Merge pull request #7271 from dotCooCoo/fix/pdf-long-page-warning-names-option
PDF: the long-page warning names the in-points option
2026-11-14 05:40:30 +07:00
BFlat e21e2622d0 Merge pull request #7281 from dotCooCoo/fix/path-precision-picks-one-pass
Path Precision Tools: group selected segments and crossings by subpath in one pass
2026-11-14 05:40:21 +07:00
BFlat 4097683778 Merge pull request #7290 from dotCooCoo/fix/font-wheel-test-glyph-protection
Tests: the floating Character font wheel test turns off missing glyph protection
2026-11-14 05:40:10 +07:00
BFlat e244a65981 Merge pull request #7293 from dotCooCoo/fix/offset-path-parallel
Offset Path: offsets several selected paths on several threads
2026-11-14 05:40:00 +07:00
BFlat 55fe8b62d4 Merge pull request #7278 from dotCooCoo/fix/rtl-tab-line-hyphenation
Type: right-to-left lines with tabs are measured as they are drawn
2026-11-14 05:39:52 +07:00
BFlat 00d2f6d24b Merge pull request #7283 from dotCooCoo/fix/nested-svg-image-count
SVG import: broken nested SVG images do not use up the image allowance
2026-11-14 05:39:43 +07:00
BFlat 31eb3b0d1e Merge pull request #7285 from dotCooCoo/fix/pdf-warped-image-written-once
PDF export: a warped image writes its pixels once for all its pieces
2026-11-14 05:39:34 +07:00
BFlat 2f44c32060 Merge pull request #7032 from addibble/taper-one-face
3D: a tapered Extrude & Bevel wall is one face
2026-11-14 05:39:24 +07:00
dotCooCoo 15b75be860 i18n: Italian translates 1,802 more status and error messages
it is in COMPLETE_MESSAGES, but the untranslated report listed 1,842
status and error messages with no Italian row, so with the interface in
Italian those messages showed in English.

- it.tsv gains 1,802 `@msg` rows. The translations are VectorCraft's
  own, written from the English meaning with the catalog's terms, and
  await native review. Placeholders, backticked names, command ids,
  double-quoted values and bare true, false and null stay as the
  English writes them.
- The untranslated report now lists 40 Italian messages. 38 of those
  keys get Italian rows from open pull requests 7211, 7214 and 7116,
  so they are left out here. Menu strings, `tl!` literals, undo labels
  and swatch names are unchanged.
- The Italian row of docs/localization-parity.md gives the catalog's
  current counts, and docs/target-app-parity.md has a line for the
  change.
2026-11-13 14:35:40 -08:00
dotCooCoo a8833442dd Type: a document replaced in the back measures its type when it is replaced
Session::replace_document on a tab other than the current one cleared
the bounds of type set in a font named like a composite font and left
that type with estimated bounds until the document came to the front.

- The replacement's type without bounds is measured at once, in the
  document's own composite fonts, lent for the measuring. The front
  document stays the one whose composite fonts are registered.
- When the document then comes to the front unchanged, its type in its
  own composite fonts is not measured again. Type set in a composite
  font only another document defines is still measured again, as
  missing, and so is type set in a composite font name that another
  thread's loan held with a different definition.
2026-11-13 14:29:46 -08:00
dotCooCoo 12bc3a653d Shapes: addon.shapes commands refuse values outside their ranges
addon.shapes.polygon and addon.shapes.star rounded a fractional side or
point count and clamped it to 3..1000, so 2.5, 2 or 5000 drew a shape
with a different count than asked for. addon.shapes.configure read
sides only as an unsigned integer, ignored a fraction and cut a large
value to 32 bits before clamping it, so 4294967299 gave a triangle.

The commands now refuse a supplied value outside its range with an
error that names the parameter and the range, and draw or change
nothing; an omitted or null value still takes its default:

- sides and points: a whole number from 3 to 1000
- rounded and randomized: -10 to 10; spokeRatio: 0.01 to 1
- turns: 0.01 to 1024; divergence: 0 to 1000; innerRadius: 0 to 0.999
- width, height, radius and radius1: 0 or more (a negative size was
  made positive before, and a negative corner radius became 0)
- radius2: 0.01 to 1 times radius1 (the ratio was clamped before)
- radii: four numbers, each 0 or more (a malformed array was ignored)
- arcMode: whole, pie (slice), chord or arc (open); any other value
  drew a whole shape before

addon.shapes.configure checks every value before it changes any of the
selected shapes, keeps fitting a corner radius to half each rectangle's
shorter side, and records no undo step when no shape changes.

Docs: docs/mcp.md (3D Box and shapes add-on), docs/target-app-parity.md.
2026-11-13 14:29:23 -08:00
dotCooCoo 6b487226aa i18n: Japanese has a row for every message the untranslated report lists
The Japanese catalog lacked a row for 1,842 of the status and error
messages that the untranslated report finds in the sources, so the
status bar showed those messages in English.

- ja.tsv gains 1,842 `@msg` rows: the errors of commands and their
  parameter checks, scripts, file import and export, printing and
  plug-ins. The translations are VectorCraft's own, written from the
  English meaning with the catalog's conventions and its existing
  feature names. Placeholders, parameter names, command ids and
  literal values stay as the English writes them.
- The untranslated report now lists no Japanese messages. Menu
  strings, `tl!` literals and undo labels are unchanged.
- Two of the messages have two placeholders in a row. The status bar
  cannot match such a row, so it does not use those two rows.
- The row for `{k} has control characters{_1}` does not match the
  message for a multi-line File Info field, where the last
  placeholder is empty.
- The Japanese row of docs/localization-parity.md gives the catalog's
  current message counts.

Docs: docs/target-app-parity.md, docs/localization-parity.md.
2026-11-13 14:28:53 -08:00
dotCooCoo 193ed34794 i18n: German has a row for every message the untranslated report lists
The German catalog lacked a row for 1,842 of the status and error
messages that the untranslated report finds in the sources, so the
status bar showed those messages in English.

- de.tsv gains 1,842 `@msg` rows: the errors of commands and their
  parameter checks, scripts, file import and export, printing and
  plug-ins. The translations are VectorCraft's own, written from the
  English meaning with the catalog's conventions and its existing
  feature names. Placeholders, parameter names, command ids and
  literal values stay as the English writes them.
- The untranslated report now lists no German messages. Menu strings,
  `tl!` literals, undo labels and swatch names are unchanged.
- Two of the messages have two placeholders in a row. The catalog does
  not load such a row, so those two are still shown in English.
- The German row of docs/localization-parity.md gives the catalog's
  current message counts.

Docs: docs/target-app-parity.md, docs/localization-parity.md.
2026-11-13 14:16:51 -08:00
dotCooCoo 9fd0ede79f Canvas range: removed guides are looked up through sets, and nested layers are walked once
When a read document is brought within the canvas, each saved guide was
checked against the removed artboard ids and the removed layer ids with
linear scans, and each guide node against the removed artboard ids, so
the work grew with the number of guides times the number of removed ids.
The ids are now collected into ordered sets, and each guide is checked
in logarithmic time.

Removing what lies beyond the canvas scanned each container's whole
subtree to see whether anything reached beyond, then scanned each child
again while removing, one level down at a time, so a deeply nested
object was measured once for every group around it. Removing guide nodes
probed each layer's subtree the same way before descending into its
sublayers. Both now make one read-only walk that records, by index,
which objects reach beyond (or which guides are removed), and the
removal follows that record. The objects clipped by a clipping path
beyond the canvas are still not measured.

Containers with nothing to remove are still not copied. The objects and
guides that remain, their order, the counts, the retired artboard ids
and the saved selections are unchanged.
2026-11-13 14:15:25 -08:00
dotCooCoo 2fc344cb8c PDF/.ai import: graphs keep their data and settings when their records read whole
A legacy .ai graph (%_Gs ... %_GS) opened as plain art, with its table,
type, axes and settings left out. A grouped column, stacked column, line
or area graph now keeps them on its group when every record has the
shape and values the version 7 specification's appendix A gives, the
table holds at least one number, the drawn art strokes the plot's left
and bottom edges, and each series group is tagged once as a whole
column, holds only paths and paints them with one fill and one stroke.
The saved art stays as it is on open; Graph Data redraws the graph from
its data afterward, with each series' fill and stroke kept.

Any other graph (pie and scatter graphs, designs, customizations, record
shapes of later versions, tables past 10,000 categories, 256 series or
2,600,000 cells) keeps its art as plain objects with the existing note.

Saving a graph to .ai, in either form, now warns that its data and
settings are not written.
2026-11-13 14:15:19 -08:00
dotCooCoo e914d8abdd Merge main into fix/doc-info-values-translated 2026-11-13 14:13:45 -08:00
dotCooCoo c13408fb8e i18n: French translates every status and error message the report lists
fr is in COMPLETE_MESSAGES, but the untranslated report listed 1,842
status and error messages with no French row, so with the interface in
French those messages showed in English.

- fr.tsv gains 1,842 `@msg` rows in one block after the last row. The
  translations are VectorCraft's own, written from the English meaning
  with the catalog's terms and typography (no-break space before
  : ; ? ! and inside « »). Placeholders, backticked names, command ids,
  double-quoted values and bare true, false and null stay as the
  English writes them.
- The untranslated report now lists no French messages. Menu strings,
  `tl!` literals, undo labels and swatch names are unchanged.
- The French row of docs/localization-parity.md gives the catalog's
  current counts.
2026-11-13 14:04:32 -08:00
dotCooCoo 77e4dceb9d Type on a path: overflowing right-to-left type shows the longest start that fits
Right-to-left type on a path that overflows searched for the start of its
text that fits with a binary search, which assumed that a longer start
never fits once a shorter one does not. A letter tracked to a negative
advance breaks that: with steps of 6, 6, -3 and 6 points in a 10 point
span, the first three letters fit while the first two do not, and the
search showed only the first letter.

- Without hanging punctuation, and with no spacing or on a path of one
  straight segment, every start is measured in O(n log n): each glyph
  keeps its place in the whole text's visual order, except the trailing
  whitespace and controls that UAX #9 rule L1 sets to the paragraph's
  level.
- With hanging punctuation, or spacing on a curve, when a letter has a
  negative advance, a start is a candidate only when its glyphs, in its
  own visual order and each at the least step it can take, reach no
  farther than the path's length. This lower bound holds with negative
  advances.
- At most 32 candidates are arranged from the longest. When none of
  them fits, a binary search over the rest shows a start that fits,
  which is not always the longest.
- With hanging punctuation, or spacing on a curve, text with no negative
  advance keeps the binary search.

Right-aligned and centered type on a curve with Spacing was placed using
the width its glyphs have at the start bracket, while the steps were
measured where the line was placed, so a line that fits could end past
the end bracket and lose glyphs. It is now placed using the width of the
steps it has where it is placed.
2026-11-13 14:04:05 -08:00
dotCooCoo 380f03345f Merge main into feat/smooth-free-end-tangent 2026-11-13 14:01:37 -08:00
dotCooCoo 6e308c4e54 Graphs: Global Edit, Randomize Opacity and Sort Objects keep what they give a series
A path directly in a graph series lost what these commands gave it when
the graph was drawn again, for example after graph.setData:

- While Global Edit is on in an isolated graph, a match that is a series
  path, or a series group whose paths take the edit, now has what
  changed on its paths stored for their series: the paint, weight,
  opacity and blend mode of the topmost visible fill and stroke, and the
  object's opacity and blend mode. The edited match is stored by its own
  command as before.
- addon.arrange.randomizeOpacity and addon.arrange.sort with apply
  "opacity" now store the object opacity, with the blend mode, of the
  series paths whose opacity they change. When they change several
  paths of one series, the frontmost of them sets what the graph
  stores, as for the other commands.

The graph stores them in the same undo step as the edit. The params
text of addon.arrange.randomizeOpacity and addon.arrange.sort,
docs/mcp.md and docs/target-app-parity.md describe the new behavior.
2026-11-13 13:53:55 -08:00
dotCooCoo ff846e1483 Merge main into feat/smooth-cursor-without-path 2026-11-13 13:50:39 -08:00
ncic eaa3efe45e Merge upstream main into pencil-no-guides
# Conflicts:
#	crates/tools/src/draw2/gesture/tests.rs
#	docs/target-app-parity.md
2026-11-13 22:49:03 +01:00
ncic 1b3454b4f8 Merge upstream main into snap-labels
# Conflicts:
#	docs/target-app-parity.md
2026-11-13 22:48:42 +01:00
dotCooCoo e51e67116d Path: anchor edits on a long selected path skip measuring it for the canvas check
The check after each edit that the selected objects stay within the
canvas measured the whole of every selected path. Moving one anchor of
a 10,000-anchor path measured all 9,999 segments. A path or compound
path whose anchors and handles are all finite and inside the canvas
range now passes without being measured, because its curves stay inside
their control points. A path with a point outside that range is still
measured, so the check accepts and refuses the same edits as before.

path.moveAnchor to the point the anchor is already at, and
path.setHandle to the point the handle is already at, now leave the
document as it is and record no undo step.

vectorcraft_geom::path::measured_segments counts the segments that
PathData::bounds measures, and the new engine tests use it.
2026-11-13 13:46:30 -08:00
dotCooCoo cccc17d8f5 Merge main into fix/smooth-keeps-closed-start 2026-11-13 13:43:28 -08:00
dotCooCoo 08f7ce6670 Area type: lines stop at a gap between slanted contours however narrow
Two contours of an area-type frame now count as meeting along a slanted
edge only when their edges lie exactly on one line, checked with an exact
cross product, and the intervals on either side of a seam join only
where both edges cross the middle of the band at the same x, found along
the same line. Before, both checks allowed a few units in the last place
of the coordinates, which near the edge of the canvas (x = 3,999,000)
is wider than a real gap of one to four units, so a line ran across the
gap outside the frame.

An edge divided by an anchor exactly on it still joins, including where
the divided edge's own crossing would round to another x.
2026-11-13 13:42:05 -08:00
dotCooCoo c8177dc7e8 Effects: a plug-in before a fill's or stroke's own 3D effect is told its fill or stroke
A 3D effect on a fill or stroke worked on copies of the object's fills
and strokes, and the copies were looked up among the object's items by
identity, so a live-effect plug-in before that 3D effect was given
`_context.item` "object" and a deferred stage there was listed with no
item. The drawn art, the 3D meshes and Expand as Wireframes now pass the
original item, so the plug-in is given `{"fill": i}` or `{"stroke": i}`
as docs/plugins.md describes. The paints of a type's first run still have
no item.
2026-11-13 13:33:01 -08:00
BFlat 4558196e16 Merge pull request #7053 from dotCooCoo/feat/warichu
Type: Warichu stacks selected characters on lines inside their line
2026-11-14 04:32:40 +07:00