Commit Graph
354 Commits
Author SHA1 Message Date
Cursor Agentandnrq6993-a11y d792e7b2be Pattern edit preview: two dimmed copy groups around centre tile
Review feedback on #3498: dimmed neighbours share one opacity composite
before the centre tile and one after, preserving overlap paint order
without per-copy compositing.

Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
2026-10-22 09:23:27 +07:00
Cursor Agentandnrq6993-a11y e87534342b Fix #3480: pattern edit preview honors Overlap stacking
## Summary
In pattern editing mode, preview copies were drawn in row-major order and the
centre tile was always composited on top, so Pattern Options Overlap had no
visible effect when negative spacing made tiles overlap (as in Illustrator).
Preview copies now paint in the same order as tiled pattern fills.

## Tests
- vectorcraft-doc: preview_paint_order_respects_overlap

Closes storytold/vectorcraft#3480

Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
2026-10-22 09:23:27 +07:00
Murat Vural e33094e11d i18n: add Turkish (tr) localization
- Add crates/ui-egui/src/i18n/tr.tsv covering all menu strings, commands, UI labels, and status/error messages with standard Turkish vector software vocabulary.
- Register Turkish in LANGUAGES with plural_none and complete_menus: true.
- Support tr, tr-TR, tr_TR.UTF-8, tr-CY locale mapping in lang_from_tag.
- Add Turkish typographic quotation marks in crates/doc/src/setup.rs.
- Add turkish_reads_as_turkish unit test and register tr in KEEPS_MENU_NAMES and COMPLETE_MESSAGES.
- Include RTF diagnostic message with exact TSV escaping.
2026-10-22 07:21:51 +07:00
Jürg Lehni 60f4c4f145 Render: moved type with mitred strokes redraws where its spikes reach
Follows #3461: character strokes' miter spikes reach past half the weight, which the redrawn area missed.
2026-10-22 06:52:06 +07:00
Dylan M. Taylor f64db69b53 Use chrono instead of hand-rolled UTC conversion (#3387)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the hand-rolled Howard Hinnant civil/days-from-civil copies and the
leap-year/days-in-month tables with chrono as a workspace dependency:
- apps/vectorcraft/src/logging.rs: timestamp (log line time)
- crates/doc/src/metadata.rs: civil (Unix seconds -> [y,m,d,h,mi,s]) and iso8601
- crates/pdf/src/file_info.rs: unix (date -> seconds) and in_month, used by the
  PDF/XMP date parsers (pdf_date, xmp_date)
Output formats and parsing semantics are unchanged and pinned by the existing
tests (PDF D:... and XMP dates, offsets, leap-year rejection, year clamp 0-9999,
pre-epoch clamp). chrono uses only std, so wasm stays safe. AGENTS.md/CLAUDE.md
now say time goes through chrono.

Follow-up to storytold/cadcraft#1785

* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed

chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked
inside the logger. timestamp now uses the fallible DateTime::from_timestamp
and reads such a clock (or a pre-epoch one) as the epoch instead. Years past
9999 are written with {:04} like before, because %Y would add a '+'. Tests
cover the out-of-range and 5-digit-year cases.

* Don't build an unrepresentable SystemTime in the far-future test

UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.

* time: preserve old out-of-range date conversion (no epoch fallback)

`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any instant
past chrono's year range to 1970-01-01. Restore the old conversion domain with
a cycle-based helper that routes the calendar through chrono (the calendar
repeats every 400 years = 146_097 days, so month/day/time come from chrono and
only the year is carried in i128, keeping every i64 exact).

- apps/vectorcraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
  pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/doc/src/metadata.rs: `civil` cycle-based, keeping its clamp EXACTLY
  as the old code had it: the input is clamped to 0000-01-01..9999-12-31 first
  and the year is clamped to 0..=9999 afterwards. (With the input clamp the old
  `unwrap_or_default()` was already unreachable; this drops it and makes the
  conversion explicit, with identical output.)

Pinned old outputs in tests:
  logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch),
          10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
  metadata civil(i64::MIN)[0]=0, civil(i64::MAX)[0]=9999;
          iso8601(i64::MIN)=0000-01-01T00:00:00Z,
          iso8601(i64::MAX)=9999-12-31T23:59:59Z (year clamp, as before)

* Don't build a year-318857 SystemTime unchecked in the logging test

Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-21 19:36:32 -04:00
BFlat b15230bfb5 Merge pull request #3461 from lehni/update/canvas-partial-redraw
Canvas: an edit redraws only the area it changed
2026-10-22 05:09:38 +07:00
Jürg Lehni ede08590a0 Canvas: an edit redraws only the area it changed
Wide effect rasters cropped to the frame also serve parts of it.
2026-10-22 04:45:43 +07:00
bflatastic 92f295d2a2 Blend: Ease In / Ease Out and separate Color Ease in Blend Options (#1124)
Squashed from #2539 onto main.
2026-10-22 04:40:10 +07:00
bflatastic 34cde7c3db Character: Character Direction sets the selected characters left to right or right to left
Squashed from #3436 onto main.
2026-10-22 04:18:35 +07:00
bflatastic b45bb3dd54 Character panel Tate-chu-yoko marks vertical type as a horizontal block
Squashed from #3399 onto main.
2026-10-22 01:06:24 +07:00
bflatastic adbe0566a6 G4: System Layout spaces every text object in whole points
Squashed from #3412 onto main.
2026-10-22 00:54:43 +07:00
bflatastic 14f804cdee Type: ruby (ふりがな) for live text
Squashed from #3448 onto main.
2026-10-22 00:10:56 +07:00
dotCooCoo 4d1581f3da Document Info: a Repeat Objects category lists the document's repeats
Document Info had no Repeat Objects category, and document.info
refused category "repeats". The reference app's Document Info panel
menu lists Repeat Objects after Font Details.

Document Info now has a Repeat Objects category after Embedded Images.
It lists each radial, grid and mirror Repeat object by name with its
kind and the number of instances it makes. For a grid it gives the
rows and columns instead, followed by the number of instances it
draws when that is fewer. The count comes from the new
RepeatSpec::instances, which gives the number of instances a repeat
draws. That number is smaller than its options give when repeats
nested in its source would pass the instance budget. With Selection
Only the category lists the Repeat objects in the selection.
document.info gives them as repeatObjects, the panel's flyout offers
the category, and the text report that Document Info › Save… and
File › Package write has a REPEAT OBJECTS section. The title has a row
in every catalog.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-22 00:01:36 +07:00
bflatastic c895f64063 Format: rustfmt after the batch 2026-10-21 23:38:03 +07:00
nrq6993-a11yandCursor 07a49d447d G4: Scale Text Attributes bakes transform scale into type sizes
After scaling groups, type.scaleTextAttributes matches panel sizes to what draws on canvas without leaving scale in the affine.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 23:38:03 +07:00
nrq6993-a11yandCursor 59534c1cbb Fix LiveCorners::style to report stored radii, not fitted draw sizes.
Star corners with a large uniform radius kept None in the panel because fitted draw radii differ per tip; tests and UI expect the value the user set.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 23:38:03 +07:00
nrq6993-a11yandCursor ab0ddf0eae G1: Live Corners relative rounding (#1144)
Store corner radii as fractions of each corner's max so rounding scales with the shape; expose via Corners dialog and object.setLiveShape relativeRadii.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 23:38:03 +07:00
bflatastic 58066428c6 Artboards: the Artboards panel locks an artboard and the art on it
Squashed from #2592 onto main.
2026-10-21 23:38:02 +07:00
bflatastic 398c6316d9 G4: Fractional Widths rounds a type object's advances to whole points
Squashed from #3411 onto main.
2026-10-21 23:16:04 +07:00
bflatastic bbd9f51096 G4: No Break keeps marked characters on one line
Squashed from #3398 onto main.
2026-10-21 23:15:27 +07:00
dotCooCoo 7d6eb3246e Type: Optical Margin Alignment hangs punctuation in right-to-left paragraphs
Optical Margin Alignment and Roman Hanging Punctuation left every
right-to-left paragraph of point and area type, and type on a path
whose first paragraph runs right to left, as it was: the quotation
marks, full stops and commas at the ends of its lines stayed inside
the margins.

A right-to-left line now hangs at the margins its alignment places it
against, as a left-to-right line does. Auto aligns a right-to-left
line at its right, so it hangs there. The line is read from right to
left: where it hangs at its left margin, a full stop and the quotation
mark after it that end the line hang together. In vertical type,
where the characters of a right-to-left paragraph run from the top
down, the pair is read from the top.

Type on a path whose first paragraph runs right to left hangs at its
brackets by the same rule. In horizontal type, Auto places it at the
end bracket, so it hangs there. On a closed path, its hang at the end
bracket gets the space between the end and start brackets before its
hang at the start bracket does, as in right-aligned type.

A justified right-to-left line of one word is spread between its
letters as they are drawn. The gap after each letter went to the
letter that follows it in the text, so the two leftmost letters
touched and the word ended one gap short of the right margin.

Docs: docs/mcp.md, docs/type-parity.md, docs/target-app-parity.md.
2026-10-21 23:15:07 +07:00
dotCooCoo 1267efbcf9 Use Preview Bounds: objects are measured with their geometry effects
With Use Preview Bounds on, the bounding box, the Transform panel,
Align, Distribute, Distribute Spacing and object.setBounds measured
each object without its geometry effects. A square with Offset Path
10 measured as the square, and type with Outline Object measured by
its layout box instead of its glyphs.

The new vectorcraft_effects::preview module measures objects as they
are drawn. A path is measured by its effected shape, with the
geometry effects of each fill and stroke that paints. Type, images,
live objects and symbol instances are measured by their reshaped art.
A group or layer with geometry or Pathfinder effects, or with a fill
or stroke of its own that has geometry effects or paints members with
geometry effects, is measured by its evaluated art. A blend, envelope
or repeat whose objects have geometry effects is measured by its
evaluated art, with those objects measured after their effects. Raster
effects are not measured. In one object, the art of at most 4,096 symbol
instances is measured with its geometry effects, and the other
instances are measured without them. With Use Preview Bounds off, the
measures are unchanged.

object.setBounds sizes objects with geometry effects by measuring
them after each scale it tries, so type with Outline Object set to
100 pt wide gets a glyph box 100 pt wide.

Selection::anchors_box is split out of Selection::anchor_bounds. The
doc comment of VectorcraftApp::selection_bounds said that it includes
effects; it now says that it leaves them out.
2026-10-21 17:11:08 +07:00
bflatastic dda52d0b20 Artboards: each artboard can have a background color on the canvas
Squashed from #2403 onto main.
2026-10-21 17:11:08 +07:00
HEOJUNFOandClaude Opus 5.5 764d04926e Pick placed images on their frame with Selection by Path Only (#3291)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 16:55:17 +07:00
dotCooCoo b0056a127d Type: Optical Margin Alignment hangs punctuation at the ends of type on a path
Optical Margin Alignment and Roman Hanging Punctuation hung punctuation
in point and area type only. Type on a path kept its first and last
glyphs inside its brackets with either option on.

When Optical Margin Alignment is on, or the first paragraph has Roman
Hanging Punctuation on, the glyphs at the ends of left-to-right type on
a path now hang outside its brackets by the same amounts, measured
along the path. The type hangs at the start bracket when its line is
placed there, hangs at the end bracket when the line is placed there,
and hangs at both when it is centered. Glyphs that hang past the end
bracket do not make the type overflow. On an open path a hang stops at
the path's end, and on a closed path the two hangs together take at
most the stretch between the end and start brackets. SVG export writes
type on a path that hangs as outlines, with a warning.

Docs: docs/mcp.md, docs/type-parity.md, docs/target-app-parity.md.
2026-10-21 16:46:23 +07:00
dotCooCoo 1cea163b11 Paragraph: Insert Kashidas justifies Arabic lines with kashidas
The reference app justifies Arabic text with kashidas, set per paragraph
with the Paragraph panel's Insert Kashidas list (None, Short, Medium,
Long, Stylistic). VectorCraft had no kashidas, so a justified Arabic
line was spaced out at its word spaces like a Latin one.

The Paragraph panel, with the Indic options shown, now has Insert
Kashidas. On a justified horizontal area type line that is shorter
than its measure, each Arabic word with joined letters gets one kashida
between its last pair of them, drawn as tatweels (U+0640) in the font
of the letter after it. The kashidas share the line's room equally,
each at most a quarter of an em (Short), half an em (Medium) or an em
(Long); a Stylistic kashida is a row of the font's own tatweels, up to
an em. The word spaces take the room the kashidas leave, so the line
keeps its width, and line breaks do not change. The letter after a
kashida advances over it, so the caret before that letter, a selection
of it and a click on it take the kashida in. None, the default, leaves
lines as they were.

In horizontal type that is not on a path, the caret in a letter drawn
with several glyphs (a letter with a vowel mark) is now placed by the
whole letter, as hit testing already measured it, instead of by its
first glyph.

text.setFormat takes kashida. The setting is saved with the paragraph
only when it is not None, paragraph styles carry it, and Reset Panel
sets it back to None.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-21 16:46:22 +07:00
dotCooCoo 6041bb3c62 Objects on Path: clipping paths and parts of blends, envelopes and repeats stay in place
object.objectsOnPath.attach took a path inside a compound path, blend,
envelope, repeat or Intertwine group, or a clipping path, as the path
and put it into the new Objects on Path group, so it was no longer a
part of its container. As objects, a blend's key path, a repeat's
source art, an envelope's content and a clipping path were put into
the new group the same way.
The Objects on Path tool highlighted a blend's key path or a clipping
path and attached the selection to it.

objects_on_path::held now finds the container that holds an object.
The command refuses such a path or object with a reason, and the tool
does not highlight it or attach to it. A member of a compound path
given as an object still attaches as its whole compound path. The four
new messages have rows in every complete-message catalog.

Docs: docs/mcp.md (Objects on Path).
2026-10-21 16:46:13 +07:00
Drew DibbleandClaude Opus 5.5 061e905724 G1: selection tools take a clipping path on its outline
A clip group answered a press only strictly inside its clipping path,
and there the art above the path came first in the hit test, so where
the clipped art extends past the mask's edge the clipping path's
segments could not be reached on the canvas.

With the new HitOptions::clip_outlines, which the Selection, Direct
Selection and Group Selection tools set
(ToolContext::selection_hit_options), a press within the selection
tolerance of a clipping path's outline, inside or just outside it,
hits the clipping path (HitKind::Outline) before the art it clips:
Direct Selection takes its segments and the Selection tool the clip
group. It applies to a clip group's clipping path and to a layer's
clipping mask, and only to one a click may hit (may_hit: visible,
editable, within reach). Inside the mask the art is hit as before.

The other tools, which act on what is drawn (the eyedropper, type,
gradients...), leave the option off, and hit_test_visible never
applies it, so they still take the art at the mask's edge rather
than the mask's empty appearance. Select Behind's one-pass walk takes
a layer mask's outline first too, so it lists what the repeated hit
test finds. Object Highlighting and a right-click's selection in the
canvas (selection_hit_at) take what those tools take; dropping a
swatch on art still hits what is drawn.

The reference app reshapes a clipping path with Direct Selection:
https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-clipping-masks.html

Tests in doc: a_clipping_path_is_hit_on_its_outline_before_its_art,
only_the_selection_tools_hit_a_clipping_path_on_its_outline and
select_behind_takes_a_layer_masks_outline_first; the random Select
Behind test now clips layers and sublayers and sets clip_outlines,
and fails without the Select Behind change. In tools,
a_press_on_a_clipping_path_takes_its_segment_over_the_art fails
without the change; select::a_press_on_a_masks_edge_selects_its_clip_group
fails when the Selection tool uses the plain options; the eyedropper
copies the art on the edge. Recorded in docs/ui-parity.md,
docs/gaps.md and docs/target-app-parity.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 15:42:37 +07:00
FractalEncrypt 240b0ca881 TRACE.B1: Exclude retained trace bitmaps from result bounds and highlights 2026-10-21 15:42:36 +07:00
dotCooCoo 7fbd215e97 Brushes: Select All Unused selects the brushes the document does not use
The reference app's Brushes panel menu has Select All Unused, which
selects the brushes that are not used in the document. VectorCraft's
Brushes panel menu had no such item, and no command listed the unused
brushes.

brush.unused (Select All Unused) is a query that lists, in library
order, the brushes no stroke paints with. Strokes in the art and its
opacity masks, in symbols, graph designs, pattern tiles and graphic
styles count, and so do strokes in the art of a brush that is used.
The art of a brush that is not used does not count, so a brush whose
own art is the only art that paints with it is listed, and so is a
brush used only in the art of an unused brush. The Brushes panel
menu's Select All Unused selects these brushes, and Delete Brush and
Duplicate Brush then act on them.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-21 15:42:36 +07:00
MarekSpodBiedry 92a9b91255 Keep the swatch link on gradient stops resampled for a blend (#3234) 2026-10-21 15:42:35 +07:00
MarekSpodBiedry ecd01ad6f1 Keep the swatch link when blending two tints of one swatch (#3234) 2026-10-21 15:42:35 +07:00
NetterSilverandClaude Haiku 5.5 781d68a971 Encode trailing space as _x20_ in XML ids to keep round trip
Found while testing crates/doc/src/xml_id.rs:80 (strip_unique), triggered by from_object_name at :32

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 15:20:11 +07:00
Dylan M. Taylor cdc4ecc67e Merge pull request #3223 from Matoo125/fix/nested-repeat-budget
Repeat: nested Repeats stop at 250,000 instances in all
2026-10-20 12:17:16 -04:00
Dylan M. Taylor 0c42504069 Merge pull request #3194 from Matoo125/fix/mesh-remove-lines-malformed
Gradient meshes: deleting mesh lines leaves a malformed mesh as it is
2026-10-20 12:17:01 -04:00
Martin Vlach 03b2960f4d Repeat: nested Repeats stop at 250,000 instances in all
One Repeat makes at most 250,000 instances (a 500 x 500 grid; radial
counts stop at 1,000), but its source may hold another Repeat and every
instance draws that one again: expand() clones the inner Repeat once per
instance, and the renderer, the exporters (expand_deep) and hit testing
evaluate each copy. Nothing bounds the product, so a document under 1 KB
with a 500 x 500 grid of a 500 x 500 grid asks for 62.5 billion
instances: opening it freezes the app or runs it out of memory, and the
unsaved work in other documents is lost with it.

A Repeat now counts the instances the Repeats nested in its source make
in all and keeps only as many of its own as fit in 250,000, the budget
pattern fills already keep (at least one, so the source still shows).
A Repeat with nothing nested keeps all its instances, as before.

The test nests a 500 x 500 grid in a 500 x 500 grid and in a mirror,
counts the instances from the transforms (62.5 billion before) and
checks they stay within the budget, while one grid and a small nest keep
all of theirs.

Reported-by: BS.ehlas.cz
2026-10-20 14:23:50 +02:00
Martin Vlach da8e6af7e8 Gradient meshes: deleting mesh lines leaves a malformed mesh as it is
GradientMesh::remove_point_lines (the Mesh tool's Alt-click,
object.mesh.deletePoint) only checked that the clicked point exists,
then indexed the points above and below or left and right of it by the
mesh's rows and columns. A gradient mesh read from a document is not
checked against its rows and columns, so one with fewer points than
they need (rows 2, columns 1 and 3 points instead of 6) indexes past
the end and panics: the engine guard rolls the command back with an
internal error, and the web app, which has no guard, aborts.

It now returns false for a mesh that isn't valid, as locate (and with
it adding mesh lines), the outline and the mesh lines already do, and
the command reports that nothing could be deleted.

The test Alt-clicks an interior row's point and an interior column's
point of two such meshes and checks nothing is deleted and the mesh is
unchanged.

Reported-by: BS.ehlas.cz
2026-10-20 09:20:24 +02:00
Martin Vlach bf6692ae82 Gradient meshes: a grid whose point count overflows is malformed
GradientMesh::is_valid and the two mesh envelope checks (envelope_grid
and the envelope's surface) compare the number of points with
(rows + 1) × (cols + 1), multiplying the document's u32 rows and
columns unchecked. With u32::MAX rows and columns the product is 2^64:
debug builds panic on the overflow and release builds wrap it to 0, so
a mesh or a mesh envelope without points passes as valid. Selecting
it, Outline view, hit testing and export then index points that aren't
there (the mesh lines, outline and edges), tessellating it asks for an
impossible capacity, and a mesh envelope with handles to fill in loops
2^64 times making them until memory runs out. On wasm32, where usize
has 32 bits, 65,535 rows and columns are enough, and a panic there
aborts the web app.

The three checks now share grid_len, which counts the points with
checked arithmetic and gives None when the count doesn't fit in a
usize: a grid that can't be counted matches no point list, so it is
malformed like any other wrong count.

The test checks that a u32::MAX × u32::MAX mesh without points isn't
valid and has no lines, outline or quads, that envelope_grid refuses
those counts, and that a mesh envelope with them leaves art in place.

Reported-by: BS.ehlas.cz
2026-10-20 09:18:33 +02:00
Dan Green 7852f363a1 Fix #2143: selected ruler guides align to a key object
A lone object selected with ruler guides can be the key (the Selection
tool's click on it), and Align to the key moves the other selected
guides to its edge or centre: vertical guides to left, centre or right,
horizontal ones to top, centre or bottom. Parallel guides line up on a
key guide. Aligning to the selection or the artboard leaves guides alone.
2026-10-20 05:56:59 +07:00
Dan Green 263c36567f Fix #2143: a ruler guide can be the key object Align lines objects up on
A click with the Selection tool on a guide selected with objects makes
it the key (select.key {guide}), drawn thicker; a click on it again
cancels it. Align to Key Object is on with one object and a selected
guide and makes the guide the key. object.align lines the objects (or
selected anchors) up on the guide's line and leaves the guide where it
is: a vertical guide sets x only, a horizontal one y. The selection
keeps key_guide beside key, never both; it goes with the guide or the
objects, and follows the guide when earlier guides are deleted.
Distribute Spacing doesn't space from a key guide.
2026-10-20 05:56:58 +07:00
Martin Vlach 38e73060a7 Live shapes: a polygon read from a file has 3 to 1000 sides
Every command keeps a live polygon's side count in 3..=1000
(shape.polygon, object.setLiveShape, the Polygon tool), but reading a
document took `sides` as any u32. Selecting such a polygon builds its
Live Corners: LiveShape::polygon_outline makes one point per side and
set_corners resizes the corner radii and kinds to the side count. A
document a few bytes long ("sides": 4000000000) makes the app allocate
tens of gigabytes and abort or stall, and the unsaved work in other
documents is lost with it. Transforming such a polygon with cut
corners regenerates the same outline.

`sides` is now clamped to 3..=1000 when it is deserialized, so a
polygon from any document that is read (opened, placed, packaged or
taken from a library) keeps the limit the commands keep, and the code
that uses the count needs no check of its own.

The test reads polygons with 0, 5, 1000 and 4,000,000,000 sides,
checks they come out with 3, 5, 1000 and 1000, and that the clamped
one's outline has 1000 anchors.

Reported-by: BS.ehlas.cz
2026-10-20 05:28:32 +07:00
bflatastic a52198cefa Appearance: image fills and strokes draw the shape Convert to Shape or Outline Object gives them
Squashed from #2500 (its merges of main dropped).
2026-10-20 02:06:54 +07:00
Hamza 39b59b5730 Rulers: stop labelling an inch ruler at a sixteenth
Labelling to a hundred and twenty-eighth left the left ruler unreadable: it
spells a label one character per line, 8.5 pt apart, and at zoom 50 the step is
1/64 with labels 56.25 pt apart while "0.015625" needs 61.5 pt. At zoom 100 it
is "0.0078125" in the same space.

The label ladder stops at a sixteenth, as the report says Illustrator's does,
so zooming in past that leaves the labels where they are and only the ticks
between them grow denser -- down to a hundred and twenty-eighth, which is the
32nds, 64ths and 128ths the report expects to appear unlabelled. Every label is
at most the six characters of "0.0625", which fits the 50 pt the ladder leaves.
2026-10-19 23:47:04 +07:00
Hamza 2c6a2ad082 Rulers: an inch is divided in half, not in ten
Zooming in on an inch ruler stepped through 0.2, 0.1 and 0.05 of an inch:
`Unit::ruler_step` picked from one decimal ladder for every unit, and the
rulers divided whatever it returned by ten. A tenth of an inch is not a mark an
inch ruler carries (#2827).

Below an inch the steps are binary fractions now, 1/2 down to 1/128, and
`Unit::ruler_minor` gives the unlabelled ticks between two labels: an eighth of
the step on an inch ruler, so every tick is a binary fraction of an inch and
they stay about as dense as the tenths elsewhere. An inch and wider keeps the
shared ladder, and the metric units, points, pixels and picas keep their
tenths. Ruler-tick snapping follows the ticks that are drawn, which moves an
inch document's Shift-drag of a guide onto the same grid.

`ruler_label` took its decimals from `(-step.log10()).ceil()`, which writes
1/16 as "0.06" and 1/8 as "0.13" -- wrong values, not just rounded ones. It
uses the fewest decimals that write the step exactly, and drops trailing zeros
as `Unit::number` does, so a sixteenth-inch ruler labels the inch `1` rather
than spelling `1.0000` down six lines of the vertical ruler.
2026-10-19 23:47:03 +07:00
Hamza 7bc7025a62 Exports: say that an encrypted PDF still differs, and test the fallback
Two review points on #2880.

The reproducibility claim needed qualifying. PDF encryption draws its AES
initialisation vectors and the file `/ID` from a random seed, which
`SOURCE_DATE_EPOCH` deliberately does not reach, so a PDF exported with a
password never repeats however the variable is set. The docs and the ROADMAP
line say so now.

The byte-identity test passes an explicit `created`, so it never exercised the
clock-or-variable fallback. `pinned_or` takes the environment and the clock as
arguments, and the new test shows the variable is preferred, that a blank,
negative or non-numeric value leaves it to the clock, and that a host with
neither has no date to record. The environment is injected rather than set: a
test that changes this process's environment is `unsafe` under edition 2024 and
would reach every other test running beside it, which is why `cursor_theme`
takes a lookup closure too.
2026-10-19 22:19:51 +07:00
NetterSilverandClaude Haiku 5.5 3eed3be8e6 Stop locked guide objects from blocking marquee selection
Fixes #3036

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:19:42 +07:00
dotCooCoo 219253f163 Objects on Path: attach objects to a path and spread them along it
VectorCraft had no Objects on Path. A click with the Objects on Path
tool attaches the selected objects to the path clicked. The tool is in
Edit Toolbar's Modify section and the Blend tool's flyout, and Object ›
Objects on Path › Attach and the context menu with two or more objects
selected choose it. The objects and the path go into an Objects on
Path group with the path at its bottom.

- object.objectsOnPath.attach orders the objects column by column from
  left to right and top to bottom in each column. The first one sits
  on the path's first anchor, the last one sits on the end of an open
  path, and the rest are spread evenly by length; a closed path takes
  them all the way round. Each object's attachment point (Pivot, one
  of nine, default the center of its bounds without strokes) sits on
  the path, and the object is turned to the path there. An object that
  was rotated by an angle other than a multiple of 90° is first turned
  to the nearest multiple of 90°. Rotate tilts every object by that
  many degrees counter-clockwise. Symbol instances are measured by
  their art. Guides, compound paths, type, paths without length and
  graphs are refused with a reason.
- After every edit the objects go back on the path: editing the path
  moves them, an object moved on its own goes back, and objects moved
  into or out of the group in the Layers panel join the end of the
  path or leave it. Scaling, shearing or reflecting the whole group
  keeps each object's scale, shear or reflection, and the objects are
  spread evenly along the transformed path again. Undo takes the path
  and the objects back in one step, and a copied group stays live on
  its own path.
- object.objectsOnPath.options changes Pivot and Rotate, and
  object.objectsOnPath.expand leaves a plain group. Both act on the
  groups that `ids` names, or on the groups of the selection when
  `ids` is not given.
- The native file keeps the group live; SVG, PDF and the other exports
  write plain art. The tool highlights the path under the pointer, has
  a cursor entry, and Escape during a press cancels the click.

Part of #2545

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-19 21:51:32 +07:00
Hamza 1c80b23745 Exports: SOURCE_DATE_EPOCH stands in for the clock, so output repeats
PNG, PDF, EPS, legacy `.ai`, packaged zips and saved `.vectorcraft` files
record the wall-clock time they were made in, so the same commands run twice
gave different bytes although the pixels matched (#2880):

  PDF   /CreationDate and /ModDate   D:20261018155816 vs D:20261018155817
  PNG   the `Creation Time` tEXt entry
  .vectorcraft  one byte of the stored date
  SVG   carries no time, and already repeated

`metadata::output_time` is the time a file records: `SOURCE_DATE_EPOCH` when it
holds a non-negative whole number of seconds, else the clock, as the
reproducible-builds specification defines it. A date a command is given
(`file.new {created}`, a save's `modified`) still wins over the variable, and a
blank, negative or non-numeric value is ignored so that an export is never
refused over the environment.

`metadata::now_unix` keeps the real clock and now has exactly two callers, both
of which must not be pinnable from the environment: the recovery heartbeat, and
the seed PDF encryption draws on.
2026-10-19 00:37:55 +07:00
EricKuckandClaude Fable 5.1 1b8e3d9ae1 Outer Glow grows its solid silhouette by a quarter raster pixel before the blur, as the glows AI files export measure
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 23:50:57 +07:00
NetterSilverandClaude Haiku 5.5 f2601137ab Resolve the "ft" unit suffix to Feet in Unit::named
Found while testing crates/doc/src/lib.rs:274 (Unit::named)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 20:12:47 +07:00