Commit Graph
120 Commits
Author SHA1 Message Date
bflatastic f774968b20 xtask: the wasm check compiles the web app; the web's SVG depth cap follows from its stack
`cargo xtask wasm` (part of `cargo xtask ci`) checked the wasm-safe crates but not
apps/vectorcraft-web, so web-only code was never compiled before a merge. It now runs
`cargo check --target wasm32-unknown-unknown -p vectorcraft-web` too (no Trunk, no network).

The SVG importer's cap on the web was 48, sized from native measurements. rustc links the web
build with a 1 MiB stack (`-z stack-size=1048576`; the app passes no linker arguments of its
own) and usvg uses about 15 kB a level: half the stack at that cost gives 34, set in one place
with the arithmetic.
2026-10-14 10:13:45 +07:00
bflatastic 0efcaeb558 Importers: a brush that draws itself by name no longer crashes
A scatter brush whose art is an instance of a symbol stroked with that brush, or any brush whose
art is painted with a pattern whose tile is stroked with it, drew itself without end: a stack
overflow in a hand-written native file. Brush pieces already strip the brushes of their own
strokes; now what they draw by name that would draw a brushed stroke (a symbol instance, or a
pattern paint, also through the symbols and patterns those use) is left out too, in one place
(`stroke_pieces`, which every renderer and exporter calls with the document).

