Commit Graph

370 Commits

Author SHA1 Message Date
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 42be928367 M8.16: Enveloped type exports distorted, run by run, everywhere
Every exporter (SVG, PDF, EPS, EMF/WMF, DXF) evaluated live objects with no
text outliner, so type inside an envelope was written undistorted. They now
share vectorcraft_effects::expand_live_deep, which outlines type per run in
each run's paint, as the canvas now does too (it used the first run's paint
for the whole line).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:27:08 +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 5d21826fa8 M8.1: Delete/Backspace remove the selected width points and Puppet Warp pins
The Width and Puppet Warp tools now claim Delete and Backspace while they
have width points or pins selected, so the shortcut routing (desktop and MCP
press_key) removes those instead of clearing the artwork. With nothing of the
tool's selected the keys still clear the selected objects. Headless Backspace
clears the selection like the desktop app does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:26:55 +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
Brandon Thomas 2c02502f43
Merge pull request #176 from theironchef/fix/dxf-hatch-spline-edge
DXF import: a hatch spline edge without fit data keeps its loop
2026-10-05 21:51:30 -04:00
Brandon Thomas e35f75f733
Merge pull request #174 from theironchef/fix/regions-one-pass
Pathfinder: find all regions in a few contour passes, not one per mask
2026-10-05 21:51:26 -04:00
theironchef a4d93b1462 Lint: as_chunks in the font scan too
The system font scan reads collection offsets and table records with
chunks_exact; clippy 1.99 asks for as_chunks there as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9TrG4w49oSRYbP3cqxA3D
2026-10-05 12:17:22 -07:00
theironchef 719e648de1 Lint: as_chunks in two tests too (import_fuzz, render encode)
With the library hits fixed, clippy 1.99 reaches two more in test code.
cargo clippy --workspace --all-targets -- -D warnings now passes on 1.99.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9TrG4w49oSRYbP3cqxA3D
2026-10-05 12:14:56 -07:00
theironchef 945dcbae1d Lint: as_chunks for constant chunk sizes, as clippy 1.99 asks
Rust 1.99's clippy adds chunks_exact_to_as_chunks, and cargo clippy
--workspace --all-targets -D warnings (cargo xtask ci) failed on its eight
hits in metafile, pdf and eps. as_chunks gives fixed-size arrays, so the
loops read the same bytes without indexing into slices, and encrypt's
CBC loop takes the remainder from as_chunks instead of the iterator.
as_chunks is stable since 1.88.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9TrG4w49oSRYbP3cqxA3D
2026-10-05 12:14:56 -07:00
theironchef 86dbc42d42 DXF import: a hatch spline edge without fit data keeps its loop
A spline edge read an optional fit count (97) after its control
points. Files before DXF 2010 have no fit data there, so the 97 that
follows is the loop's count of source objects: the edge took it, then
failed reading fit points where the 330 handles are, and the whole
hatch was dropped without a warning. That is every associative hatch
bounded by a spline from AutoCAD R2000 to R2007.

A fit count is 0 or followed by fit points (11), and a source count
by handles (330), so the edge now only takes the 97 when it is 0 or
points follow.

Fixes #175.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9TrG4w49oSRYbP3cqxA3D
2026-10-05 12:14:39 -07:00
theironchef 9002f86f73 Pathfinder: find all regions in a few contour passes, not one per mask
regions() made one linesweeper contour pass per distinct coverage mask,
and each pass walks the whole arrangement, so finding the regions cost
masks x segments. Two crossing blends make thousands of both: the
Ribbons example's EPS export never finished, and Flatten Transparency,
the Flattener Preview, Divide, Shape Builder and Live Paint slowed down
the same way on dense art.

The masks now go into batches whose faces share no vertex (a greedy
colouring of the masks meeting at each vertex), one contour pass per
batch. A face's contours are walked as in its own mask's pass, since
no other face of the batch touches it; each contour's mask is the one
of its batch at its first point (a vertex of the arrangement), and
contours group under the nearest enclosing contour of the same mask,
as grouped() does in that mask's pass. The regions come out identical,
in the same order.

