Commit Graph

116 Commits

Author SHA1 Message Date
midasdf 44b2b984ed i18n: add Japanese menus and initial vertical type tools 2026-10-06 16:55:47 +09:00
bflatastic 695c541f0d M8.17: Edit envelopes on the canvas: Edit Contents, mesh points and handles, Control bar
- Edit Contents: while an envelope's contents are edited, clicks hit the
  content where it sits undistorted and the Selection tool selects it; the
  menu item reads Edit Envelope then. No default shortcut: the reference
  app's (Shift+Cmd+V) is Paste in Place here.
- A selected envelope shows its mesh (a warp's surface grid, a mesh's own,
  a top object's outline) instead of its content's outlines.
- Mesh envelope points get bezier handles (append-only; none stored is the
  smooth Catmull-Rom mesh, which the first handle edit fills in exactly
  along the grid lines). setMeshPoint takes `handle`; object.mesh.movePoint,
  addLine and deletePoint edit mesh envelopes too. The Mesh tool and Direct
  Selection drag points and the clicked point's handles (shared MeshEdit);
  the Mesh tool adds rows and columns inside mesh envelopes. Reset with
  Mesh keeps the shape along the grid with handles; Release returns the
  exact mesh.
- Control bar: Edit Envelope / Edit Contents, warp style, orientation,
  bend and distortions or mesh rows and columns, Reset, Envelope Options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:16:59 +07:00
bflatastic 8255d606ae M8.14: Type and symbols in perspective are really projected and stay editable; attachments survive copies
Type and symbol instances attached to a plane keep their art and carry a projection
(Node.perspective.projection, append-only); the canvas and every export draw their outlines
through it (reshape/bake), so they are foreshortened and stay type and symbol instances. Bounds,
hit testing and transforms follow the projection. Object › Perspective › Edit Text
(perspective.editText, or double-clicking with the Perspective Selection tool) shows the type flat
where it is drawn in isolation mode; exiting isolation projects it again. Release with
Perspective keeps their look. Copies, pastes, duplicates and Selection-tool moves keep the
attachment (the art moves as dragged). Flare, spiral and polar grid draw on the active plane, and
so do the click-to-size shape dialogs (perspective.draw `at`: sizes in plane units).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:10:00 +07:00
bflatastic 04fa49413c M8.16: Envelopes bend images, symbols, gradients, patterns and appearance; warps keep their frame
- Images distort as a raster mesh warp: cut by Fidelity into triangles,
  each a clipped image under the affine map of its corners (pieces overlap
  slightly, no seams; the renderer paints them without clip layers; SVG
  writes the pixels once and <use>s them).
- Symbol instances distort as their symbol's art (the renderer's cache
  follows edits to the symbol).
- Distort Appearance bakes strokes (filled outlines) and geometry effects
  before bending; Distort Linear Gradients bends linear gradients; Distort
  Pattern Fills bends the tiles. Opacity masks of the content bend too.
- Bug fix: a rotated, sheared or reflected warp envelope re-warped along
  the page axes. Envelopes now keep their own frame (append-only model
  field), so the warp turns with the art; move and scale keep it square.
- Live evaluation shares one set of hooks (vectorcraft_doc::live::Hooks,
  vectorcraft_effects::{expand_live, expanded_live_group,
  expand_live_deep}) between the canvas, Expand and every exporter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:01:47 +07:00
bflatastic 98a3e15ec3 M8.20: blends interpolate strokes, gradients, groups, type and symbols faithfully
Closed shapes start where they twist least (unless the Blend tool
picked the anchors); dashes, miter limit, arrowhead scale and width
profiles interpolate (caps, joins and brushes switch halfway);
gradients with different stop counts resample to common stops; groups
with different member counts grow the extra members out of a point;
compound steps stay compound paths; type, symbol instances and images
interpolate their transforms, type its colours and sizes run by run.
New blends are knockout groups and the canvas draws them knocked out
as the exports do. Expand and Release keep the blend's name,
transparency, opacity mask and appearance, and Object > Expand expands
blends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:01:28 +07:00
bflatastic 117aa0b5a3 M8.6: Puppet Warp pins appear at once, warp from the rest shape and follow Undo
- Pins show as soon as the tool has art selected: automatic ones at the
  centre and the middle of each limb's end (at least three), until pins are
  placed. They stay across tool switches while the selection is unchanged and
  a new selection starts afresh.
- The pins live in the document (Document.puppet, never saved) with the rest
  shape they started from: every warp starts from it, so warps don't stack and
  dragging a pin back restores the original; Undo/Redo take the pins back with
  the art. Adding and deleting pins are undo steps too.
