The Pen flyout (P / Shift+P) now holds Pen, Freeform Pen and Curvature Pen.
Freeform Pen: a drag draws freehand; on release the trace is fitted with
cubic Beziers within the options bar's Curve Fit (default 2 px) by
photocraft_vector::trace::fit_trace (Schneider, "An Algorithm for
Automatically Fitting Digitized Curves", Graphics Gems 1990, the fitting
Make Work Path already uses), keeping turns sharper than 60 degrees as
corners. Releasing on the start point closes the path.
Curvature Pen: each click adds a smooth point and the path runs through
every point (Catmull-Rom style handles from
photocraft_vector::edit::curvature_knots); dragging a point moves it,
double-clicking toggles corner/smooth, clicking the first point closes,
Enter/Esc finish and Backspace removes the last point, as with the Pen.
Both commit through the Pen's path: a shape layer in Shape mode, else the
targeted vector mask, else the work path, as one History step.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Paths could be reshaped after drawing since #865, but the number of
anchors could not change. Two new engine commands do that, and two tools
in the Pen flyout drive them on the canvas.
- photocraft_vector::edit::add_anchor splits the segment at t with de
Casteljau, so a curve keeps its shape and gets a smooth anchor; a
straight segment gets a corner on the line. delete_anchor joins the
neighbours keeping their handles, drops an emptied subpath and refuses
to remove a path's only anchor.
- path.addAnchor / path.deleteAnchor take the same targets as the other
point edits (work path, saved path by name, shape layer path, vector
mask) and record one history step each.
- Tool::AddAnchorPoint / DeleteAnchorPoint join the P group after the Pen.
They act on Direct Selection's paths, highlight the segment or anchor
under the pointer, show a +/- cursor badge, swap with Alt and turn into
Direct Selection with Cmd/Ctrl. An edit clears a Direct Selection on the
same path so it never points at a shifted anchor.
- Icons derived from Lucide pen-tool (ATTRIBUTION.md), hints and tool
names in all 16 catalogs, README tool list, roadmap.
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Drop f32 round-off coverage in the vector rasterizer (#2537)
Each row's cell accumulator is prefix-summed in f32. A left boundary adds
coverage and the right boundary subtracts it, but for curved or off-grid
edges the two don't cancel exactly, leaving ~1e-7 coverage in the pixel
just past the shape. Shape layers wrote the fill colour there (alpha
rounding to 0 at 8/16-bit, ~1e-7 at 32-bit), so content bounds and
shape.info reported curved shapes one pixel too wide.
Snap coverage below 1e-6 to 0 when writing a row. That is under half a
16-bit step, so 8- and 16-bit coverage values are unchanged; only the
residue pixels go away. Selections, vector masks and strokes share the
rasterizer and get the same fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rasterizer: NaN coverage becomes 0, not full (#2537)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Expose native shape stroke controls with staged previews
* Document shape stroke defaults in MCP control schema
* Regenerate docs/scorecard.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: optimalbytee <optimalbytee@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Concurrent requests for the same cold combined mask each rasterized and feathered the whole
image. A per-key OnceLock slot now lets one caller build while the others wait for the same
tiles; different keys build independently outside the cache mutex, and the result is admitted
through the byte-budgeted cache with the generation taken at lookup, so a purge mid-build still
wins. The vector mask renders sequentially inside the slot so a Rayon worker can't wait on itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vector::Paint::from_fill discarded every gradient field beyond stops,
angle, scale and style, so Photoshop files whose shape fills use opacity
stops, colour midpoints, a centre offset (Ofst), dither or no
"Align with layer" rendered with the wrong ramp, alpha, position or
banding. Paint::Gradient now carries the full ramp (sorted stops,
midpoints, opacity stops, dither) and the compositor's geometry
(centre offset, whole-pixel end-point snapping, pixel-corner sampling,
the align frame choice: shape bounds or canvas), with shape strokes
painting through the same path.
The vector crate cannot depend on compose, so the ramp and geometry
mirror effects::gradient_units/gradient_t, fill_layout::gradient_layout
and gradient_fill::{Ramp, midpoint_remap, dither}. A parity test in
compose (which sees both) renders each non-default field, style and
frame as a shape layer and as a fill layer and requires equal pixels;
from_fill binds every Fill::Gradient field — no ".." — so a new field
fails to compile until the shape renderer honours it.
* Direct Selection tool: reshape paths after they're drawn (#790)
Adds Photoshop's Direct Selection tool (A, in the Path Selection flyout) and
the Pen's Cmd/Ctrl (Direct Selection) and Alt (Convert Point) modifiers. It
selects and drags anchors, direction handles and segments of the work path,
the active shape layer's path and a targeted vector mask.
The geometry lives in photocraft_vector::edit; new engine commands
path.moveAnchors, path.moveHandle, path.bendSegment and path.convertPoint
apply it (one history step per drag, drivable over MCP and the control
channel), and the canvas previews a drag with the same code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tool::ALL holds 49 tools now that Direct Selection and Magnetic Lasso both landed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Keep OnceLock geometry initialization sequential so Rayon work stealing
cannot re-enter an unfinished slot. Raster coverage and scratch remain
local to each requested rectangle; export bands share only geometry.
- Real crash: build_engine_data took its template from the PSD's text
engine data and did expect("EngineDict"), so a text layer whose engine
data lacked EngineDict (or wasn't a dictionary) crashed PSD export.
The template is now repaired; regression test
engine_data_template_without_engine_dict.
- color: the built-in CMYK<->sRGB transforms fall back to the naive
formulas instead of expect if they ever fail to link.
- doc: insert_above falls back to the top of the root instead of
expect("valid path"); next_layer_name searches a bounded range.
- paint: Mips::new loops with while-let instead of last().expect.
- All eight crates deny unwrap/expect/panic/unimplemented/todo/unreachable.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
CI's `cargo fmt --all -- --check` has failed on every run because the
repo had no rustfmt config and was never formatted. Use the same settings
as EffectCraft/FilmCraft (max_width 160, use_small_heuristics Max), which
also keeps the churn small. Formatting only; no code changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>