Fixes #173.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9TrG4w49oSRYbP3cqxA3D
2026-10-05 12:10:36 -07:00
BFlat fd99375b1b
Merge pull request #166 from theironchef/fix/layers-rename-commit
Layers: renaming a layer or object commits on Enter or a click elsewhere
2026-10-06 01:04:22 +07:00
BFlat f24198ec7e
Merge pull request #160 from theironchef/fix/undo-during-interaction
Edit: Undo during typing or a drag takes back only that, and Redo returns it
2026-10-06 01:04:16 +07:00
theironchef 8c8ac37300 Layers: an open rename stays with its document
Node ids are per document, so a rename left open while another document
came to the front renamed that document's node with the same id. The
panel drops an open rename when the document it shows changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9TrG4w49oSRYbP3cqxA3D
2026-10-05 10:58:54 -07:00
theironchef 11740c9731 Layers: renaming a layer or object commits on Enter or a click elsewhere
The name field asked for focus every frame, which took it back from
Enter, Escape or a click elsewhere, so lost_focus never fired and the
rename was never committed. It now asks only while opening, as the
Artboards panel's name field does.

Fixes #165

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9TrG4w49oSRYbP3cqxA3D
2026-10-05 10:51:46 -07:00
bflatastic c4b87e36b2 Fix #81: Chinese, Japanese and Korean names show in the UI, not boxes
Layer and object names, document tabs and any other UI text in scripts the
UI fonts lack now draw in an installed font that covers them, as the canvas
already did.

Root cause: egui only had the bundled UI fonts (Source Sans 3, JetBrains
Mono) and its defaults, none with CJK glyphs. After each frame the UI now
looks at the text it painted; the first time a character the UI fonts lack
appears, a thread finds an installed font covering it through the font
database (the canvas's fallback search, so a font the canvas loaded is
reused, its bytes shared rather than copied) and adds it to every egui
family as the last fallback. Nothing is bundled, Latin-only UIs load no
extra font, and on the web, without system fonts, the boxes remain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:49:22 +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 49e8413ebe Fix #130: installed fonts are found by name whatever ran before
Opening, placing, pasting or importing a file, the font menus and MCP now find
the installed system fonts (Arial, Times New Roman...) in a fresh session, so a
file renders the same whatever happened earlier, and PDF import keeps the real
family names (Microsoft YaHei, not "Microsoft Ya Hei").

Root cause: FontDb only consulted the system font catalog, which stayed empty
until a CJK fallback or Character panel > Show System Fonts happened to scan
the font folders. Every lookup by family name (face, families, styles,
has_family) now waits for one cataloging scan per session: the app starts it
in the background, other processes run it on first use. The scan reads only
each file's table directory and name table (about 70 ms for 780 faces here,
instead of reading every font file whole), the catalog is indexed by family,
the family list is cached, and an installed style of a loaded family (a
bundled family also installed with more weights) loads when asked for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:49:06 +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
theironchef 2076f82b47 Edit: Undo during typing or a drag takes back only that, and Redo returns it
edit.undo cancelled an open interaction (rolling back the typing session
or drag preview without recording it) and then undid the step before it:
one Undo removed two steps, and the typing could not be redone. Undo now
commits the interaction first, so it is the step taken back and Redo can
restore it; commit_interaction does nothing when nothing is open or
nothing changed.

Fixes #159

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9TrG4w49oSRYbP3cqxA3D
2026-10-05 10:39:53 -07:00
bflatastic a52c030963 Fix #31: the Save Changes dialog fits its content and the window at every UI scale
The Save / Don't Save / Cancel prompt (and every dialog in the shared
dialog frame) is as tall as its text and buttons at every UI Scaling value,
including when the scale changes while it is open, and never wider than the
window: at 2x in a small window the question wraps instead of running off
the left edge.

Root cause: the tall, mostly empty prompt in the report was the shared
frame's button row taking all the window's height, fixed on main by
d3df1bce (M14.1). What remained was the width: the frame's minimum and
maximum widths are in points, and at a large UI scale a small window has
fewer points than the dialog's 466 pt, so it overflowed the window. The
frame now caps both widths to the window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:31:10 +07:00
bflatastic 25a194cc00 Fix #44: right-clicking a grouped tool opens its sub-tools flyout
A right-click on a toolbar button with sub-tools (the small triangle) opens
the flyout and it stays open until a tool in it is chosen or the user clicks
elsewhere. A long press opens it too, and releasing that press no longer
closes it or picks the button's tool.

Root cause: the right-click did open the flyout, but the flyout closes on a
click outside itself, and the release of that same right-click (on the tool
button, outside the flyout) closed it in the same frame. The release of a
long press shorter than egui's click time did the same, and also selected
the button's tool. Clicks on the flyout's own tool button now leave it to
that button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:31:10 +07:00
bflatastic 831d916d3d Fix #84: the toolbar and the panel icon column scroll in short windows
In a window too short for them, the toolbar (tools, Fill/Stroke, colour
chips, drawing modes, Screen Mode, Edit Toolbar) and the dock's panel icon
column scroll with the mouse wheel, or with the scroll bar that shows while
the pointer is over them, instead of being cut off at the hint bar.

