The 15 warp presets (Edit > Transform > Warp, smart object warps, Warp
Text) were our own closed-form approximations, up to 70 px off
Photoshop's: Arc kept its centre fixed and pulled the ends in where
Photoshop keeps the inner edge's corners and raises the middle, Flag
waved the wrong way at a third of the height, Squeeze shrank instead of
pinching.
Measured on Photoshop 27.11 (scripted renders of a grid image whose
colours encode each pixel's position, 102 cases: every preset at bend
±50 and ±100, both distortions, vertical orientation, square, wide and
tall boxes), every preset is a single bicubic Bezier patch (fits within
0.5 px). Its control grid:
- Arc: each row a circular arc of b·π around one centre, the inner edge
keeping its chord; Arch, Bulge, Arc Lower/Upper and Shell built from
the same arcs. Circular in pixels on any box.
- Flag, Wave, Fish, Rise, Fisheye, Inflate, Squeeze, Twist: control
points moved linearly with the bend, in box units.
- Vertical distortion scales the top and bottom rows about their
midpoints and blends the rows between; horizontal distortion scales
each column. Vertical orientation swaps the axes and the two
distortions.
All 102 cases land within 1 px (median) of Photoshop. On the corpus's
Photoshop-made warped text, warp-arc-50 now spans 340x85 at (-3, 3)
against Photoshop's 350x88 at (-7, 3), where it was 278x74 at (28, 62).
Presets also convert to custom meshes exactly now (to_mesh splits the
patch instead of fitting). Tests that pinned the old shapes now assert
Photoshop's; presets_match_photoshop checks measured points.
Still open: Warp Text warps over our line box, while Photoshop uses the
type layer's stored `bounds`; with those the five corpus warp files land
within a few px (arc, flag, bulge with distortion, twist, vertical rise).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Image warp: grab anywhere on the grid, Ctrl-click to cut, Warp in the transform menu
Warp mode of Free Transform now works from anywhere inside the grid, not
only on its points:
- Press and drag on any part of the grid and the surface under the pointer
follows it. The surrounding anchors bend with it, fading out with
distance. The picture's border stays put unless the grab is made near
it, and then the border bows through its own handles. The four corners
never move.
- Ctrl-click (or Option-click) cuts the grid both ways at the pointer.
- Right-clicking a Free Transform box lists Warp after Perspective. The
menu marks only the box's real current mode.
- Anchor and handle drags keep every grid line smooth through its anchors.
- Grid points are quieter: anchors are small, and a handle with its arm
shows only near the pointer or while it is dragged.
- The preview draws the most displaced cells first and samples finer where
the warp pulls hard, so a strong pull no longer shows as a comb.
Refs #214
* Warp options bar: Split, Grid and Warp controls with orientation and bend always shown, Reset, and the buttons right after the fields
The Warp bar now reads Split, Grid, Warp style, orientation and Bend / H / V.
The orientation and bend fields are always there and only act on a preset
style. A new Reset button puts the grid back to the default untouched one
while staying in warp mode.
Cancel and Commit sit right after the fields instead of at the far right of
the window. A bar too narrow for both pins them to the right edge and clips
the fields, so they stay reachable. The Free Transform bar shares the same
function and gets the same placement.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
width() and height() computed `x1 - x0` / `y1 - y0` with plain i32
subtraction. For extreme edges (e.g. an inverted i32::MAX..i32::MIN
rect, or a valid rect spanning most of the i32 range) the subtraction
overflows: debug builds panic, release builds wrap. warp.* commands
build a Rect straight from the JSON `rect` param, so an extreme value
reached this from user input.
Both now return the exact extent via `abs_diff` when the far edge is
past the near one, and 0 otherwise. The extent of any i32 pair fits in
u32, so no clamping or casting is needed. A plain `saturating_sub`
would avoid the panic but turn a wide valid rect into a width of
i32::MAX, which is wrong.
Follow-up (not in this change): warp_cmds still accepts absurd `rect`
params; it should reject them with a parameter error up front.
Co-authored-by: Luna <luna161@users.noreply.github.com>
* fix(geom): saturate Rect::translate and inflate on i32 overflow
from_xywh already uses saturating_add. translate and inflate used
plain +/- and panicked in debug (or wrapped in release) when a layer
near the i32 edges was nudged off-canvas.
* fmt: Rect::translate/inflate at the workspace width (#159)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- 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>