Probed the rest of item 2 of the depth follow-up with hand-written hostile inputs, kept as tests:
graph designs of deep art and of a symbol holding itself, Affinity groups nested to and past
both of its reader's caps, DXF blocks inserting themselves, EMF/WMF saving states without end,
and plug-in manifests and output nested 30,000 to 100,000 deep. None of them overflowed.
2026-10-14 10:13:45 +07:00
Saad Abdullah ad165a5a00 fix(svg): memoize descendant selector matching to stop combinatorial backtracking (#1703) 2026-10-14 08:01:50 +07:00
bflatastic c10da25ce4 Importers: nesting is capped, so a deeply nested file can't overflow the stack 2026-10-14 07:37:52 +07:00
bflatastic 301a99d91c SVG import: nested :not() selectors are bounded 2026-10-14 05:16:03 +07:00
Gert Pellin 6f9595542d SVG import: no slice indexing when writing a new style attribute 2026-10-14 05:16:00 +07:00
Gert Pellin 5f8c4e9ab5 SVG import: style sheet rules with :not() selectors apply (#1557)
usvg's style sheets (simplecss) drop rules whose selector has a :not(), so
rect:not(.keep) { fill: green } left the rect as it was. The text pass's own
cascade now reads :not() of a list of compound selectors (counted as its most
specific argument), and the import pre-pass writes what such rules set on an
element into its style attribute, with the value that wins the cascade there,
so usvg reads what CSS would: against other rules, the style attribute and
!important, inherited from groups, display: none and CSS transforms included.
Selector lists are split at top-level commas only.
2026-10-14 05:15:59 +07:00
dotCooCoo d9bd9b134d Symbols: instances of imported symbols are measured by their art
Every symbol instance was measured as its transform applied to the
20 × 20 square around the origin, which symbols made in the app keep
their art in. The SVG and DXF importers keep symbol art as drawn, so
the box of an instance of an imported symbol did not match its art.
The SVG Star instance at x 20, y 30 draws from (20, 30) to (40, 50)
and was measured from (10, 20) to (30, 40). Align moved such an
instance by the wrong amount, clicks and marquees beside its art
selected it, and the canvas, exports and prints left it out when its
art reached the view or the page and the square did not.

An instance is now measured by its symbol's art, which
Document::symbol_boxes looks up in the document each time
(Node::geometric_bounds_with, visual_bounds_with and
reach_bounds_with). With Use Preview Bounds on, the strokes in the art
count at their own weight, as the canvas draws them, and
object.setBounds counts them at that weight when it sizes an instance
or an object that holds one. When strokes do not scale and a width or
height of 0 or more is smaller than what a path's stroke adds,
object.setBounds now gives the path no width or height, as it does
for an instance whose art has strokes; before, it mirrored the path.
With Use Preview Bounds on, object.setBounds now measures the margins
that strokes add object by object, the members of groups and the
objects in symbol instances' art included, and solves for the width,
height and reference point asked for when the width and height are 0
or more. Before, it measured the margins of the whole selection and
could miss the size or the reference point asked for when strokes did
not scale and the objects' strokes added different margins, and when
strokes scaled and the width and height were out of proportion, even
for one path. A clip group, a Shaper Group, a compound path, type, and
a group or instance with effects or an opacity mask of its own are each
measured as one object, and so is an instance in perspective or one
turned or slanted relative to the bounding box, other than by quarter
turns. When the selection holds more than 4,096 objects to measure this
way, each selected object is measured as one. Instances inside symbol
art are measured by their own symbol's art. An instance is measured as
the square when its symbol is missing, when its symbol or a symbol
nested in its art holds an instance of itself, and when symbols nest 32
deep in its art.

Align and Distribute, the Transform panel, the bounding box and the
selection outline, clicks, the marquee, document.inspect and
document.node, Fit to Artwork Bounds and Fit to Selected Art, canvas
culling, thumbnails, the SVG filter region of an instance with a
raster effect, raster effect images, Export Selection, the SVG of an
artboard, printing, and the PDF, EPS, EMF/WMF and DXF writers use the
new box. The renderer also culls an instance by the effects and
brushed strokes in its art, and it culls a blend, envelope or repeat
by the brush art of what it holds and the effects in the art of the
instances in it. It culls an instance with geometry effects of its own
by the effects in its art after those geometry effects reshape the art.
It measures the effects in a symbol's art once for each symbol and
linear part of the transform that places it. For symbols whose art
holds instances, it measures at most 4,096 placements a frame, and at
most 4,096 more for each pattern tile it rasterizes and each placed
document it draws. Past that, it estimates the reach from the symbol's
reach with no transform, which it measures once. When the drawn
document's symbols change, the renderer drops the cached bounds of the
instances and of the objects that hold them.

The PDF and EPS writers, the SVG of an artboard and Skip Blank
Artboards grow the box of an instance whose art holds brushed strokes
by 16 times the widest stroke (at least 1 pt) of the objects in that
art that have a brushed stroke, plus 8 pt, as the renderer grows the
cull bounds of a brushed path. They keep an instance whose brush art
reaches the page. The SVG of an artboard also keeps an object that
holds an instance when what the canvas paints for it, the effects in
the symbol's art and the instance's own included, reaches the artboard.

Not yet: other commands that measure objects, such as Select › All on
Active Artboard, Transform Each and Convert to Artboards, still measure
an instance by the square.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-14 04:56:30 +07:00
dotCooCoo 21a386a6d4 SVG import: the style cascade looks up each element's rules by id, class and type
The SVG import's style cascade tested every <style> rule against each
element it was asked about. The import asks about the objects outside
<defs> and the like when the file contains `display`, about each shape
when the file contains `non-scaling-stroke`, and about each <text>, its
spans and its ancestors. On a file with 2,000 class rules and 100,000
rectangles, `vectorcraft-cli info` took 3.54 s with one `display`
attribute in the file and 2.03 s without it.

The rules are now filed under the id of their selector's rightmost
compound, else under its first class, else under its type. An element
is tested against the rules filed under its id, its classes and its
type, and the rules filed under none of these, in source order. It
matches the same rules in the same order and gets the same
properties. The file with the `display` attribute now takes 2.10 s.

Docs: docs/target-app-parity.md.
2026-10-14 04:56:30 +07:00
bflatastic 04bb6d6f27 SVG import: an Inkscape page whose box can't be placed is skipped 2026-10-13 13:36:41 +07:00
Gert Pellin ef0ab7ba9d SVG import: Inkscape's pages become artboards
Inkscape 1.2+ saves a multi-page document as <inkscape:page> elements
in its <sodipodi:namedview>; VectorCraft opened it with one artboard
the size of the root, leaving the other pages' art outside any. Each
page with a size now becomes an artboard at its place (through the
viewBox, as the art is), named by its inkscape:label or "Artboard N".
Files without pages keep their one artboard.

Fixes #1601
2026-10-13 13:36:41 +07:00
Gert Pellin 546c81624a SVG import: an unsupported filter's warning names the filter and the object
A filter that can't become a live effect warned "filter on a group
ignored", even on a plain <rect>: usvg wraps every filtered element in
a group of its own, which the message named. The warning now names the
filter and its primitives and the element (by id, or "an object"), and
says it is drawn without it: "filter 'f' (feColorMatrix) on an object
isn't supported; drawn without it".

Fixes #1598
2026-10-13 13:36:40 +07:00
bflatastic db85d465c8 SVG import: flowed text with a region too large to place is left out with a warning 2026-10-13 10:52:37 +07:00
Gert Pellin e794c1ac63 SVG import: Inkscape flowed text (flowRoot) becomes area type
<flowRoot> (SVG 1.2 flowed text, which Inkscape writes for text typed
into a frame) was dropped without a word: usvg doesn't draw it. The
text pass now reads it as area type: the first <rect> of its
<flowRegion> is the frame (with the rect's transform), each <flowPara>
a paragraph (white space collapsed), styled through the same cascade
as <text>. Another kind of region leaves the text out with a warning.

Fixes #1568
2026-10-13 10:52:37 +07:00
bflatastic 5fe916a425 SVG import: percentage lengths in a symbol with a viewBox are of the viewBox (#1561) 2026-10-13 09:58:14 +07:00
Ayanprogrammer11 3af08505f8 Replace linear scan in CSS class dedup with a HashMap lookup
Export::class() deduplicated declaration blocks with
classes.iter().position(), an O(n) scan per style, making exports with
many distinct styles (e.g. large palettes) quadratic overall. Track the
1-based class number per declaration in a HashMap alongside the
first-use-ordered Vec, so each lookup is O(1) while output stays
byte-identical.

Also adds a regression test covering 256 distinct declarations with
recurrence of early styles, for both InternalCss and StyleEntities
styling.
2026-10-13 08:53:06 +07:00
Gert Pellin 2c01a42b35 SVG import: CSS transforms in CSS syntax move objects; the font shorthand styles type
usvg takes a `transform` CSS property as a presentation attribute that
overrides the element's own, but reads its value as SVG: CSS units
(`translate(100px, 0)`, `rotate(45deg)`) don't parse, and the object
was left with no transform at all. Before usvg runs, transform
declarations in `style` attributes and `<style>` rules are now written
in SVG syntax (lengths in px, angles in deg/rad/grad/turn, translateX/Y,
scaleX/Y and skew as SVG's functions); values that can't be are left.

The text pass's own cascade now expands the `font` shorthand into its
longhands, as usvg does for the rest of the drawing, so text styled
with `font: italic bold 30px …` keeps its size, weight and style.

Refs #1557
2026-10-13 08:53:06 +07:00
Gert Pellin 3239e8b97e SVG import: <textPath> follows basic shapes (circle, ellipse, rect, line, polyline, polygon)
A <textPath href="#id"> pointing at a basic shape imported as point type
with "textPath has no usable path", as only <path> was followed. SVG 2
allows any basic shape, and Inkscape writes one whenever text is put on
a circle. The shape now becomes its equivalent path as SVG 2 defines
it: circles and ellipses start at 3 o'clock and rounded rects after the
top-left corner, all clockwise; lines, polylines and polygons follow
their points. The shape's transform applies as for a <path>. Shapes
that render nothing (zero radius or size) still fall back to point
type with the warning.

Fixes #1527
2026-10-13 06:56:37 +07:00
EricKuck 5b413448d9 SVG export: knockout groups of opaque art draw in order, without masks 2026-10-12 23:06:34 +07:00
arhammusheer 5beac4f122 #1244: smaller objects: Node 352 -> 152 bytes, NodeKind 200 -> 80, and a cheaper undo estimate
Rarely set fields move into one optional boxed NodeExtras, and large NodeKind payloads (images,
live shapes, blends, envelopes) are boxed. Every object, and every undo step that copies it,
shrinks: a 50k-path move keeps ~24.5 MB per step instead of ~40 MB. Files stay byte-identical
(Node keeps its flat JSON through a borrowed Serialize and a from-wire Deserialize, no flatten),
and loading is slightly faster. Undo and redo reuse their step's size estimate instead of
walking the document again (8.3 -> 5.6 ms for a 50k-path move).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc

# Conflicts:
#	crates/engine/src/cmd/graph.rs
#	crates/svg/src/export.rs
2026-10-12 11:02:32 +07:00
sanyamsingh001andCodex 7a49c49ade SVG.1: preserve data attributes and validate export ranges
Co-authored-by: Codex <[email protected]>
2026-10-12 06:59:49 +07:00
arhammusheerandClaude Opus 5.5 0724aafbb6 #1172: share an object's appearance with its undo history until it changes
Every edit of an object copied its whole appearance (its fills, strokes and effects, about
540 bytes for a basic object) into the new node, though a move leaves it as it is. Over 45
whole-selection moves of 10,000 paths, undo history grew by 589 MB; now 349 MB (41 % less), and
a nudge or a drag of the selection takes 12 ms instead of 15.

- Node::appearance is a vectorcraft_doc::Shared<Appearance>: copy-on-write. A clone shares it,
  and the first change through DerefMut copies it (Arc::make_mut). Reads, writes, equality (also
  against a plain Appearance) and the saved form are those of the Appearance itself, so call
  sites read and change it as before. Sites that build or assign a whole appearance add .into(),
  and the few that need a plain copy take (*n.appearance).clone().
- Node::transform changes the appearance only when it has gradients to transform, and pattern
  paints are transformed only when there are some: a move no longer copies an appearance it
  doesn't change.

The workspace suite (5,301 tests) and clippy pass with the change; the doc and engine suites
pass again after the transform guard.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
2026-10-11 23:27:21 +07:00
arhammusheerandClaude Opus 5.5 7652439b40 #1172: Pathfinder Outline 19x, SVG export 1.5x and open 1.2x faster, same output
At 50,000 paths unless noted, against main:

- Pathfinder Outline (300 paths): 145 → 7.5 ms. Each segment was tested against every junction
  of the arrangement, and each chain against every chain kept so far. Now a segment tests only
  the junctions inside its control box (junctions are sorted by x; a point within reach of a
  curve is within reach of that box), with the same nearest-point test. Coincident chains are
  found through a grid on their midpoints, taking the first match in the same order a scan does.
- export SVG: 267 → 176 ms. Path data is written straight into one string (`push_num`, the
  same digits and trimming as `fmt_num`) instead of a string per number, point and segment.
- open .vectorcraft: 239 → 195 ms. The JSON was parsed once only to find where it ends:
  - A file with no NUL has no data after it.
  - Otherwise the marker (which holds the first NUL, as JSON can't) is taken when it directly
    follows the JSON's last byte, as the writer puts it.
  - Anything else takes the old parse.

`perf --deep` prints output hashes of an SVG export and a save → open → save round trip; main
and this branch print the same ones. pathfinder_pinned (Outline included) passes, and so do the
pathops, format and svg suites (345 tests).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
2026-10-11 23:26:53 +07:00
zakariayacineandClaude Fable 5.1 16af3cc3fa Type: Digits — Default, Arabic, Hindi and Farsi numeral systems
Adds the Character panel menu's Digits option (with the Middle Eastern /
Indic options, beside Paragraph Direction in the Paragraph panel) as a
character attribute: CharStyle::digits, saved only when set, with
text.setFormat {digits} and text.setRangeStyle {id, start, end, digits}
("default" | "arabic" | "hindi" | "farsi"; a bad value is an error).

Shaping draws every digit of the Western, Arabic-Indic (U+0660–U+0669)
and Eastern Arabic-Indic (U+06F0–U+06F9) sets in the chosen system,
whatever was typed, before font fallback, so a font without the
Arabic-Indic digits gets a fallback for them. The text, its clusters,
the caret, selection and search stay as typed, as with All Caps; SVG
export writes the digits as drawn.

Catalog rows for Digits, Arabic, Hindi and Farsi in the ten translated
languages. Docs: docs/mcp.md (Digits), type-parity.md (digit types
done; Character Direction, kashidas, composers and the split caret
still missing), target-app-parity.md and gaps.md.

Behaviour from the public documentation (a character-level option with
the values Arabic, Hindi and Farsi in the Character panel menu):
https://helpx.adobe.com/indesign/desktop/language-and-proofing/arabic-and-hebrew/choose-digit-types.html

Tests: doc (the mapping and serialization), text (glyphs of "page 3" in
Hindi/Farsi equal the typed Arabic-Indic digits' on the typed clusters;
skips without a font covering the three sets), engine (setFormat,
setRangeStyle, undo, error), svg (digits written as drawn), ui-egui
(the menu shows with the Indic options).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-11 22:18:01 +07:00
Biromedic f4035c98d1 Fix #1141: SVG import keeps a mask whose art is all hidden, so its object stays hidden
Mask art that is all visibility:hidden imports as nothing, and opacity_mask
then returned no mask: the object was drawn whole. An empty mask is what the
file means (black all over); the renderer and the PDF and SVG exports already
draw empty mask art by its clip and invert options.
2026-10-11 16:00:38 +07:00
imachusandClaude Opus 5.5 621e1a6640 Type: Proportional Metrics — full-width glyphs on the font's proportional widths, palt (#966)
A character attribute (Character panel menu with the East Asian options;
`text.setFormat` / `text.setRangeStyle {proportionalMetrics}`), saved only
when on. In horizontal type it applies `palt`; vertical type sets as before
until the vertical shaping change (`vpal`) follows.

Line-end Punctuation Half Width takes nothing more off the punctuation
`palt` re-spaced (the flush opening bracket, consecutive punctuation, the
line-end half width): the segment is shaped once more without `palt`, and
the glyphs whose advance differs are marked. Punctuation the font leaves
full width is trimmed as before. SVG writes `"palt" 1` with the
character's other features.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 07:00:35 +07:00
bflatastic a0438f73f9 #1010: SVGs with one absolute and one unitless root side import unstretched
A root such as width="96pt" height="96" took CSS pixels (0.75 pt) along x and points along y,
so a square came in 24 x 32 pt and the artboard 96 x 96 instead of 96 x 72; with a viewBox the
art was stretched too.

When either root side is absolute, both axes now take CSS pixels at 96 per inch (usvg already
parses at that resolution): the unitless or px side is the same CSS length it would be in a
browser. The root scale is one number.

The test opens width="96pt" with height 96, 96px and 72pt, and width 128 with height 72pt: a
96 x 72 artboard and a 24 x 24 square; with viewBox 0 0 32 32 a 72 x 72 square at x 12 (meet),
and with preserveAspectRatio="none" the whole artboard.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	ROADMAP.md
2026-10-11 03:51:43 +07:00
bflatastic 0497c9e46d #1007: Reverse Gradient mirrors off-centre midpoints
Reversal read the midpoints after Gradient::reverse had reordered the stops, so each stop
took the wrong segment's midpoint: red-to-blue with its midpoint at 20% reversed to a 50%
midpoint instead of 80%, moving the transition.

Gradient::reverse now mirrors each segment's midpoint with it, so a reversed gradient is the
same ramp the other way round; paint.editGradient's own (wrong) pass is gone, and the SVG
importer's reflect spread, which had a correct copy of the same mirroring, uses it too.

Tests: two- and three-stop gradients with off-centre midpoints sample the same reversed (t vs
1 - t) and come back after two reversals; paint.editGradient reverse turns a 0.2 midpoint into
0.8.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	ROADMAP.md
2026-10-11 03:51:31 +07:00
alpha-rock c36b6976e3 M9: add live Revolve with artboard rotation and lighting controls 2026-10-10 23:44:46 +07:00
bflatasticandClaude Opus 5.5 72c9cec0e0 Fix #847: draw Underline and Strikethrough
The styles were stored on the text and written to SVG's text-decoration,
but the type was drawn without the lines. vectorcraft_text::decorations
now gives each underlined or struck-through stretch of a run on a line
its bar, placed and sized from the font's own underline and strikeout
metrics (with fallbacks for fonts that have none). The canvas, Create
Outlines, PDF export (outlined and real text) and SVG's outlined text
draw them in the run's paint.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 06:03:04 +07:00
bflatasticandClaude Opus 5.5 83b51ab9c7 SVG export: name the unsupported pixel effects without a product name
The brands check keeps other products' names out of user-facing text; #694's
SVG warnings called Radial Blur, Smart Blur and Unsharp Mask "Photoshop-style
raster effects". They are "pixel effects" now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 13:43:49 +07:00
bflatasticandClaude Opus 5.5 1e7f5084d8 Part of ROADMAP gap 2: Radial Blur, Smart Blur and Unsharp Mask on a pixel-filter pipeline
Effect › Blur › Radial Blur (spin/zoom, draft/good/best) and Smart Blur
(Normal mode), Effect › Sharpen › Unsharp Mask: Photoshop-style filters in
vectorcraft_effects::pixel, run by the renderer on the object's pixels in
stack order with Gaussian Blur and Feather, cached while panning; lengths in
points, bounded work per pixel. PDF, Expand Appearance and SVG export write
the effected objects as images. Dialogs with preview (method/quality
dropdowns, fields in documented order), menu entries, translations, tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 13:30:31 +07:00
bflatasticandClaude Opus 5.5 a2e4206155 Part of #372: SVG import names the one layer from the root element
Other editors export a document with a single layer without a layer group:
the layer's name goes on the root <svg> (id, or data-name) and its art sits
directly inside. Such a file opened as "Layer 1"; now its one layer takes the
root's name. Files whose top-level groups are the layers are unchanged, and an
Inkscape root's generated id (with inkscape:version) isn't taken as a name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 09:00:24 +07:00
bflatasticandClaude Opus 5.5 d5e6be1565 Part of #372: SVG import keeps wrapped layers and round-trips sublayers
Top-level groups already opened as layers named from data-name, else their
id. Now that also holds when one more id-less group wraps them (a transform
on all the art, or a <switch>'s chosen group), and hidden objects inside a
<switch> are kept (hidden) instead of dropped.

Nested groups stay groups unless something marks them as layers: the SVG
export now writes data-vc-layer="sublayer" on layers inside layers, and
import makes such a group (or an Inkscape layer group) a sublayer of the
layer holding it, in that layer's colour. A marked group anywhere else
stays a group.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 07:36:21 +07:00
haven b843fe8af3 M7: inline symbols in text
Document symbols placed inline in a run of type (beyond Illustrator; like
InDesign inline anchored objects): TextRun.inline holds the symbol, scale and
shift; the run is one U+FFFC laid out as a glyph sized to the art, centred on
the cap height. text.insertInline and Type > Insert Inline Symbol; editing
keeps the graphic whole; canvas, SVG (<use>) and PDF draw the art; Create
Outlines turns it into a symbol instance.
2026-10-08 15:45:34 -07:00
haven f7761bf5bc M7: per-paragraph paragraph attributes
Each paragraph has its own alignment, indents, spacing, hyphenation, tab
stops and paragraph style (TextObject.paras; para stays paragraph 0's, so
old files and readers work). Layout, editing (split/merge), threads, the
Paragraph/Tabs/Paragraph Styles panels, ranged text.setStyle/setFormat/
tabs/paraStyle.apply, SVG export/import and the fuzzers follow.
2026-10-08 15:07:13 -07:00
haven d2bb95daab M7: area type vertical alignment
Area Type Options > Align: Top / Center / Bottom / Justify (AreaOptions.verticalAlign,
LayoutOptions.vertical_align). Each row/column cell is aligned on its own; rectangular
cells shift their lines, other frames and text wrap reflow until settled without
losing text. Justify shares the leftover space equally between line gaps. Area Type Options
shows Align and First Baseline as dropdowns (the parameter dialog's choices table).
2026-10-08 14:28:02 -07:00
bflatasticandClaude Opus 5.5 2a35c4a82f Fix #522: SVG round trips keep hidden objects and data-* attributes
An SVG used as an animation rig lost its hidden alternates and their
rotation pivots: the exporter left hidden objects out (only hidden layers
could be kept), and the importer dropped data-* attributes.

- SVG export: hiddenLayers (on by default for document.save, as before)
  now keeps hidden objects too, each written display="none" on its own
  element (path, image, text or group). Exports still leave them out
  unless asked; the CLI's convert asks, as a conversion keeps the
  document.
- An object's own data: ObjectAttributes.data, the data-* attributes in
  order (ours, data-name and data-vc-*, aside). The importer keeps them on
  the objects and layers they belong to (at most 64 per element), the
  exporter writes them back, the native format saves them (missing in
  older files), and object.setProps {data: {key: value|null}} sets or
  removes them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 03:36:08 +07:00
haven 6d56195eda Placed documents: the object, drawn exactly and kept as art by every exporter 2026-10-08 12:20:51 -07:00
bflatasticandClaude Opus 5.5 1d14f337db Fix #511: Live Corners on any path's corners (stars, polygons, pen paths)
Corner widgets only ever showed on live rectangles: CornerWidgets::of bailed
out unless the selection's live shape was LiveShape::Rectangle, and
object.setLiveShape only edited rectangle radii. A star is a plain path, so it
had no widgets with either selection tool.

- geom::corners: the corners of any path (anchors without handles between two
  straight sides) and the path with them cut round, inverted round or
  chamfered, each corner a circle tangent to both sides, its radius no larger
  than takes the cut halfway along its shorter side. Rectangles now cut their
  corners with it (bit-identical anchors and layout).
- doc::corners::LiveCorners/set_corners: one model for a live rectangle's,
  a live polygon's (new radii/kinds) and any other path's corners (new
  LiveShape::Path keeping the uncut outline, plain again once nothing is cut);
  radii scale with transforms (Scale Corners) and stay circular.
- object.setLiveShape takes anchor indices for `corners` on any path; polygons
  keep a shared radius as their sides change.
- Widgets: Direct Selection on any path, Selection on live rectangles and
  polygons; placed on each corner's bisector, the drag follows it, the red
  limit outline marks corners at their own limit; the angle preference and
  View > Hide Corner Widget apply. Corners dialog and the Properties and
  Transform panels' Corner Radius (now on polygons too) work on them.
- Tests for the star, the polygon and a pen path in geom, doc, tools, engine
  and ui-egui; docs and ROADMAP.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 23:05:41 +07:00
bflatasticandClaude Opus 5.5 44c7e64e1b Fix #442: alike circular corners on rectangles that aren't square
Live rectangles drew elliptical corners after an uneven scale in v0.4.0;
inconsistent on a rectangle that isn't square:

- doc: LiveShape::fold_scale moves an uneven scale left in `xf` (files
  saved before #291) into the size and radii; `transform` uses it, and
  `folded()`/`corner_style` give Live Corners document units.
- engine: object.setLiveShape folds that scale first, so setting a radius
  or kind (fields, dialog, widgets, MCP) gives circular corners on such
  shapes instead of keeping the ellipses.
- geom: max_corner_radius/fitted_corner_radius are the one limit every
  corner shares (half the shorter side).
- tools: corner widgets sit at the centres of the corners as drawn (a
  radius past the limit put them off the arc, further in along the long
  side), drag in document units on stretched shapes, and outline the
  edited corners in red at the limit, as the reference app does.
- svg: CSS export writes the drawn radii (CSS would draw a radius past the
  limit when its neighbours leave room).
- docs: mcp.md Live Corners, ROADMAP.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 14:34:37 +07:00
bflatasticandClaude Opus 5.5 1706e84829 Fix #429: Type on a path brackets, flip and Type on a Path Options that keep the type in place
Type on a path could only be repositioned by Type on a Path Options, whose menu item sent
`{start: 0}`, so OK threw the text to the start of its path; nothing showed or dragged its
brackets. Now, as in Illustrator:

- Type on a path flows between a start and an end bracket (`TextKind::OnPath { end }`, None:
  the end of the path or once round a closed one; `TextKind::path_span`). Flipping
  (`TextObject::flip_on_path`) reverses the path keeping a closed path's first anchor and swaps
  the brackets, so the type keeps its stretch of the path.
- Type on a Path Options gains Align to Path (ascender, descender, center, baseline) and
  Spacing (glyphs spaced as if set that far above the path: closed up round the outside of
  curves, opened up round the inside), laid out by the text engine.
- `type.pathOptions {start?, end?, flip?, effect?, alignToPath?, spacing?, ids?}` sets them in
  one undo step; with none of them it queries. The menu item opens the dialog with the selected
  type's Effect, Flip, Align to Path and Spacing (dropdowns for the choices) and leaves the
  brackets alone, so OK as opened changes nothing.
- The Selection and Direct Selection tools show selected type on a path's path and its start,
  centre and end brackets (`tools::pathtype`, with a bracket cursor): dragging the start or end
  bracket sets where the type runs, dragging the centre bracket slides it along the path and,
  across the path, flips it (Cmd/Ctrl held: it only slides). Each drag is one undo step.
- Arc-length measuring moved to `vectorcraft_geom::ArcPath` (layout, the tools and
  `text.createInPath` share it). SVG export outlines path type whose end bracket, alignment or
  spacing `<textPath>` can't express.
- docs/mcp.md documents the command and the drags; ROADMAP updated; dialog strings translated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 12:50:25 +07:00
bflatasticandClaude Opus 5.5 55543da0cb Fix #420: SVG import keeps clipped layers as layers with a clipping mask
A top-level <g id> becomes a layer only when it was plain, so an Inkscape
layer (or our own export of a layer with a clipping mask) carrying a
clip-path came in as a clip group, and a file whose layers were all
clipped to one frame opened as one "Layer 1" of clip groups with the
layer names lost.

A top-level group that does nothing but clip its art (no mask, filter,
opacity or blend) is now a layer too: its clip path becomes the layer's
clipping mask (the clipping path first, as Make Clipping Mask in the
Layers panel leaves it), a clip path clipped in turn nesting a clip
group inside the layer as before. The clip building is shared with clip
groups. Top-level text that joins a clipping layer (before or after it)
isn't in its clip: the mask then becomes a clip group inside the layer
and the text sits beside it. A clip path with no shape leaves the layer
empty.

Tests: the issue's Inkscape layers (names kept, each a clipping layer,
drawn as resvg draws the SVG), a nested clip on a layer, the export ->
import round trip of clipping layers, and text joining clipping layers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 10:29:38 +07:00
bflatasticandClaude Opus 5.5 ffed92aba5 Fix #396: SVG type opens at the size it draws at
SVG import kept a <text>'s nominal font-size and left the scale of the
transforms and viewBox above it in the type's transform, so the Character
panel showed one size and the canvas drew another (7 pt under scale(3)
drew at 21 pt).

The importer now moves that scale into the type: the scale across the
baseline (the mean scale for type on a path) multiplies the size,
leading, baseline shift, character stroke weights and dashes and the
text path, and the transform keeps only the rest (a stretch along the
baseline, a skew, a rotation or a reflection). The type draws exactly as
before. Paints resolve in the grown text space, non-scaling strokes and
the stroke under the characters are worked out before the fold, the
factor is rounded to 6 significant digits (usvg's transforms are single
precision) and never pushes sizes past the Character panel's
0.1–1296 pt (the rest stays in the transform).

Tests: a group's, the text's own and a viewBox scale; rotated and scaled
text equal to the same text written at twice the size; stretch and skew;
type on a scaled path; the size limits; gradients on scaled text; SVG
export → import of scaled type (stable over two round trips).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 07:18:34 +07:00
bflatasticandClaude Opus 5.5 b05c14f8eb Fix #381: round single corners with Direct Selection (per-corner Live Corners)
A live rectangle stored four corner radii but drew them all with the largest,
and the corner widgets always rounded all four corners, so one corner could not
be rounded on its own.

- geom: rectangle_with_corners draws each corner with its own radius and kind
  (Round, Inverted Round, Chamfer); a sharp corner is one anchor, a cut one two.
  Uniform corners keep the exact rounded-rectangle anchors.
- doc: LiveShape::Rectangle gains `kinds` (saved only when a corner isn't round;
  older files read as round), plus helpers mapping anchors to corners.
- engine: object.setLiveShape takes `corners` and `kind`; without `corners` it
  edits the Direct-Selected corners, else all four, and keeps the selected
  corners selected as their anchors change.
- tools: with Direct Selection, only the selected corners show widgets and a
  drag rounds just those; Alt-click cycles the corner kind; double-click opens
  the new Corners dialog (also `ui.corners`).
- ui: Properties/Transform radius fields show and set the selected corners;
  CSS export keeps per-corner radii and skips cut-in corners.
- docs: mcp.md (Live Corners), control-protocol.md (Corners dialog), ROADMAP.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 06:56:32 +07:00
bflatasticandClaude Opus 5.5 ab62b5f8e8 Fix #320: CMYK images stay CMYK from PDF import to PDF export
A PDF's DeviceCMYK image opened as RGB (the interpreter's pixels as a PNG),
so every export wrote it as DeviceRGB or ICC RGB, silently. Images now keep
their CMYK samples, as CMYK colours already did:

- Model: CMYK images are blobs holding CMYK samples, a four-component JPEG
  (Adobe's inverted samples) or a Deflate CMYK TIFF. vectorcraft-doc's cmyk
  module reads their ink amounts (ImageBlob::is_cmyk, ::cmyk, capped at 64
  megapixels) and writes the TIFF (ImageBlob::cmyk_tiff).
- PDF import: DeviceCMYK, CalCMYK and four-component ICC images keep their
  samples. A JPEG with Decode [1 0 1 0 1 0 1 0] is kept byte for byte; any
  other is decoded (8 or 16 bits, its Decode array applied) and stored as a
  CMYK TIFF. Masked CMYK images and 1/2/4-bit ones open in RGB with a warning.
  Placing a CMYK TIFF keeps it CMYK too.
- PDF export: CMYK images are written in CMYK (DeviceCMYK, or ICC-based with
  the CMYK profile when colours are tagged: PDF/X-3, PDF/X-4, PDF/A). A CMYK
  JPEG neither resampled nor recompressed passes through unchanged; others are
  resampled in CMYK and compressed as ZIP or as a CMYK JPEG (jpeg-encoder,
  which also replaces the ZIP fallback and its warning for images converted to
  CMYK). Output conversion treats them like CMYK colours: Preserve Numbers and
  PDF/X-1a keep their numbers, Convert to Destination converts them to another
  CMYK profile or to RGB through a 17^4 lookup table (cms::CmykLut, now also
  behind the renderer's ink table). A CMYK image that can't be decoded is
  reported in the warnings.
- Canvas: CMYK images show through the working CMYK profile, as CMYK fills
  do, and paint their own inks in ink planes (CMYK raster exports, CMYK
  blending). SVG export writes TIFF images as PNG.

Illustrator's Output › No Conversion keeps colour data as it is
(https://helpx.adobe.com/illustrator/using/pdf-options.html).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 06:04:07 +07:00
bflatasticandClaude Opus 5.5 0687d2d127 Type: Paragraph Direction instead of an Automatic alignment toggle; bidi only where needed (review of #300)
Illustrator sets a paragraph's direction (Left-to-Right / Right-to-Left
Paragraph Direction in the Paragraph panel, with the Middle Eastern
options) and keeps its alignment buttons physical; it has no automatic
alignment toggle. So:

- ParaStyle.direction (None: from each paragraph's first strong
  character, as before; written only when set), the Paragraph panel's two
  direction buttons with Show Indic Options (original icons), and
  text.setFormat {direction: "auto"|"leftToRight"|"rightToLeft"}. The
  layout uses it as the base direction.
- The Automatic alignment check box is gone. Justify::Auto stays as the
  start of the paragraph's direction, set on new type only (text.create,
  the Type tools, with the mojikumi default in one new_type_para()):
  TextObject::point is Left again, so importers (PDF, SVG, DXF, EMF) keep
  their own alignment. The alignment buttons show what Auto resolves to.
- Bidi only where there is something right to left: plain paragraphs skip
  the algorithm (no per-layout allocation), it runs once per paragraph
  (shaping takes the paragraph's levels instead of recomputing them; path
  type computed it four times), and the shaper keeps Han, Hiragana,
  Katakana and Bopomofo in one segment (splitting at every change of
  script cost Japanese text a shaper call per kana-kanji switch).
- Vertical type keeps its logical order, so its glyphs and lines are not
  marked right to left (the caret no longer runs backwards down a column).
- PDF import puts Hebrew and Arabic, which PDFs (and VectorCraft's own
  export) draw in visual order, back in logical order with the direction
  that shows it as drawn; with bidi layout it came back mirrored.
- SVG import pins the direction SVG implies (left to right unless
  direction: rtl); SVG export also outlines right-to-left paragraphs.
- Docs (development.md), ROADMAP (folded into the Type entry, Type core
  row) and README updated; catalog rows for the new labels.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 05:25:01 +07:00
Daniel Efrat edc7c931dc Type: Hebrew and Arabic text laid out right to left (bidi), aligned by its direction
- Unicode bidi (unicode-bidi, unicode-script, unicode-general-category):
  each paragraph's direction from its first strong character, Hebrew and
  Arabic shaped right to left, embedded Latin and numbers keep theirs,
  lines wrapped in logical order and reordered per line by whole
  clusters; source text, undo and style ranges stay logical.
- Caret, hit testing, selection and the arrow keys follow the visual
  order.
- New text aligns automatically: right-to-left paragraphs right, others
  left (Justify::Auto, text.setStyle {justify: "auto"}); the Paragraph
  panel highlights the resolved alignment and has an Automatic alignment
  toggle. Explicit alignments, and older documents', are kept.
- SVG export outlines bidirectional text (with a warning) until it can
  be written as directional live text.
2026-10-08 05:24:44 +07:00
BFlatandClaude Opus 5.5 079f9a8b9a Fix #296: variable fonts list and draw every named style
The font database made one style per font file, so a variable font
such as Figtree[wght].ttf listed only its default instance (Light).
Faces now also yield their fvar named instances, each a style of the
family named by its subfamily name (the default instance is the face
itself; unnamed or duplicate instances are left out; axes and
instances read are capped). The system font scan reads the fvar table
beside the name table, so installed instances are listed before the
file is loaded and found by name and by PostScript name.

An instance face carries its axis settings: skrifa draws its outlines
and reads its metrics and advances at that location, harfrust shapes
with a ShaperInstance, so layout, the canvas, Create Outlines and hit
testing all use the instance. PDF export embeds it through krilla's
Font::new_variable, SVG @font-face through subsetter's
subset_with_variations: both write a static font of that instance.

Tests build a small variable font in code (test_fonts, via
write-fonts; no font file is committed) and check listing, resolving,
glyphs and advances, PDF and SVG export and damaged fvar/gvar tables;
Bahnschrift and Segoe UI Variable are checked where installed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 21:01:26 +07:00
midasdf 44b2b984ed i18n: add Japanese menus and initial vertical type tools 2026-10-06 16:55:47 +09:00