Root cause: both were drawn straight into their side panels with no scroll
area, and the toolbar's only fallback (two columns) runs out below about
760 pt. They now draw inside widgets::strip_scroll, a vertical ScrollArea
next to the menus' widgets::menu_scroll.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:31:10 +07:00
bflatastic 3d890203dd Fix #82: the window fits the screen and comes back where it was
The first window opens at 1440 x 900 when the screen has room for it around a
taskbar or Dock, and maximized (with a size that fits once un-maximized)
when it doesn't, as on 1600 x 900, 1366 x 768 or 150% scaled 1080p screens.
The window's size, position and maximized state are saved with the UI
preferences (UiState::window) and restored at the next launch, moved fully
onto a monitor that is still connected and maximized if it no longer fits.

Root cause: main.rs created the window at a fixed 1440 x 900 at the system's
default position, never checked it against the monitor and never remembered
it. The new apps/vectorcraft/src/window.rs places the still-hidden window
from the monitors winit reports (CreationContext::winit_window) and tracks
its geometry every frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:31:10 +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 9c707b37fc Fix #37: a name typed without an extension gets the chosen format's on Export too
Save, Save As and Save a Copy already added the save format's extension to a
name typed in the save panel, but every other write through it (Export,
Export Selection, Save as PDF, text and preset exports, Document Info) wrote
the name as typed, so "art" became an extensionless file.

