Commit Graph

144 Commits

Author SHA1 Message Date
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 c3c4185cef M8.9: Perspective grid presets: built-in views, Save Grid as Preset, presets manager
View > Perspective Grid > One/Two/Three Point Perspective list the built-in views
([1P/2P/3P-Normal View], plus Low and High views generated in code) and the saved
presets of each type; Save Grid as Preset... and Define Grid's Preset menu and Save
Preset... save the fields. Edit > Perspective Grid Presets... (previously a
mislabelled reset to the two-point grid) manages them: New, Edit, Delete, Import,
Export (.vcperspective), stored with the preferences. New commands
perspective.presets.list/save/delete/export/import; perspective.grid.preset takes
a preset name. Preset import is fuzzed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:51:12 +07:00
bflatastic 92cadd6083 M8.3: Liquify Tool Options dialogs, pen pressure and Brush Affects
Double-clicking a Liquify tool opens its Tool Options (dialog liquifyOptions):
the shared Global Brush Dimensions (Width, Height, Angle, Intensity, Use
Pressure Pen), the tool's options (Detail, Simplify with its checkbox, Twirl
Rate, Complexity, Brush Affects, Horizontal/Vertical) and Show Brush Size.
The canvas captures pen pressure from pen/touch force, ui.pointer and MCP
pointer events take `pressure`, and object.liquify takes [x, y, pressure].
Fixes: LiquifyParams::to_json now sends affectAnchors/In/Out; Alt-drag sizes
the brush from its current size (Shift keeps proportions) instead of
collapsing it. Hint-bar text for the seven tools. Registers the MCP Liquify
tests file added in M8.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:50:16 +07:00
bflatastic 67b2213957 M8.5: Puppet Warp pins rotate, multi-select and Select All; Control bar and hint
- Alt-dragging near (not on) a selected pin turns the art around it; the pin
  keeps that turn through later warps. arap::Pin gains an angle constraint and
  object.puppetWarp takes angles: [deg|null...].
- Shift-click adds pins to the selection or takes them out; dragging a pin
  moves every selected pin. Select All Pins selects them all (it used to
  deselect). Alt-click no longer deletes a pin; Delete removes the selection.
- Control bar: Expand (0 allowed), Show Mesh (on by default), Select All
  Pins. Hint-bar text for the tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:28:05 +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 ecf245b7c9 M8.8: Define Grid dialog prefilled from the grid, with viewing angle, units, scale, colours and opacity