- object.puppetWarp gains rest: true (the tool's pins); the new query
  object.puppetWarp.pins reads them. A click off the mesh adds nothing and
  says so (not-allowed cursor, canvas label); the command refuses such pins.
- The mesh grid covers the expanded art, so tiny art with a wide Expand no
  longer allocates millions of cells.
- Tests on groups, compound paths and open strokes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:16:10 +07:00
bflatastic 3d35b8518f M8.19: blend spine and keys editable on the canvas
A selected blend shows its spine. Direct Selection drags spine points
and handles, the Pen tool adds and deletes points on a selected
blend's spine (object.blend.spine.moveAnchor/addAnchor/removeAnchor).
The straight spine becomes a path on the first edit, its points pinned
to the key objects (append-only BlendSpec.key_anchors): moving a key
moves its end of the spine and the other way round; spines from older
files and Replace Spine still spread the keys evenly. Direct and Group
Selection pick blend keys on the canvas, edits redraw the blend live,
and Release leaves the spine as a path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:12:58 +07:00
bflatastic c5985b3f47 M8.18: Blend Options dialog and the Blend tool's anchor targeting
Blend Options is a dedicated dialog (Spacing dropdown with its value,
Align to Page / Align to Path buttons, live preview) opened on the
selected blend's own options, so OK no longer turns Smooth Color into
5 steps; with nothing selected it sets what new blends start with
(object.blend.info reads them). The Blend tool blends from clicked
anchor points (blend spec `starts`), adds keys to an existing blend
instead of nesting it, opens Blend Options on double-click, Alt-click
and its toolbar button, and has its own cursors and hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:01:18 +07:00
bflatastic a0847e8a25 M8.12: Perspective Selection tool scales, copies, moves perpendicular and nudges in perspective
The Perspective Selection tool shows the selection's bounding box in perspective; its handles
scale in plane space (perspective.transform; Shift proportional, Alt from the centre). Alt-drag
moves copies that stay attached, pressing 5 while dragging moves along the plane's normal
(ToolKey::Digit, delivered by the canvas and MCP press_key), and the arrow keys nudge in
perspective (perspective.nudge). Transform Again repeats the last perspective move or scale in
plane space. Attachments (plane and depth) now live on the objects (Node.perspective), so copies,
duplicates and pastes keep them; grids from older files hand theirs over. Homography moves to
vectorcraft-geom. Hint-bar text for the tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:00:15 +07:00
bflatastic fe6d6640fb M8.15: Envelope dialogs, Reset with Warp/Mesh, full Envelope Options
Make with Warp and Make with Mesh open dedicated dialogs (style dropdown,
orientation, bend and distortion sliders; rows and columns) with live
preview; while an envelope is selected the menu shows Reset with Warp /
Reset with Mesh instead, which switch the envelope's kind and keep its
content (Maintain Envelope Shape samples the current surface). Envelope
Options is a full dialog prefilled from the selection (Anti-Alias,
Preserve Shape, Fidelity, Distort Appearance, Distort Linear Gradients,
Distort Pattern Fills); with nothing selected it sets the defaults for new
envelopes. object.envelope.info reads the settings. Release returns the
warp or mesh shape as a gradient mesh and keeps opacity, blend mode and
mask; Expand keeps name, knockout, isolation and mask; Object > Expand
expands envelopes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:11:10 +07:00
bflatastic 9920156bb6 Fix #122: colour adjustment live effects (Effect > Color Adjustments)
The issue asked for non-destructive colour adjustments in the Effect menu
and the Appearance panel that work on vector paints, live type and
embedded images. Only the destructive Edit Colors commands existed, and
the live-effect pipeline only knew geometry and raster effects.

Effect > Color Adjustments adds six effects (adjust.*). They recolour,
in stack order, everything an object paints with: fills and strokes
(gradient stops too), type, gradient meshes, embedded images and a
group's members, each colour keeping its model. The Edit Colors walk over
an object's colours moved to vectorcraft_doc::recolor so the commands
and the live effects share it. The renderer draws adjusted art through
its object-effect path (recoloured images cached within a memory
budget), exports bake the adjusted colours (images as recoloured
copies), and Expand Appearance makes them permanent. The effect dialog
shows sliders, a channel menu and a draggable Curves graph, all previewed
live as one undo step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:42:26 +07:00
bflatastic 6780ffae60 Fix #33: position and size fields evaluate math typed before the unit suffix
A `*` or `/` operand had to be a bare number, so `1080/2 px` (the number
replaced by an expression, the field's suffix still there) read as nothing.
The unit after a factor or divisor now measures the whole expression, in
every unit (`Unit::parse`, shared by every unit field). Numeric fields
(num_field, opt_field, plain/mixed fields) select their whole text, unit
included, when they gain focus or are double-clicked, so typing replaces it;
unitless fields gain the same arithmetic (`parse_number`, `45*2°`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:08:10 +07:00
bflatastic 96604acffb Fix #39: rotated objects keep their angle and a rotated bounding box
Rotating an object baked the rotation into its geometry and kept nothing
else, so the bounding box, the handles and the Transform/Properties
panels fell back to the page-aligned box (the Rotate field went back to 0).

Root cause: nodes carried no record of their own axes. Each node now
keeps `bbox_angle` (append-only, written only when non-zero), which every
transform turns with the object (mirroring transforms keep the nearer of
the two possible boxes). The geometry is still baked. The selection's box
(`Document::oriented_bounds`) stands at the angle the selected objects
share. The Selection tool hit-tests and scales its handles in that frame,
and the canvas draws it rotated. `object.setBounds` measures W/H and the
reference point along it. `object.rotate {absolute}` turns the selection
to an angle, which the panels' Rotate field now shows and sets. Object >
Transform > Reset Bounding Box (previously a no-op) clears the angle in
one undo step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:49:36 +07:00
bflatastic a09cbe58c8 M4.83: overprinting fills and strokes overprint in PDF
Advanced > Overprint: Preserve (the default; document.exportPdf
advanced.overprint) writes each overprinting fill and stroke (an object's
own, a character's, a type object's) with a graphics state of /OP true
/op true /OPM 1 (/OPM 0 in PDF/A); Discard writes none. White ones knock out
when Document Setup's Discard White Overprint is on, as in print. Mask art
never overprints. File > Print's composite PDF honours Print > Advanced >
Overprints: Preserve the same way (separations keep multiplying plates).

Each overprinting fill or stroke is drawn as a form marked to overprint
(crates/pdf/src/forms.rs); after writing, its content starts by setting the
overprinting graphics state, and the PDF/X self-check runs on the result.
The engine's "overprint is not written to PDF yet" warning and print's
"overprints print as knockouts in composite output" warning are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:39:28 +07:00
bflatastic 1f922fcbbc M4.81: Print advanced options: overprints, flattener, printer profile, bitmap
The pdf crate's PrintSettings gains advanced {printAsBitmap,
overprints: preserve|discard|simulate, flattenerPreset} and
color.profile, all defaulting so saved print_setup values still load.
Composite output discards overprints (no /OP) or simulates them as
Overprint Preview draws them; separations keep them. A printer profile
converts composite colours with the chosen intent (a CMYK one writes
device CMYK) and separations separate with a CMYK one. The engine
flattens transparency with the preset, or renders each printed region
as one image for Print as Bitmap, before PDF or PostScript layout.
The Print dialog's Advanced and Color Management sections show them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:27:00 +07:00
bflatastic 6752b22783 M4.91: EPS and PostScript files open and place
document.open and file.place read .eps files and PostScript by any name
(PostScript .ai opens as artwork, .ait as a new untitled document). An EPS
file VectorCraft wrote restores the document it carries (restored: true);
other files run through a small clean-room PostScript interpreter (paths,
colours and spot inks, gsave/grestore, concat, line state, bind def,
procedures, loops and dictionaries, clips, shadings, images through the
decoding filters, type as point type), and fall back to their TIFF preview
with a warning when the program can't be read. Operation, nesting, memory
and path budgets keep hostile files from hanging or exhausting memory.

The clipping-group nesting the metafile reader used moves to
vectorcraft_doc::clipnest, shared by both readers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:51:54 +07:00
bflatastic 6757c7f9f3 M4.65: Asset Export panel and the Assets tab of Export for Screens
Window > Asset Export lists the document's assets as thumbnails: art
dragged in off the canvas (or added with +) becomes one asset per object,
or one of them all with Alt; Object > Collect for Export does the same.
Assets name their objects, so they follow edits (moving the art moves the
crop) and deleting the art removes it from its asset. Click selects,
double-click renames, the bin removes. The panel's export settings are
Export for Screens' own format rows on the document's shared settings, and
Export writes each asset alone, cropped to its art, into a picked folder
(the web downloads a zip). Export for Screens' Assets tab lists the assets
with checkboxes and exports the checked ones.

Commands: assets.add, assets.remove, assets.rename, assets.list,
assets.export, assets.settings.set; document.exportForScreens takes
assets. Model: Document.assets (ExportAsset {id, name, nodes}).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:24:54 +07:00
bflatastic b379f02527 M4.75: File → Print writes the job as a print-ready PDF: paper, scale, tiles, marks and bleed, separations
file.print {settings?, path?} lays the document out on paper and writes one PDF page per sheet:
copies (collated, reversed), artboards (all, a range, ignored), blank artboards skipped, paper
size and orientation (or turned per artboard), transverse, Print Layers (template layers never
print), placement on the imageable area, no scale / fit / custom / tiles of the paper or of the
imageable area with overlap and a tile range; printer's marks and bleed as PDF export draws
them; composite or separations (one grey plate per ink that prints, overprints honoured, spots
as process), emulsion down, negative; rendering intent and preserve numbers. print.preview lists
the pages, tiles, inks and warnings; print.setup saves the settings with the document
(Document.print_setup, one undo step).

PDF export and print share one page writer; the ink separation helpers move from the renderer to
vectorcraft_doc::inks and the range parser to vectorcraft_doc::range so print can use them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:13:10 +07:00
bflatastic c96bf40d18 M4.52: Export for Screens options: full document, bleed, sub-folders, sizes, presets, format settings
Artboards: All, a range or Full Document (one file per format: a PDF of
every artboard, other formats the bounds of all art), Include Bleed
(artboards grown by the document's bleed). Rows size files as a factor,
100w, 100h or 72ppi (suffixes @2x, @100w, @100h), and choose PNG, PNG 8,
JPG 100/80/50, WebP, GIF, SVG or PDF. Create Sub-folders (by size, by
format, or a row's own folder) and the presets Mobile 1x/2x/3x and
Density buckets (ldpi…xxxhdpi sub-folders). The gear opens Format
Settings: per-format options for every row of that format (the raster
and SVG options dialogs' rows, the PDF preset). Open Location after
Export shows the first file. The dialog has Artboards and Assets tabs.

The params of the last export are saved in the document
(Document.export_settings, not an undo step), read with
document.exportSettings, and the dialog reopens on them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 13:18:10 +07:00
bflatastic b0e39d0912 M4.72: PDF export draws printer's marks in Registration around each page
document.exportPdf {marks} now draws trim marks (Roman or Japanese, at the
trim mark weight and offset, outside the bleed), a registration target at the
middle of each side, colour bars (process inks, their overprints, the
document's spot inks and black tints) and the page information (title,
artboard, export date in UTC). Marks print in [Registration] (/Separation
/All), the MediaBox grows to hold them and the art is clipped to the
BleedBox. The geometry is the shared vectorcraft_doc::marks::PrinterMarks
(built on TrimMarks) so print can draw the same marks. The "not drawn yet"
warning is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 13:05:20 +07:00
bflatastic 8fb1733f05 M4.71: PDF pages carry the bleed and trim boxes; non-printing layers are left out
document.exportPdf applies the bleed (the document's with bleed.useDocument,
else the custom values): each page's TrimBox is its artboard and its BleedBox
and MediaBox the artboard grown by the bleed, so art in the bleed is kept.
Layers whose Print option is off are left out unless includeNonPrinting (new
setting, Save PDF › General) or createLayers is on. The bleed warning is gone;
the Marks and Bleeds section shows the document's bleed when it is used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 13:05:19 +07:00
bflatastic a51083733e M4.61: PDF import fidelity: opacity masks, isolate/knockout, pattern swatches, editable text, gradient meshes, stop opacity and Extend
Luminosity soft masks become opacity masks (alpha masks as a white copy of
their art; backdrop colour gives Clip, an inverting transfer function Invert;
the PDF export's backdrop and Difference covers read back as the options; a
constant alpha mask is opacity). Transparency groups keep their /I and /K
flags (read by walking the content as the interpreter does). Tiling patterns
become pattern swatches (one per colour for uncoloured ones). Text becomes
point type, one object per line with a run per font and paint, fonts named
from the file (document.open textAs: text | outlines). Patch and triangle
mesh shadings become gradient meshes; gradients drawn through their grey
opacity mask get stop opacity back; shadings that don't extend clip the art.
A rectangular clip around all its art (a form's box) is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 13:05:12 +07:00
bflatastic 271b0c6c25 M4.88: Object → Slice makes, edits and lays out user, object and auto slices
Document.slices holds user slices, Node.slice makes an object an object
slice that follows its bounds, and Document.slices_clip_to_artboard picks
the region auto slices fill (the artboards, or the art and the slices).
Object → Slice: Make, Release, Create from Guides, Create from Selection,
Duplicate, Combine, Divide… (dialog), Delete All, Slice Options… (dialog)
and Clip to Artboard; View → Hide/Lock Slices; Select → Object → Slices;
slice.list reads the numbered layout. The canvas draws the slices in the
Slices preferences' line colour with their numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:52:15 +07:00
bflatastic 2925e7f9b5 M4.63: Links panel with status, Relink, Go To, Update, Edit Original, Embed/Unembed, Link Info and Placement Options
The Links panel lists every image (thumbnail, name, page, missing/modified/embedded
badge) with Show filters, sorting and thumbnail sizes; its buttons and flyout relink
(to a file or a folder), go to, update, edit the original, embed and unembed, show in
folder and open Placement Options. Link Info shows format, colour space, location, ppi
and effective ppi, dimensions, scale, rotation, size and dates. Edit > Edit Original
and the Properties panel's image section reach the same commands.

Engine: links.list, links.goTo, links.embed, links.unembed, links.info and
links.placementOptions; links.relink and links.update now place the new file as the
image's Placement Options say (transforms, bounds, file dimensions, fit, fill, the
alignment point, clip to the old bounds). Model: ImageObject.placement.
UI: links.editOriginal, links.reveal, ui.placementOptionsDialog; Services.open_file
and pick_folder on the desktop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:40:05 +07:00
bflatastic 65cdc45ed9 M4.35: native files compress, save atomically, drop unused images, keep newer keys and save down to v1
- Format v3: optional gzip (Preferences > File Handling > Use Compression, or
  document.save {compress}); compressed files open by their magic bytes, legacy
  ones too.
- Every file write goes to a temporary file renamed over the target (engine
  writes and the desktop app's writer), so a failed save keeps the old file.
- Only the images the document uses are written.
- Document keys from newer versions are kept (Document.extra, flattened).
- Assigned colour profiles are a real field (Document.color_profiles),
  migrated from unknown.colorProfiles on load.
- document.save {version: 1|2} writes files older versions open; {preview}
  embeds a PNG of the first artboard of at most 256 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:27:02 +07:00
bflatastic 489bb84755 M4.17: File menu details: converted files, 30 recent files, one colour-mode command, Show in Folder, saved view
Older native files (.drawcraft or an older format version) open as
"<name> [Converted]" and Save asks for a .vectorcraft name instead of
overwriting them (preference appendConverted). Open Recent Files honours the
0-30 preference; empty slots are hidden (the native menu rebuilds when they,
saved views or User Defined libraries change). File > Document Color Mode is
one command, file.documentColorMode, converting through the colour settings;
choosing the current mode is not an edit, and object.convertDocumentColorMode
stays as an alias the command palette leaves out. New File > Show in Folder
(file.reveal; paths with spaces work on Windows). Native files store the view
they were saved with (Document.last_view, written at save time so it never
dirties the document) and reopen there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:11:19 +07:00
bflatastic 2e5ec7b251 M4.53: Linked images remember their file and keep a preview; missing links ask Replace or Ignore, modified ones follow Update Links
A linked image (`ImageObject.link`, now a LinkInfo: absolute path, path relative to the document,
size, modification time, hash, page; old path strings still load) keeps a low-resolution preview
(`ImageBlob.proxy`, at most 256 px a side) that a native save writes in place of the pixels of
images only linked images show. Opening a native document reads the linked files again, looking
for each at its path, then relative to the document and by name in its folder: missing files
show their preview, modified ones are read again only with Update Links: Automatically.
`document.open` reports `missingLinks`, `modifiedLinks` and `updatedLinks`.

links.check reports each link's status, links.update reads modified files again and
links.relink points images at another file or at same-named files in a folder (each keeps its
bounds; one undo step). In the app, a document opened without its linked files asks about each
(Replace…, Ignore, Apply to All), then offers to update modified links when the preference asks.
The renderer caches previews apart from the file's pixels; the Eyedropper samples previews in
the object's pixel space.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:07:10 +07:00
bflatastic 94f0f1b0d2 M4.56: Document Raster Effects Settings with colour model, background, anti-alias, clipping mask and room around
document.rasterEffectsSettings now also takes and reports colorModel,
background, antiAlias, clippingMask, addAround and preserveSpotColors in one
undo step (Document.raster_effects, RasterEffectsSettings, next to the
existing raster_effects_ppi so old files load unchanged). The raster effect
images of PDF export and Expand Appearance get the extra room and are
finished by the settings (hard edges, grayscale or bitmap, opaque on
white, white only under the art with a clipping mask). Rasterize takes
them as defaults (params override), and its clipping mask puts the image
in a clip group with the art's outline. The settings have their own dialog
(rasterEffectsSettings, ui.rasterEffectsSettingsDialog) and Rasterize…
opens its options prefilled from them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:55:48 +07:00
bflatastic 6a446b4fa3 M4.55: File Info with author, description, keyword chips, rating and copyright, written to SVG, PDF and PNG
file.info takes and reports the title, author, author title, description,
keywords, rating, copyright status, notice and URL in one undo step, plus
the read-only created (file.new) and modified (every save to a file) dates;
junk values are refused. Document.metadata (DocMetadata) round-trips
natively. PDF export writes author, subject and keywords to the info
dictionary and XMP, PNG export writes tEXt/iTXt chunks, SVG export writes
<desc> and, with the metadata option, every field as Dublin Core. The File
Info dialog (fileInfo, ui.fileInfoDialog) edits it with keyword chips and
star ratings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:55:47 +07:00
bflatastic f346727dd8 M4.23: paste placement, Paste Remembers Layers and the Paste menu's enable rule
Paste (also without formatting) goes to the centre of the view
(edit.paste {center}); Paste in Front/Back with nothing selected go to
the top/bottom of the current layer, in order; Paste on All Artboards
keeps the objects' offset to the artboard they were copied from.
layer.pasteRemembersLayers {on?} (document flag paste_remembers_layers,
saved, document.inspect) pastes objects back into the layers they came
from by name, making missing ones; the Layers panel now has its menu
with the layer commands and the check item. The Paste menu items are
enabled while the system clipboard holds SVG (looked at once per frame
at most, a few times a second).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:45:34 +07:00
bflatastic d527349f5a M4.20: Document Setup with bleed, transparency grid, paper and type options
File > Document Setup is a tabbed dialog over one engine command,
document.setup (no params: a query; any keys: one undo step;
document.setUnits stays as the units alias).

- General: units, bleed (four sides with a link toggle), Edit Artboards,
  Show Images in Outline Mode (greyscale pixels under the frame),
  Highlight Substituted Fonts/Glyphs (pink boxes on screen only).
- Transparency: grid size and colour presets or custom colours, Simulate
  Colored Paper, the flattener preset (built-in or saved) and Discard
  White Overprint (Overprint Preview keeps white overprints visible).
- Type: language with its quotes, Use Typographer's Quotes for the Type
  tool and Smart Punctuation, superscript/subscript size and position and
  small caps size (Character panel position/smallCaps; changing the
  setup restyles the text in the same undo step), text export (SVG text
  as text or outlines by default).
- The canvas draws a red bleed outline and the grid in the document's
  colours and cell size as one textured quad, also in rotated views; a
  white Background Contents exports raster formats opaque.

Model: Document.setup (DocSetup, defaulted, skipped when default) and
CharStyle position/small_caps; old files load unchanged.
tests_transparencygrid's helper now finds the grid by its texture (the
grid is one textured mesh instead of per-cell rectangles).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:28:16 +07:00
bflatastic 3b99df3b9a M14.5: Preferences > Units drive every length the UI shows and reads
The General unit is the active document's units: Preferences > Units >
General sets them (one undo step) and is the units new documents start
in (and the unit shown with no document open); Document Setup changes
only the document. The Transform panel, Properties, the Control bar, the
Info panel, rulers, the status bar, Align spacing, Tabs, paragraph
indents, Pattern Options, dialog distances (Move, shape sizes, path
dialogs, Artboard Options, effect distances, Transform Each, parameter
dialogs), New Document and the canvas measurement labels (smart-guide
sizes, moves, the Measure tool, the Live Corners radius) all follow it,
live. Units > Type drives font size, leading and baseline shift; Units >
Stroke also reaches the Appearance panel and the Magic Wand weight
tolerance. Typed units ("5 mm", "12 pt") work in any field; all ten
units are choices; length preferences show in their unit and accept a
unit; offset/simplify/grid/type-size params read typed units. Typed text
starting with a multi-byte character no longer panics the parser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:43:41 +07:00
bflatastic 0744347065 M14.4: tests main gained follow the no-panic lint rules
As the branch does for older tests: `panic!("…")` instead of
`unreachable!()` in test code (clippy.toml only allows unwrap, expect and
panic there), and the integration tests main added (CLI file I/O, render
gradient text and stroke geometry, SVG export suites) opt out of the
unwrap/expect/panic lints. No test logic changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:40:34 +07:00
bflatastic 1a5f7f6441 M14.4: no panics in the shipped code main gained since the no-panics branch
The M3/M4 work added ten panic sites the new workspace lints deny:
- doc: the lens/taper width profiles fall back to a uniform profile
  instead of `expect` (the presets are a constant table, covered by tests).
- engine: `FlattenOptions::default` builds the medium preset directly
  (`builtin`), `apply` skips an empty group instead of `expect`, the
  writable-format fallback reports an error instead of `expect`, and the
  new-art template's stroke comes back as an `Option` (it always has one)
  instead of `expect`.
- tools: the freeform gradient drag returns no actions for a grab it never
  starts (was `unreachable!`), and the gradient tool's release takes the
  gesture with `if let` instead of `expect`.

Behaviour on the success paths is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:39:18 +07:00
Brandon Thomas 3e323bfa2a Robustness: VectorCraft never crashes — no panics in shipped code
Shipped code no longer unwraps, expects or panics: every one of the 124
unwrap()/expect()/panic!/unreachable! sites outside tests now handles the
failure (Result/Option, ?, let-else, defaults) and reports errors to the user.

- Lints: workspace clippy denies unwrap_used, expect_used, panic, todo,
  unimplemented and unreachable; clippy.toml allows them in tests, and
  integration tests, examples and testkit opt out explicitly.
- Safety net (vectorcraft_engine::guard): a panic in a top-level command rolls
  the document back and returns EngineError::Internal; a panicking tool is
  reset; the MCP server answers with an internal error and keeps serving; the
  UI frame and each control request are guarded.
- APIs that hid failures now return them: Rendered::to_png/to_jpeg/to_webp
  return Result (JPEG/WebP errors used to write empty files),
  FontDb::face returns Option, Region::top returns Option.
- Fuzzing: engine/tests/import_fuzz.rs feeds garbage, hostile and mutated SVG
  and PDF through import → render → export (30k cases locally, no panics).
- Docs: AGENTS.md/CLAUDE.md rule, docs/development.md "Robustness" section,
  README pointer and a PR template checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:28:59 +07:00
bflatastic 025d758abe M3.83: gradients within, along or across a stroke
The Gradient panel's Stroke buttons now work: with the Stroke proxy on a
linear or radial gradient they lay it within the stroke (placed on the
page, as before), along the path (start to end of each subpath) or across
it (left edge to right edge; inside/outside strokes span their visible
side). `paint.editGradient {stroke: true, strokeMode}` sets it (new art
too with nothing selected); `document.inspect` reports
strokeOptions.gradientMode. Model: StrokeLayer.gradient_mode (default
Within, not written).

Shared geometry in effects::stroke::gradient: ribs on the bisectors of
the flattened path (fans round sharp corners, widened under arrowheads,
following width profiles) cut slices each painted with a linear piece of
the gradient (Gradient::span). The canvas and raster export clip them to
the stroke's outline and arrowheads, grown a pixel and composited with
copy so there are no seams and translucent stops paint once. SVG and PDF
write the same slices clipped to the outline, with a warning (new
vectorcraft_svg::export_with_report). Outline Stroke and Expand make
gradient meshes clipped to the outline. Mesh quads now grow along their
edges so thin quads show no seams. Dashes, profiles and arrowheads keep
working in both modes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:47:54 +07:00
bflatastic d8eff17b9f M3.98: radial annotator extent ellipse, aspect handle and off-centre focal point
Radial gradients show a dashed extent ellipse: its handle across the bar
sets the aspect ratio, the ellipse elsewhere rotates it, and the dot inside
the centre ring drags the focal point (GradientGeom.focal) for an off-centre
radial. paint.setGradientGeom takes aspect (%) and focal ([x,y] | null), and
start/end may be left out; the focal point keeps its place in the ellipse as
the vector, aspect and object transform. It renders as a two-point radial,
exports and imports as SVG fx/fy and PDF two-point radial shadings, expands
to off-centre rings, and travels in gradient params and the native format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:43:43 +07:00
bflatastic b8793eeaef M3.97: freeform points drag out to delete, spread ring drags, Lines mode draws lines
With the Gradient tool on a freeform gradient the selected point shows its
spread as a dashed ring with a handle; dragging either sets the spread. A
point dragged out of the object is removed on release, and double-clicking a
point opens the gradientStop popover, which now edits the selected freeform
point (colour, opacity, spread). In Lines mode successive clicks draw one
smooth line (with a rubber band to the pointer); clicking an existing point
continues the line from it, and Escape or a click off the art ends it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:43:43 +07:00
bflatastic b79262d4f4 M3.64: Width tool depth: Width Point Edit, multi-select, Alt-copy and discontinuous points
Double-clicking a width point opens Width Point Edit (Side 1, Side 2, Total Width, a link
keeping the sides equal, Adjust Adjoining Width Points, Delete; dialog `widthPoint`, also
`ui.widthPointEdit`). Shift-click selects several width points, which drag and widen together
as one undo step; Alt-dragging a point's centre copies it (`stroke.widthPoint.copy`); dropping a
point onto another makes a discontinuous point (two points at the same t), and the stroke's
width steps there. `stroke.widthPoint.set` takes `adjustAdjoining`, keeps a discontinuous point
when only its widths change, and `stroke.widthPoint.remove` takes `indices`. Flipping a profile
along the path swaps the sides of its steps; `document.inspect` reports `widthPoints`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:38:09 +07:00
bflatastic 9537eca5f7 M3.90: blend modes and opacity composite inks in CMYK documents
A CMYK document that shows transparency is drawn once per ink plane
(complemented C, M, Y; complemented K as a grey) so every blend mode,
opacity, group, knockout and effect composites inks, PDF-style; the
planes' inks are shown through the working CMYK profile (cached 4-D XYZ
table). Cyan multiplied over magenta now shows C100 M100. Opacity masks
keep screen luminance; RGB colours and placed images are separated
first. RGB documents and opaque CMYK documents keep the screen-colour
path.

PDF export of CMYK documents writes its transparency groups with
/CS /DeviceCMYK (luminosity mask groups stay RGB) and puts top-level
transparency in a non-isolated page-wide group so it blends in CMYK too.
Flatten Transparency composites CMYK documents ink by ink and makes CMYK
regions. Non-separable modes no longer give NaN for greys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:30:06 +07:00
bflatastic 5afc78a100 M3.66: Scale Strokes & Effects scales dashes and effect distances; Scale Corners works
Scaling with Scale Strokes & Effects on now scales stroke weights, dash
lengths and dash offsets and every distance parameter of object, fill and
stroke effects (groups and layers too), from new length metadata in the
effects catalogue. Off, nothing painted changes size and type keeps its
character strokes' weight. Scale Corners off keeps live corner radii.
object.scale/transform/transformEach take strokes?/corners? (default: the
preferences) and journal the values they used. The Scale dialog and a new
Transform Each dialog get both checkboxes, kept as the preferences on OK;
the panels' toggles go through prefs.set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:59:37 +07:00
bflatastic ea61d34a93 M3.85: spot colours defined in Lab, with Spot Colors options for Lab values or CMYK equivalents
Colours gain a CIE Lab model (`Color::Lab {l,a,b}`, serde tag `lab`), handled through the
CMS (display, separation, conversions, gamut), tints (toward paper white), keep_model, harmonies,
recolouring, blends, Info and the Color panel. Swatch Options gets a Lab mode
(`swatch.edit {mode: "lab"}` / `{color: {l,a,b}}`), and colour params accept `{l,a,b}`.

Swatches panel → Spot Colors… (`ui.spotColors`, dialog `spotColors`) runs `swatch.spotOptions
{useLab}` (Document.spot_use_lab, default on): Lab spot colours show and separate from their Lab
values, or art linked to them takes their working-CMYK equivalents, as one undo step. PDF export
writes Lab spot inks as Separation spaces with a Lab alternate (tint function from paper white),
or a DeviceCMYK one with CMYK equivalents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:46:57 +07:00
bflatastic c1df6f0621 M3.77: [Registration] swatch on every plate, trim marks on every artboard, Japanese marks and live Crop Marks
[Registration] is a built-in swatch that prints 100% on every plate: Separations
Preview shows it on each process and spot plate and PDF export writes it as
/Separation /All. It can't be edited, moved, duplicated, merged or deleted;
swatch.list shows it after None and the Swatches panel applies it as a swatch.
Object > Create Trim Marks strokes the marks in Registration and marks every
artboard when nothing is selected ({style?, allArtboards?}). The preference
japaneseCropMarks (General) switches to Japanese marks: double lines at the
trim and bleed edges plus centre marks. Effect > Crop Marks (cropMarks) is a
live effect whose marks follow the object; rendering, SVG/PDF export and
Expand Appearance draw them. The mark geometry lives in doc::marks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:46:04 +07:00
bflatastic 4591c85845 M3.76: Attributes panel with overprint, centre point, path direction, fill rule, image map, URL and note
Window > Attributes (Cmd+F11) shows Overprint Fill/Stroke, Show/Don't Show
Center, Reverse Path Direction Off/On, the non-zero / even-odd fill rule,
Image Map, a URL field with recent URLs and a Browser button, and a note.
New commands attributes.set and path.setFillRule; path.reverse takes ids and
reversed; attributes.info reports every value. Objects keep the attributes in
Node.attrs. SVG export wraps linked objects in <a>, and import reads links
back. The canvas draws the centre point of selected objects that show it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:46:04 +07:00
bflatastic 705976fd96 M3.84: Object > Expand dialog; gradient fills expand to a mesh or N objects
Object > Expand… opens a purpose-built dialog (Object, Fill and Stroke
checkboxes, disabled when the selection has nothing for them; Expand
Gradient To: Gradient Mesh or Specify N Objects, default 255). Every
option now does something:
- Object: type to outlines, live shapes to paths, effects baked.
- Stroke: strokes outlined into filled paths.
- Fill: linear/radial gradient fills become N solid rectangles or
  concentric ellipses, or a gradient mesh with lines where the colour
  changes, inside a clip group shaped like the object; freeform
  gradients become a mesh shaped like the object. Colours keep their
  stops' colour model. With Fill off gradient fills stay live.
object.expand {} defaults match the dialog (stroke now defaults on);
new query object.expand.info and UI command ui.expandDialog.
Create Gradient Mesh on a gradient-filled object takes the colours the
gradient paints at each point (GradientMesh::for_path_with, from_grid,
highlight).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:25:33 +07:00
bflatastic 3b2f46137a M3.89: a clipping path's fill paints behind the clipped art, its stroke over it
Paint given to a clipping path (paint commands with its id) now shows: its
fill paints inside the clip behind the clipped art and its stroke over the
art, unclipped, on the canvas and in SVG and PDF. Node::clip_paint splits the
clipping path into its fills and its strokes (type: its characters' too) for
all three outputs; the renderer caches the parts per clipping path. Clip group
visual bounds take in the stroke. Make Clipping Mask still leaves the clipping
path unpainted, as the reference app does; Ungroup no longer wipes a painted
clipping path's appearance (Release already kept it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:11:04 +07:00
bflatastic 874a287958 M3.80: graphic style libraries in the library panel, with .vcstyles save and load
Window > Graphic Style Libraries and the Graphic Styles panel's library button open
read-only libraries of original styles generated in code (Shadows and Glows, Outlines
and Rules, Hand-Drawn, Gradient Finishes, Shape Effects, Blends and Transparency) in
the shared library panel. Clicking a style adds it and applies it to the selection as
one undo step (Alt: on top). Save Graphic Style Library writes .vcstyles JSON (styles
unlinked from swatches, with the patterns they paint with) to the user library folder
(User Defined) or a file; Other Library loads one, or another document's styles.

Commands: graphicStyle.libraries, graphicStyle.library, graphicStyle.addFromLibrary,
graphicStyle.saveLibrary, graphicStyle.loadLibrary; UI: window.graphicStyleLibrary
(.other), ui.saveGraphicStyleLibrary, window.userGraphicStyleLibrary1-10. The user and
loaded library bookkeeping is shared with swatch libraries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:06:13 +07:00
bflatastic c1dc860f1d M3.63: visual bounds and clicks take in the whole stroke
Paths and compound paths now measure their visual bounds along their shape:
the body (inside/outside alignment, the width profile's widest point), the
tips of the miter spikes the limit keeps, projecting caps' corners and the
arrowheads' reach. A rectangle's right-angle miters stay within half the
weight (Fit to Selected Art, Rasterize and all-art SVG export get tighter,
exact boxes); Appearance::outset is the conservative reach for anything else.
Clicks hit what the strokes paint: outside strokes outside the path (0.8 x the
weight out), inside ones inside, the profile's width where clicked, caps,
spikes and heads. Click culling keeps a cheap box (hit test 0.34 ms per click
on the 50k-path budget). The subpath/tangent helpers move from the stroke
geometry into vectorcraft_geom::bez so both share them; a cross-check test
holds every painted point of the real stroke geometry inside the bounds and
hittable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:55:23 +07:00
bflatastic fdad11a99c M3.62: thirty more generated arrowheads with rendered previews
Arrowhead gains 30 original shapes generated in code, appended after the first
ten so saved names stay stable: Barbed, Half Arrow (left/right), Concave,
Double Arrow, Chevron, Feather, Swallowtail, Reverse Triangle, Triangle to Bar,
Kite, Leaf, Drop, Half Circle, Oval, Target, Tag (+ open), Diamond (open),
Hexagon (+ open), Star, Cross, Plus, Double Bar, Dot on Bar, Slash, Double
Slash, Bracket and Fork. Each is drawn in local units of the head's weight
inside the usual 4 x 4 box, with its own stop for the stroke under it.
Arrowhead::label() names them in the menus. The Stroke panel's arrowhead
dropdowns render every head as the canvas draws it (cached textures, a shared
widgets::doc_preview that the Brushes panel now uses too) in a scrolling list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:55:23 +07:00
bflatastic 3e73ad8581 M3.61: width profile library: Add to Profiles, Delete Profile, Reset Profiles
The Stroke panel's Profile list now holds saved profiles next to the built-ins,
kept with the preferences (Prefs.width_profiles). Add to Profiles… names the
selected stroke's variable width, Delete Profile removes the selected stroke's
saved one (built-ins can't be deleted), Reset Profiles removes them all.
Three more generated built-ins (Pinch, Teardrop, Wave). The dropdown (Stroke
panel and Control bar) lists saved profiles and draws a custom profile's own
silhouette. Commands: stroke.widthProfile.add/delete/reset/list; stroke.set
{profile} takes a built-in id or a saved name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:55:21 +07:00