The save panel's answer now goes through one rule (io::with_extension): kept
when it ends in the suggested name's extension or names a known format, else
that extension is added. The web already downloads under the suggested name
or adds the format's extension in its Save As dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:08:34 +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 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 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 d3d6cce917 Fix #34: Ctrl/Cmd+Shift+V pastes in place
Paste in Place is Cmd+Shift+V, but egui reports every Cmd+V chord as a
Paste event (or, for what isn't text, only the key's release), never as
the key, and the handler ran Paste for every Paste event. A paste now runs
the paste command whose V chord is held (Paste in Place, Paste on All
Artboards, Paste without Formatting; user overrides included), Paste for
the Paste key and Shift+Insert. Envelope Distort > Edit Contents also had
Cmd+Shift+V; it loses the duplicate, and a test checks that no two
commands, tools or panels share a default shortcut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:49:20 +07:00
bflatastic 72be324732 Fix #86: modifiers sent with ui.key, ui.click and ui.drag reach every handler
The control channel attached its modifiers only to the synthetic key and
button events, while most handlers read egui's held modifiers
(i.modifiers), which only the keyboard set. The raw input hook now sets
egui's modifiers (a ModifiersChanged event) for the frames synthetic input
spans: a key's press, text and release, a click, and a drag from press
through its moves to release; the keyboard's modifiers, tracked from the
host's events, come back after. ui.key's text now comes with the press.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:49:20 +07:00
bflatastic 638edec175 Fix #80: the hint bar names keys as the menus do (Alt and Ctrl off macOS)
The hint bar's segments were hard-coded strings with macOS names (Option,
Cmd) next to one Alt. The hints now write keys as shortcut strings
("Alt+Click") and show them through the menus' pretty_shortcut, and the
fallback hint shows the effective Search Commands shortcut (or the menu
item when it has none).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:49:20 +07:00
bflatastic 03ea4079f9 Fix #83: Ctrl and + zooms in however + is typed (numpad, Shift+=, a + key)
The shortcut parser mapped "=" to Key::Equals and the matcher only took
that exact key, while egui reports the numpad plus, and + on layouts that
give it its own key (and often Shift+=), as Key::Plus. "+" and "=" now
parse to one chord key, a recorded + is stored as =, and the matcher lets
a = chord take Key::Plus too (also next to the native macOS menu, which
handles only the chord as written).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:49:20 +07:00
bflatastic 30550f038a Fix #136: SVG export and CSS write each style's numeric font-weight
font_props wrote `bold` for any style whose name contained bold, black
or heavy and nothing for the others unless fonts were embedded, so
Semibold came back Bold and Light or Medium came back Regular. It now
writes the style's numeric weight (font_descriptor, from style_weight)
for every writer: 600 for Semibold, 300 for Light, 900 for Black, `bold`
for 700 and nothing for 400. The SVG import already reads these numbers
back to the matching face.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:36:29 +07:00
bflatastic 535220cf34 Fix #139: SVG import paints paint-order="stroke" text strokes under the letters
The text import reads <text> from the XML and never looked at
paint-order, so character strokes always painted over the fill. A text
whose characters all share one stroke painted first now gets that
stroke as the object's own stroke below the Characters row (as the
canvas, SVG and PDF export already draw it), with its gradient or
pattern resolved in the document; characters with differing strokes
keep them and the import warns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:36:29 +07:00
bflatastic 65dd757d59 Fix #131: SVG import keeps non-scaling strokes at their screen width
usvg knows vector-effect but drops it from the tree it returns, so every
stroke was scaled by its transforms. The importer now records the shapes
that ask for vector-effect="non-scaling-stroke" in its XML prepass (by id)
and gives their strokes and dashes the width they have on screen; type
does the same on its character strokes. Where the width can't be kept
(art in patterns, markers or <use> copies, stretched or skewed type) the
import warns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:36:29 +07:00
bflatastic 885b7acac1 Fix #87: rust-version is 1.95, and clippy 1.95 passes
egui 0.36 needs rustc 1.95 (hayro and krilla 1.92), so the declared 1.90 let
cargo fail on dependency versions instead of naming the toolchain. Clippy 1.95
also flagged four collapsible matches that newer clippy no longer does; each is
rewritten without changing behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:05:32 +07:00
bflatastic d0d934bfda M14.7: Cmd/Ctrl+N opens the New Document dialog
The engine's file.new also claimed Cmd+N and engine commands win shortcut
lookups, so the shortcut made a blank document. file.newDialog now owns it,
as the docs already said; file.new stays the headless command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:43:48 +07:00
bflatastic 258337cf7c M14.3: the Home button shows the Home screen over open documents
It used to open the New Document dialog. Now it runs the new app.home UI command:
the Home screen replaces the canvas while the documents stay open as tabs;
choosing a tab, or a document opening, closing or becoming active, returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:43:48 +07:00
bflatastic d4c4361b4a M14.1: long menus scroll inside the window and use all the height they have
Menu bar menus, their submenus, panel menus, the effects menu groups and library
submenus are as tall as their items down to the window's bottom edge, and scroll
only past that (widgets::menu_scroll), instead of running off screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:43:48 +07:00
bflatastic 79581c7ff5 M4.87: PDF 1.3 compatibility: transparency flattened with the chosen preset
compatibility "1.3" writes a %PDF-1.3 file (its XMP metadata says 1.3
too) without transparency: a copy of the document is flattened first
with flattenerPreset (stored with PDF presets; empty is High Resolution)
and the flattener options over it, see-through image pixels counting as
transparency and rasterized areas clipped to their regions so no soft
mask is needed. Page groups are left out and the writer refuses a file
that would still have transparency. PDF/X-1a and PDF/X-3 share this
path (their version is now PDF 1.3; 1.4 is still accepted and written
as 1.3), and with it the preset, the header and metadata code and the
transparency check. Passwords at PDF 1.3 encrypt with 40-bit RC4
(revision 2). The Save PDF dialog lists PDF 1.3 and picks the flattener
preset under Advanced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:05:36 +07:00
bflatastic 8961824876 M4.86: PDF page thumbnails and fast web view (linearised files)
Embed Page Thumbnails gives every page a /Thumb image: the page drawn on
white, 106 px on its long side, without the layers the page leaves out
(the engine draws them; vectorcraft_pdf::page_areas says what each page
shows; PDF/X files get grey ones). Optimize for Fast Web View writes a
linearised file: the objects are put in linearised order and numbering,
encrypted when a password asks (the encryption dictionary goes with the
catalog, PDF layers' groups too), then laid out with the linearization
dictionary, both cross-reference tables and the page offset and shared
object hint tables (the hint stream encrypted like any other). Their
"not applied yet" warnings are gone. Reading the objects back uses the
lexer of encrypt.rs (syntax.rs adds what rewriting needs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:05: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 0e4d3f6871 M4.82: PDF export writes top-level layers as PDF layers
Create PDF layers from top-level layers (document.exportPdf createLayers, the
Save PDF dialog's General option) now writes an optional content group per
top-level layer (template layers are left out): named as the layer, off when
the layer is hidden, /PrintState /OFF when its Print option is off, locked
when it is locked, listed top first, with a print auto-state (not under a
standard). The file reopens with those layers.

The PDF writer has no optional content, so each layer is drawn as a form of
its own (no transparency group, so it draws as in place) starting with a mark;
after writing, the uses of the layers' forms are wrapped in /OC ... BDC/EMC
and the catalog gets /OCProperties (crates/pdf/src/forms.rs, through the
shared patch writer and the PDF syntax reader of encrypt.rs). PDF 1.4 and a
knockout page group write plain page content with a warning; the dialog greys
the option at 1.4. The "not written yet" warning is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:39:28 +07:00