View > Perspective Grid > Define Grid... opens a dedicated `perspectiveGrid` dialog
(ui.perspectiveGridDialog) prefilled from the current grid instead of a generic
form with fixed defaults that forced kind 2. The grid model gains (append-only)
name, viewing angle (the station point), units, scale, plane colours and opacity;
grids with a viewing angle foreshorten each axis as a true camera. New commands:
perspective.grid.get (read-only) and perspective.grid.define (one undo step,
only changed fields apply).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:40:26 +07:00
bflatastic 08108f693f M8.2: tool options last across tool switches and launches
Each tool keeps its options (a table in vectorcraft_tools::settings) when another
tool is chosen, across document switches, and in the saved preferences
(Prefs::tool_settings). The 7 Liquify tools share one set of Global Brush
Dimensions; the Symbolism tools share their brush. tool.setOption takes
{values} and a `tool` (another tool's options are stored for later), returns
the options, and works headless (MCP host command).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:23:44 +07:00
bflatastic f12428264c Fix #36: the font menus list the installed fonts, with a search field
Type > Font, the Character, Properties and Glyphs panels and Find Font list
every installed font by default, as the reference app does, instead of the
four bundled families. The family dropdowns get a search field that filters
the list as you type (Enter picks the first match) and open on the current
font. Show System Fonts, which no longer means anything, becomes Refresh Font
List (text.rescanFonts): it picks up fonts installed since the app started and
redraws type set in them without editing the document. text.fontList lets MCP
agents list the families and a family's styles.

Root cause: the font lists came from FontDb::families(), which only held the
bundled fonts until something scanned the system fonts (fixed with #130). The
Type > Font menu, rebuilt every frame, now builds its items once per font
change, and renderers lay type out again when the fonts change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:49:22 +07:00
bflatastic 285d94a3e6 Fix #122: Object > Vector Halftone
The issue asked for a vector halftone that turns art or an image into
vector dots, lines or shapes by tone, with angle, frequency, shape, mono
or CMYK separations and clipping to the object. Nothing like it existed.

object.vectorHalftone renders the selection offscreen, samples its tone
once per cell of a grid turned by the screen angle and places a shape
whose area follows the tone (circle, ellipse, square, diamond or line).
Mono screens follow lightness in one colour; CMYK screens follow the ink
amounts the CMYK output separates, one screen per ink at the classic
angles, multiplied over each other. Dots are clipped to the art's
outline as real geometry (only the dots crossing it go through a
boolean). It is a command rather than a live effect: the live pipeline
can't sample rendered tone when exporting. Its dialog previews live and
OK keeps it as one undo step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:42:26 +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 a844503e4c Fix #122: Mirror & Cut, Line Cut and Rectangle Cut tools
The issue asked for one-step symmetric shapes, splitting all selected art
along a line and cropping art to a rectangle as real geometry. Only the
freehand Knife and the Scissors existed, and neither works on compound
paths as one shape.

New commands path.mirrorCut, path.lineCut and path.rectCut cut the
selected paths and compound paths (inside groups too) with pathops
booleans against a half-plane or rectangle, and split open paths with
the Knife/Path Eraser helpers. Mirror & Cut reflects the kept half and
unites it with itself (open halves are joined on the axis). The draw2
piece helpers now rebuild compound paths too. Three tools (Eraser group,
original icons) preview the cut live while dragging and commit it as one
undo step; Mirror & Cut's axis and kept side are tool options shown in
the Control bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:42:26 +07:00
bflatastic 811ad1efd8 Fix #121: sandboxed WebAssembly plug-ins: object filters and live effects
VectorCraft had an Additional Plug-ins Folder preference but nothing loaded from it. Port
PhotoCraft's wasmi plug-in host (same sandbox, manifest and parameter schema) as the new
vectorcraft-plugins crate (L2), extended for vector documents:

- Object filters read the selected paths and compound paths as JSON (native PathData and
  Paint forms: geometry, fills, strokes, gradients, opacity) and return the objects to keep,
  change, add or delete; the output is validated in full and applied as one undo step
  (plugin.run). A failing plug-in leaves the document untouched.
- Live-effect plug-ins are geometry effects in the appearance stack (effect id
  plugin.<id>), re-evaluated (and cached) when the object or parameters change, editable,
  exported and baked by Expand Appearance; a missing plug-in leaves the geometry as is.
- plugin.list / info / run / install / remove / reload; the preference folder installs
  every *.wasm at startup and when it changes; File > Open of a .wasm installs it (web).
- Object > Plug-ins and Effect > Plug-ins menus with dialogs generated from the parameter
  schema (live preview), the Appearance panel's fx menu, docs/plugins.md, an original
  example plug-in (Desaturate, a Rust crate outside the workspace) and fuzzing of the
  plug-in boundary (engine/tests/plugin_fuzz.rs).

Tool plug-ins and paint-changing live effects are left for later (docs/plugins.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:19:41 +07:00
bflatastic 6992c843b4 Fix #40: W/H constrain-proportions link in the Properties panel and Control bar
The Transform panel had the link, but kept it in panel-local egui memory;
the Properties panel's Transform section and the Control bar had none, so
size changes there were never proportional.

The link is now the constrainProportions preference (prefs.get/prefs.set),
one toggle shared by the three places (transform::constrain_link), and every
W/H field passes it to object.setBounds as `proportional`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:08:34 +07:00
bflatastic 1499834de7 Fix #85: empty point type placed by a click is discarded when editing ends
Clicking with the Type tool runs text.create at once, and ending the edit
(Escape, another tool, a click elsewhere) only stopped editing, so every stray
click left an invisible empty text object and a "Type" history step.

The Type tool now remembers the point type its click placed and, when editing
ends with it still empty, runs the new text.discardEmpty command: it removes
the text and, when nothing else changed since the step that created it, drops
those steps so neither the document nor the history keeps a trace. A command
from outside the tool that takes the edited text out of the selection
(Deselect, a Layers panel pick) or removes it now ends the edit too
(Tool::after_command). Dragged area-type frames and type in or on a path keep
their frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:08:09 +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 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 1b9b37454b M4.80: Print Tiling tool and View > Show Print Tiling
View > Show Print Tiling (view.printTiling) draws the pages of the
document's print settings on the canvas, dashed and numbered, from
print.preview's own layout (print.tiling), so the canvas and the
printed job agree. The Print Tiling tool replaces the no-op tool: a
drag places the first page's imageable corner (print.tiling.set,
saved as print_setup.tileOrigin, one undo step); a double click on
the canvas or the tool button resets it. Tiles start at the origin
and extend to cover the art; the Print dialog shows the tool's
placement with a Reset button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:26:59 +07:00
bflatastic 0428e56342 M4.92: Print to a PostScript file
file.print {format: "postscript"} (the default for a .ps path) writes the
print job as Level 3 or 2 (level) PostScript instead of a PDF: DSC comments
with %%Pages the page count, one page per sheet with its paper size,
printer's marks in Registration, separations naming their ink (%%PlateColor)
with its halftone screen, a fixed flatness and film negatives. Transparency
is flattened first with flattenerPreset. The answer says which format was
written. In the app, a PostScript job is always saved, never sent to the
printer (the Print dialog's own options are unchanged).

The PDF print's layout is exposed as vectorcraft_pdf::plan (the PDF writer
uses it too); vectorcraft_eps::print draws its sheets with the EPS writer's
document walk, each sheet once however many copies print.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:01:36 +07:00
bflatastic 7c748a5e3c M4.79: print presets in the Print dialog and Edit > Print Presets
The Print dialog's Print Preset list loads [Default] or a saved preset
(reading [Custom] once the settings differ) and Save Preset... names the
settings as a preset. Edit > Print Presets... lists [Default] (generated,
protected) and the saved presets with how they differ from it; New... and
Edit... open the preset editor (the Print dialog's sections), Delete,
Import... and Export... use .vcprintpresets files, which File > Open also
imports. Commands print.presets.list/save/delete/import/export; the presets
live in Prefs.print_presets and persist with the preferences.

The open-dialog filter count test counts the new Print presets filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:30:46 +07:00
bflatastic ffce1cf686 M4.78: File > Print opens the Print dialog and prints through the system
The Print dialog (Ctrl/Cmd+P) edits the document's print settings in the
sections General, Marks and Bleed, Output, Graphics, Color Management,
Advanced and Summary, with a preview of each printed page under the section
list (drag the art to move its placement). It drives print.setup,
print.preview and file.print; the preview pages now carry their transform,
printed area and trim box. Print sends the job's PDF to the chosen printer
through the new PrintService (lp on macOS/Linux, the shell print verb on
Windows, a hidden frame and the browser's print dialog on the web); without
one, or with PDF File, it saves the PDF. Done only keeps the settings.
UI commands file.print, print.printers and print.printerSetup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:30:35 +07:00
bflatastic ed4eceb837 M4.96: Window › CSS Properties shows, copies and exports the CSS of the selection
The svg crate's new CSS writer (css_rules) gives the CSS web pages style objects
with: rectangles and ellipses as background-color / linear-/radial-gradient(),
border and border-radius; type as font, color, spacing and alignment; shadows,
glows and blur as box-shadow, text-shadow and filter. It shares SVG export's
declarations (transparency, fonts, type spacing, colours, gradient stops), which
moved into svg/src/css.rs. Options: units, absolute position, width and height,
unnamed objects, rasterize unsupported art (as a PNG background-image).

Commands css.selection, css.generate and css.export; UI commands css.copy and
css.exportFile; the CSS Properties panel with a highlighted code view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:03:11 +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 c6450f6f70 M4.90: Save for Web (Legacy) previews and saves optimised GIF, JPEG, PNG-8 and PNG-24 images, slices and HTML
File > Export > Save for Web (Legacy) (Ctrl/Cmd+Alt+Shift+S) opens a dialog with Original,
Optimized, 2-Up and 4-Up views showing each file's size and download time, and settings for
colour reduction, colours, dither, transparency and matte, interlacing, web snap, lossy GIF,
JPEG quality, the colour table (lock, map to transparent, web shift, sort), image size and
anti-aliasing, Clip to Artboard, sRGB, metadata, slices (all or selected), HTML output and
presets. Preview in Browser opens the HTML page.

Engine: document.exportForWeb, document.exportForWeb.preview (its byte count is the size of
the file Save writes), webExport.settings and webExport.presets.list/save/delete. Saved presets
and the remembered settings are stored in Prefs. Render: quantize_locked, and encode::web for
web snap, colour table edits, lossy GIF and GIF/JPEG comments. PNG files get an sRGB chunk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:24:51 +07:00
bflatastic bf4614669e M4.38: native and .ai save options: each artboard to its own file, linked files, ICC profiles, PDF content
- document.save / Save As / Save a Copy to a native or .ai file take
  separateArtboards + range (also write <name>-<artboard>.<ext> per artboard
  beside the master file, holding that artboard and the art touching it; the
  result lists every file), includeLinked (linked images keep their files' own
  pixels), embedProfiles (ICC profiles loaded from files that the document is
  tagged with; opening installs them where missing) and pdfCompatible (native:
  also carry a PDF of every artboard; .ai: false writes blank pages around the
  native document). .ai files also take compress (compression.compressText).
- File > Save As to a native or .ai file shows the options dialog (VectorCraft
  Options) after the save panel: Save each artboard to a separate file (All or a
  range), Include Linked Files, Embed ICC Profiles, Create PDF-Compatible File,
  Use Compression, Preview; the web's Save As dialog shows them too. Save, Save a
  Copy and Save as Template reuse the options as last saved.
- Format: optional "profiles" and "pdf" members (load_file, pdf_content),
  SaveOptions.include_linked/profiles/pdf; files without them are unchanged.
- Changed test: tests_aisave::save_as_ai_writes_a_pdf_that_reopens_editable now
  confirms the options dialog Save As .ai shows before writing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:24:19 +07:00
bflatastic 975067b44a M4.37: Data Recovery keeps copies of modified documents and offers them after a crash
- Every Preferences > File Handling > Data Recovery interval, modified documents
  changed since their last copy are written to the recovery store in the
  background (the same snapshot and encoder as Background Save; the status bar
  shows it). Complex documents (over 20,000 objects) are skipped while Turn off
  Data Recovery for complex documents is on.
- A copy goes when its document is saved, reverted, closed or undone back to its
  saved state, and when the app quits with nothing unsaved.
- Each running app keeps its copies in an area of its own: a sub-folder whose
  .lock file it keeps locked for its lifetime (File::try_lock), or on the web a
  heartbeat refreshed every minute. Only areas nobody holds (lock free, or no
  heartbeat for three intervals, at least three minutes) are offered, and an
  area is held while it is restored or discarded, so a second running instance
  never offers or deletes a live instance's copies. Empty leftover areas are
  tidied away.
- The next launch after a crash asks with the Recover Documents dialog (Restore /
  Discard / Cancel): restored documents open as "<name> [Recovered]", unsaved, and
  Save asks where to save them, suggesting their original file; the copy moves
  into the new app's area.
- Store: the recoveryFolder preference, else Data Recovery beside the desktop
  app's preferences; browser storage on the web (Services::recovery_store);
  tests use an in-memory store (with in-memory locks, or heartbeats).
- Engine: file.recovery.save, file.recovery.list, file.recovery.restore {file?},
  file.recovery.discard {file?}; DocState.recovered and DocState.recovery.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:24:16 +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 3d87b71ed4 M14.4: a batch that opens a new document is journaled
command.batch kept its steps in one interaction of the document active
when it began and journaled itself by committing that interaction. A
step that opened, closed or reverted a document broke both: the switch
cancelled the interaction (throwing away the steps so far), and the
commit then ran in the new document, which had none, so the batch was
never journaled and a replay was missing the new document. An error
after the switch rolled back only the active document.

- Each step runs inside an interaction of the document active when it
  starts; a switch leaves it open while a batch runs, so every document
  the batch edits gets one undo step when it succeeds.
- The batch journals itself, whichever document it ends in.
- On error everything rolls back: documents the batch opened close, the
  ones it closed or reverted come back (Session::batch_stash), each
  one's interaction is undone, and the active document and the
  Untitled-N count go back too.
- DocState::keep_interaction / undo_interaction share the undo-step and
  rollback code with commit_interaction / cancel_interaction.
- Tests: a batch that opens a new document is journaled, replays to the
  same documents and keeps one undo step in each; a failed batch closes
  the document it opened and restores the one it closed; a failed batch
  brings back a document it reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:22:36 +07:00
bflatastic 766b612070 M14.4: a batch journals the dates and preferences its steps used
Session::note_journal only kept values noted by the top-level command, so
a command run as a step of command.batch lost them: a save in a batch
stamped File Info's modified date from the clock without recording it,
and object.scale's strokes/corners came from the preferences again on
replay. Replaying such a journal a second later (or with other
preferences) gave a different document.

- Session::execute_step runs a batch step with its own journal notes
  (note_depth) and returns its params with what it noted; command.batch
  writes those params into its step of the batch's journal entry (steps
  that note nothing stay as given).
- journal_for_action strips the replay-only dates from a batch's steps
  too, so an action recorded with one still dates the document when it
  plays.
- Tests: a save in a batch records its date and a replay with the
  recorded date moved lands on it, while an action leaves it out; a scale
  in a batch records Scale Strokes & Effects and replays alike with the
  preference off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:05:09 +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 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 8d8ca11e50 M4.64: File > Package collects the document, its linked files and fonts with a report; Document Info categories and Save
File > Package (saved documents; unsaved changes are saved first) writes
<folder>/<name>/<document>.vectorcraft, the linked files in Links/ (relinked in the
packaged copy), the fonts its type uses in Fonts/ (leaving out fonts whose licence
doesn't allow embedding) and <document> Report.txt, then offers Show Package. Without a
folder (the web, agents) the same files come back as a zip.

Document Info's flyout picks one category (document, objects, styles, spot colours,
patterns, gradients, symbols, fonts, font details, linked and embedded images) and Save…
writes the text report Package shares.

Engine: file.package {folder?, name?, copyLinks?, linksFolder?, relink?, copyFonts?,
report?}; document.info gains category, format: text and sections. Text: FontFace
path, fs_type and embeddable. UI: ui.packageDialog (dialog `package`), file.showPackage,
docInfo.save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:40:05 +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 db0a7ffd8b M4.36: Background Save and Export write a snapshot off the UI thread with status bar progress
- With Preferences > File Handling > Enable Background Save / Export on, File >
  Save, Save As, Save a Copy and exports encode and write a snapshot of the
  document on a worker thread; the call returns at once ({path, background}).
- The status bar shows a spinner and what is being written; ui.inspect lists
  the running jobs under `background`.
- The document takes the path and counts as saved as of the snapshot once the
  file is written: edits made meanwhile keep it modified.
- Quit and Close wait for running saves; the Save Changes dialog saves at once.
- The host gives a thread-safe writer (Services::write_shared, atomic on the
  desktop); without one (web, tests) everything runs at once as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:27:06 +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 ffc60d5ff8 M4.59: pattern strokes and freeform gradients keep their look in PDF
Pattern strokes are written as the pattern's tiles clipped to the stroke's
outline (dashes, caps, profile, arrowheads and alignment included) instead
of mid-grey; freeform gradients on fills, strokes and type as an image of
their colour field at the document raster effects resolution, clipped to
what they paint, instead of their average colour. Both read back within
2% of the canvas at 150 ppi with no warning. Text strokes are covered for
cap, miter limit, dash array and alignment; knockout groups keep their
soft-masked form with a warning. The freeform box and grid sampling are
shared with the renderer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:52:35 +07:00
bflatastic e967c31be6 M4.58: raster effects in PDF on every kind of object
PDF export now renders the shadows, glows, blurs and feathers of groups,
layers, type, images, symbol instances, live objects and single fills and
strokes (and those inside opacity masks, symbol definitions and pattern
tiles) as images at the document raster effects resolution, instead of
leaving them out with a warning. Shadows and outer glows go under the
untouched vector art; a path whose fills or strokes carry effects is
written as one piece per fill and stroke. Expand Appearance shares the
image helpers (moved to rasterfx.rs); effect images now cover what the
renderer paints (members' shadows included).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:52:34 +07:00
bflatastic 851ed15104 M4.55: a journal replay reproduces File Info's dates
File Info's dates come from the clock, so documents made or saved a second
apart differed: journal_replay_reproduces_document and
rich_fixture_satisfies_invariants_and_undoes_to_blank failed at random.
One approach for every clock-derived date: it is a command input that
defaults to now, and the journal records the value used.

- file.new takes created? and document.save / file.saveAs take modified?
  (Unix seconds, or null for none; default now), through date_param /
  clock_date on the one save path (save_with), so Save, Save As and the
  UI's save stamp alike; Save a Copy still leaves the dates.
- Their journal entries always record the date used, so replaying the
  journal makes the same document whenever it runs. Actions recorded from
  the journal (Session::journal_for_action) leave those dates out, so
  playing an action later dates the document then.
- Test fixtures date their documents fixtures::CREATED, and the MCP
  property test dates its local document as the headless one, so
  documents built at different moments compare equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:41:26 +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 70e2726321 M4.16: Template safety: Templates folder, "<name> template", New from Template
Save as Template suggests "<name> template.vctemplate" in the Templates folder
(the new templatesFolder preference; default Documents/VectorCraft Templates,
created by the desktop save panel) and never the source path. New from
Template is an engine command (file.newFromTemplate {path} or {name,
dataBase64}) that opens any readable file as a new Untitled-N document; in the
app, bytes from an agent go straight to the engine, otherwise the open dialog
starts in the Templates folder with template types first. .ait and template
files keep opening untitled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:07:15 +07:00
bflatastic 9fae8c56ec M4.15: Revert as an engine command with a confirmation
file.revert reads and decodes the saved file first: if that fails the
document is left as it is, still modified. Otherwise Session::replace_document
swaps the content in the same tab (same place, path, format and uid; history
and selection cleared; saved state), so every tab keeps its zoom. It runs only
for saved, modified documents. In the app, Revert asks first with the shared
confirm dialog, whose OK runs file.revert {confirmed: true} on the engine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:07:15 +07:00
bflatastic 515ce2ac94 M4.14: Save As format chooser, Save writes back its own format, Save a Copy
Save As offers VectorCraft, VectorCraft Template, PDF, SVG and SVGZ and asks
for the chosen format's options before writing: SVG Options for SVG/SVGZ, the
Save PDF dialog for PDF (its OK finishes the save); on the web a Save As
dialog first names the file and picks the format. A document opened from or
saved as SVG/PDF remembers its format and options in one place
(DocState.format, save_options: the SVG options, or every Save PDF setting),
so Save writes it again; the result's warnings name what the format loses,
then the encoder's notes. Save a Copy writes "<name> copy.<ext>" and leaves
the path, title and modified state alone. Save is off for a clean saved
document, and Save as PDF... is in the File menu.

Engine: document.save, file.saveAs and file.saveCopy take {path?, format?,
options?, svg?} (SVG options also flat) and write linked images beside an
SVG; file.formatOptions reads a format's options. save_plan/save_with are the
one save path of every frontend, on the shared SVG encoder (one SVGZ path).
The template format is writable. Save panels get per-format filters, a folder
and a suggested name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:07:15 +07:00
bflatastic 0968e34e7e M4.57: PNG, PDF and text clipboard flavours in and out
Copy and Cut also offer other apps a PNG of the copied objects, a PDF
with On Copy: PDF, and plain text when every copied object is type
(clipboard.flavours; clipboard.exportPng {scale?}, exportPdf,
exportText). Paste outside text editing takes what another app copied:
SVG, then PDF (clipboard.importPdf), then plain text as point text
(clipboard.importText), then a bitmap embedded at the view centre
(clipboard.importImage {dataBase64, mime?, center?}); pasting our own
copy keeps the lossless internal clipboard, and Cmd+V pastes a bitmap
that comes with no text. Services gains system_clipboard
(SystemClipboard: write, holds_ours, read, has): on Windows every
format at once, on macOS and Linux one (the text, else the PNG); the
web keeps SVG text only. With When pasting text: Keep Plain Text, text
the Type tool copied pastes without its formatting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:52:01 +07:00
bflatastic 9aa29aa0fd M4.44: PDF presets: built-ins in code, saved presets, Edit > PDF Presets
Built-in presets are generated in code (VectorCraft Default, High
Quality Print, Press Quality, Smallest File Size and PDF/X-1a, X-3,
X-4, the latter listed as not supported yet) and read-only. Saved
presets live with the preferences (Prefs.pdf_presets) and work as
`preset` wherever PDF options are taken: document.exportPdf,
pdfSettings, export/serialize as PDF and .ai saves.

pdf.preset.list / save / delete / export / import manage them
(.vcpdfpresets files; opening one imports it). The Save PDF dialog's
Save Preset… saves its settings under a name; Edit > PDF Presets…
(ui.pdfPresetsDialog) lists the presets with each one's description and
differences from the default, with New…, Edit… (the Save PDF dialog as
a preset editor, ui.pdfPresetDialog), Delete, Import… and Export….
document.pdfSettings now lists what differs from the chosen preset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:40:22 +07:00
bflatastic 0ffaa427d1 M4.43: PDFs and .ai files reopen editable; Save As writes PDF-compatible .ai
Preserve Editing (on in the VectorCraft Default preset) embeds the
native document in the PDF as an embedded file with a hash of the pages
it was written with. document.open of a PDF, .ai or .ait restores it
exactly (restored: true) and falls back to importing the artwork, with a
first warning saying why, when the pages were changed elsewhere or the
data can't be read; SVG and PDF share that fallback and its warnings.
The legacy drawcraft payload name is read too. vectorcraft_pdf gains
editing(), ImportReport.native and PdfOptions.native.

document.save / file.saveAs / Save a Copy write .ai (format "ai" or an
.ai path): every artboard as a PDF that always carries the document and
takes the PDF options; a restored .ai keeps its path so Save writes .ai
again, and .ait templates open their document untitled. The desktop
save dialog lists the formats Save writes. Choosing a standard turns
Preserve Editing off (PDF/A refuses it), Export for Screens and PDFs of
some artboards leave it out with a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:34:15 +07:00
bflatastic 2724263d00 M4.54: Placing a text file asks for Text Import Options and sets it as area type
file.place of a .txt file takes `text: {characterSet: unicode|ansi, platform: windows|mac,
removeLineReturns, removeParagraphReturns, replaceSpaces}`: Unicode reads UTF-8 or UTF-16 by its
byte-order mark (other bytes as the platform's 8-bit set), ANSI reads Windows-1252 or Mac Roman;
CR LF, CR and LF all break lines; joining lines makes each block one paragraph, blank lines can
go, and runs of spaces can become tabs. The text becomes area type filling `rect`, the replaced
object's bounds, or the artboard less a 36 pt margin (type reflows rather than scaling), as one
undo step. The place cursor and file.place.info take text files too, and File → Place lists them.

In the app, placing a text file (the Place dialog, a drop, file.place without options) opens the
Text Import Options dialog. Its dropdown rows share a form helper with Document Setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:07:23 +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 ee2aa96dcd M4.22: pasting between documents carries the objects' resources
Copy keeps the copied objects with every document resource they use
(Session.clipboard: Clipboard {nodes, images, symbols, patterns,
swatches, graphic_styles, char_styles, para_styles, brushes,
source_artboard, source_layers, source_doc}), following symbols,
patterns, graphic styles and text styles into what they use. Every
edit.paste* brings them in within its undo step: identical resources are
reused, missing ones added, different ones of the same name added
renamed (an earlier paste's copy is reused) and the pasted objects
relinked, graphic style links by id. Global and spot swatches that
conflict ask Merge or Add (swatchConflict; clipboard.conflicts lists
them; the app's Swatch Conflict dialog asks one by one, with Apply to
All). Tint swatches, linked gradient stops, Lab spots and the target's
Spot Colors option are honoured; [Registration] is never copied.
Clipboard SVG exports them too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:45:34 +07:00
bflatastic 20e0935116 M4.24: File > Place as an engine command: images at their physical size, SVG/PDF/native as one group
file.place {path | name+dataBase64, link, template, replace, at, rect, page, crop} places a
file as one undo step without touching the clipboard. Raster images come in at 100% of the
resolution their file declares (PNG pHYs, JFIF/EXIF, TIFF, BMP; 72 ppi otherwise), linked to
their path by default; SVG becomes one group with its embedded images; a PDF/.ai page or a
native document's artboard becomes one clipped group. The images, symbols, patterns and
swatches the art uses join the document (renamed on conflict). Replace keeps the replaced
object's stacking place and transform; Template puts the art on a new locked template layer
below the current one. file.place.info and image.info report size, ppi and colour mode.

File > Place picks a file and opens the Place dialog (Link, Template, Replace); the web picks
through the inbox. The Control bar shows Linked File/Embedded, the file name, colour mode and
ppi of the selected image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:42:30 +07:00