Repository navigation
Conversation
…ld#2830) * Options bar: Reset Tool and Reset All Tools (storytold#2825) Right-click the tool icon at the far left of the options bar for Reset Tool (the current tool's options back to their defaults) and Reset All Tools (asks "Reset all tools to their default settings?", then resets every tool's options), as in Photoshop. Both are shell commands, tool.reset and tool.resetAll, so the control channel can run them; tool.resetAll opens the confirmation as a dialog, or skips it with {"confirmed": true}. ToolOptions is one struct, so tool_reset::reset_options maps each tool to its fields; fields shared between tools (Tolerance, Anti-alias, Feather, Strength, ...) reset with any tool that shows them. Selection tools also go back to New Selection, and painting tools get the default brush (colours kept). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Reset Tool: cover Color Replacement, the anchor point tools and the crop custom ratio Tools and options that landed on main since the branch was cut. 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>
… flip (storytold#2832) (storytold#2840) The Free Transform context menu listed only the modes. It now follows Photoshop's order: the modes, then Content-Aware Scale and Puppet Warp (greyed while a box is open), Rotate 180°, Rotate 90° CW and CCW, and Flip Horizontal and Vertical. The Rotate and Flip items, whether chosen from this menu, Edit › Transform, a shortcut or the control channel, now turn or mirror the live box about its reference point without committing it (transform_tool::intercept). Before, they ran the engine preset on the layer under the open box. They are greyed while warping. Distort and Perspective are greyed over a box on editable type, matching storytold#2701. A control-channel right-click during a transform opens the same menu as the canvas. Scorecard TOOL-214-5 is now done. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
…ld#2835) (storytold#2845) edit.transform without a "target" (or "layer") param now follows the Channels panel: a targeted alpha channel or the Quick Mask transforms instead of the layer. Edit › Transform › Rotate 180°/90° and Flip Horizontal/Vertical are routed to the panel target like the paint and filter commands, so they turn a targeted alpha channel, the Quick Mask or an unlinked layer mask within its frame (one undo step). The Background no longer greys the transforms when a channel is targeted. Flips scorecard row BUG-207-4. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…orytold#1767) (storytold#2809) The rotate zone drew its own fixed arc with hard-coded black and white, and the angle was bare white text that vanished over light images. W/H and the move offset were not shown at all. The rotate zone now uses the Crop tool's turn cursor (theme tokens), bent round the nearest corner through the view, so it follows a turned or flipped view. While dragging, the shared rounded readout beside the pointer shows the angle (document-relative, one decimal), W/H in percent while scaling, or the offset while moving, like Photoshop's Show Transformation Values. live_readout() exposes the value for tests. Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…touches (storytold#2844) (storytold#2852) With Auto-Select on (or Cmd held with it off), a Move-tool drag that starts where no layer has pixels (an empty spot, or the pasteboard) now draws a marquee instead of moving the previously selected layer. Releasing selects every layer whose content touches it (Auto-Select: Group selects their outermost groups); Shift adds. New engine command layer.selectInRect; the UI side lives in move_box.rs. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
…#1067) (storytold#2803) View > Pattern Preview only toggled a flag that nothing read. With it on, the canvas now draws the document repeated around itself as a seamless tile, like Photoshop, on the GPU canvas and the CPU fallback: - pattern_preview.rs works out which copies cover the visible area (at most 31 per axis, centred on the view). - GPU: each copy is another paint callback over the same document texture, offset by whole document sizes in the shader (ViewParams::offset, the spare view-uniform slots), so nothing is composited again and seams are exact. - CPU: the same texture drawn again with a shifted view mapping. - The drop shadow is off while tiling; selection, guides and other overlays stay on the document itself. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
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>
…2861) The Type flyout (T, Shift+T cycle) gains Vertical Type Mask and Horizontal Type Mask, in Photoshop's order. They type like the Type tools, with the canvas under Quick Mask's colour and the glyphs clear while editing. On commit the glyph coverage replaces the selection; no layer is left behind and the whole session is one "Type Mask" history step. Escape always cancels and leaves the selection unchanged. The commit runs a new engine command, type.createSelection: a type layer's glyphs (optionally removing that layer, the tools' hidden working text) or type.create's keys directly, with new/add/subtract/intersect and an anti-aliasing override. type.create's layer building is shared through type_cmds::new_text_layer. Original dashed-T icons (ATTRIBUTION.md), tool label translations in every catalog, README tool list and the gaps/parity docs updated. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Speed up large feathered mask refreshes * Format preference label for workspace check * gpu test: allow float noise on a one-code vector-mask parity step The combined mask is cached as U16 now; one pixel lands exactly one 8-bit code apart (1.0000075/255), within the <=1/255 parity rule. 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>
Photoshop's Artboard tool in the Move flyout (V, ⇧V cycles Move ↔ Artboard): dragging on an empty spot draws a new board (`layer.new.artboard`), clicking a board selects it, dragging it moves it with its contents, and the selected board's edge and corner handles resize it (`layer.artboard.set`, moveContents: false). Each gesture is one undo step. Options bar: Size preset, W/H, Make Portrait, Make Landscape, Add New Artboard. Original toolbar glyph (assets/icons/artboard.svg, ATTRIBUTION.md row). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
New Perspective Crop tool in the Crop flyout (C): drag a box or click four
corners, drag corners independently onto a skewed object, perspective grid
(Show Grid), Enter commits, Esc cancels. W/H/Resolution come from the Crop
tool's fields (Front Image, Clear), or the quad's mean side lengths.
Engine: image.perspectiveCrop {corners, width?, height?, resolution?,
interpolation?} maps the quad onto an upright canvas through the
quad-to-rectangle homography (Heckbert's square-to-quad map, inverted):
every raster layer, mask, alpha channel and the Quick Mask, one undo step.
The Background stays opaque over the background colour; type, shape, fill
and smart object layers are rasterized first; vector geometry follows the
nearest affine map. Non-convex, self-crossing, counter-clockwise or
degenerate quads are refused with an error.
algo: warp_surface_clipped bounds the per-tile warp by the new canvas even
when a layer reaches past the map's horizon; convex_quad, quad_size,
quad_to_rect.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…torytold#2866) Photoshop's Actions panel has a check column: an unchecked step is skipped by playback, Batch and droplets. PhotoCraft could reorder and delete single steps but always ran every step. - Action gains `disabled` (0-based step indices, omitted from actions.json when empty), kept in step with actions.move and actions.delete. - New command actions.toggle {action, step?, enabled?}: one step, or every step like the action's own check; bad indices and types are errors. - actions.play, the desktop playback path and the steps handed to Batch and Create Droplet skip unchecked steps; actions.get reports `disabled`. - Panel: a check box per step, dimmed label when off, "2/3 steps" count. Part of storytold#219 (AUTO-219-2). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
…r's x (storytold#1946) (storytold#2821) egui's TextEdit picks the character under the pointer with Galley::cursor_from_pos, which returns the start of the text when the pointer is more than 5 points above the first row and the end when it is below the last one, for single-line fields too. So dragging up selected everything left of the anchor and dragging down everything right of it. A small egui plugin, installed by PhotocraftApp::setup_context, keeps the pointer's y on the text row of the focused single-line field while the primary button pressed in it is held, before egui sees the input. The x is untouched, so the selection follows it, as in Photoshop and native fields. It covers every single-line TextEdit, number fields being typed into included; a multi-line field (taller than two widget heights) keeps its vertical drag. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
) (storytold#2870) Select Subject recomputed superpixels, saliency, GrabCut and the full-resolution re-cut on every call (P29: 260-370 ms on 24 MP here, 693 ms p95 in the baseline). The session now keeps the last result, keyed by the pixels it came from: `same_pixels` (tile pointers) against a weakly held document, plus the sampled target (composite or layer). Selection-only edits such as Deselect keep it valid; the mode (replace/add/subtract/intersect) is still applied each time. One entry per session, dropped on pixel edits and when its document closes. Repeat: 7-10 ms p95 (was 270-370 ms). First run unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…told#2735) (storytold#2854) A dropdown needed two clicks: egui's ComboBox opens its list on a whole click and its rows only take clicks, so press-drag-release (as the menus do since storytold#775) chose nothing. press_menu::PressCombo gives combo boxes the gesture without replacing egui's widget: - after the box, a primary press on it opens the closed list (egui counts a popup opened in a pass as open, so it is drawn from the next frame); - a row released on by that press-drag is chosen and closes the list; - on the click that ends the press (pointer still on the box), egui would toggle the list shut, so it is closed just before the box and egui's toggle reopens it, drawn that frame: a plain click still leaves the list open for a second click. widgets::dropdown (and its hovered/wheel variants), dropdown_with_tooltips, the font family picker, the kerning presets, the Gradient Map presets, the shape stroke choices and the credits sort all use it. Co-authored-by: lloyd <lloyd@stackworx.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…old#2873) (storytold#2882) The blur itself was already parallel (9-15 ms on 24 MP); most of the command was serial glue around it. Modify copied the mask into a padded buffer and back out sample by sample even with no padding, and combine built a full-canvas byte buffer, scanned it, then copied it into tiles on one thread. - modify: use the mask as is when the selection is clear of the canvas edge (pad 0); pad and crop row by row otherwise. - combine / mask_to_surface: fill selection tiles in parallel straight from the mask, a row at a time; fully selected tiles share one copy-on-write tile. Bit-identical: the per-sample oracles (combine, now with whole tiles, and the old pad/crop) are compared in tests. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ytold#2859) * feat(ui): refine gradient fill properties * feat(ui): use combined stops in Gradient Editor * test(ui): restore cross-platform save path helper * Refine Properties with a shared gradient ramp --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
…ld#2883) (storytold#2884) Duplicating a layer reported no damage, so the canvas recomposited the whole document after every Duplicate Layer (P8/P9). The copy itself was already cheap (tiles are shared Arcs). Report the copy's change bounds instead, through the step_damage walk extended to bounded additions: self-contained added layers (not an adjustment, not a new clipping base, no video or excluded channels) contribute their change bounds; anything else still recomposites everything. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#2823) (storytold#2876) The Fit button was a full-size 30 pt button in the 30 pt Studio bar and the zoom field a 24 pt box in the 24 pt Pro bar, so both touched or overflowed the bar's edges. Inside every value field the theme's 24 pt interact height and 4 pt button padding made the number taller than its slot, so it sat low and, in a narrow box, spilled out of the left edge. The status bar's controls now take a compact height (22 pt in Studio, 18 pt in Pro) from status_bar_heights, leaving even margins, through new compact_value_field and compact_button widgets. A value field's number slot is at least one text row tall and the number widget is sized to it, so the digits sit 1 pt above the box's centre as intended. With the bar at its true height the canvas shifted by 1 pt, which showed that the live lasso ants were not on pixel centres; they are now snapped like the marquee's. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ld#2900) (storytold#2902) A dab that painted past a layer's stored tiles changed its resident page region, which recreated the texture and converted and uploaded every tile of the layer in that page cell again. Copy the overlap from the old texture on the GPU instead (in its own submission, ahead of the queued tile writes) and keep its tile map, so only new or changed tiles upload. 32-bit float RGB/gray tiles also convert to RGBA16F straight from their samples (same bytes as the general path) instead of decoding into a float buffer and converting pixel by pixel. P16 (32-bit, 20 layers) brush dab + refresh, median of 6 alternating runs: p50 8.9 -> 3.1 ms, p95 27.2 -> 7.5 ms. P13 is unchanged (~1.6 / ~4.5 ms). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…told#2661) (storytold#2892) Photoshop has four interface brightness levels; PhotoCraft had the two darkest (Pro, Pro Medium Gray). Add the other two with the Pro layout: - Pro (Light Gray), id proLightGray: #B8B8B8 panels. - Pro (Light), id proLight: #F0F0F0 panels. They are light themes: Preferences > Interface lists them under Light Theme, Window > Theme has an entry for each, and the legacy interface.theme preference selects and migrates them. ThemeKind::is_pro replaces the hard-coded Pro | ProMedium checks so the dock keeps the Pro tab layout on every Pro level. Labels are translated in every catalog. Tests check the panel greys, ids and names, the darkest-to-lightest order, and WCAG contrast of text, icons and accent text on every surface. Claude-Session: https://claude.ai/code/session_01CVtZHu998NDhf4Z8QvS5pd Co-authored-by: ngrafton2006 <ngrafton2006@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
…rytold#2894) * scorecard: eight Open bugs rows describe work that is already in the tree AGENTS.md sends contributors to the scorecard's missing and partial rows when picking work, so a stale row costs someone a session: they sit down to write a test that is already there. These eight ask for work that exists and is covered by passing tests. storytold#205 / storytold#207, mask and channel transforms: - BUG-205-4 ("no raster-mask test"): linked_raster_mask_moves_with_the_pixels. - BUG-205-5 ("the other two not implemented"): all three cases are covered, by linked_mask_with_a_selection_moves_only_the_selected_region, unlinked_mask_stays_put and targeted_unlinked_mask_transforms_alone. - BUG-207-4 ("transform() has no target handling"): edit.transform takes target (pixels, mask, quickMask or {channel: i}) and documents it in its params doc; targeted_alpha_channel_transforms_alone covers the alpha channel. storytold#206, kerning — the whole section: - BUG-206-1: CharStyle has both `kerning: Kerning` and `kern: f32`. - BUG-206-2 / BUG-206-3: io/tests/psd_kerning.rs round-trips manual, Off and Optical kerning, and reads the AutoKerning flag and Kerning value of every character back out of the written TySh. - BUG-206-4: Alt+Left/Right at a collapsed caret calls kern_pair, and the Character panel field parses Metrics / Optical / a manual value; both tested. - BUG-206-5 stays partial, not done: the round trips are covered, but no oracle corpus file is dedicated to kerning values, which that row also asks for. FILE-216-5 keeps its missing status (no save-path tests for layered TIFF, PDF, SVG, ACO/ASE, .shc or Smart Filters), but its note claimed there is no atomic-save failure-injection test; format/src/atomic/tests.rs has two, and BUG-203-6 already records them as done. docs/scorecard.md regenerated with cargo xtask scorecard; Open bugs moves from 22/2/8 to 29/2/1. * scorecard: the Type section's kerning and Thai rows are out of date too TYPE-218-6 ("Optical renders as Metrics; Alt+arrow moves by word") is done: Alt+Left/Right at a collapsed caret calls kern_pair, optical kerning is computed from the glyph outlines in text/src/optical.rs and applied by layout.rs when the run's mode is Optical, and both modes round-trip through PSD. Covered by type_tool_alt_arrows_kern_the_pair, optical_matches_photoshop, optical_kerning_tightens_open_pairs, txt2_carries_optical_kerning and io/tests/psd_kerning.rs. The row points at storytold#206, whose own rows are in the same state. TYPE-218-3 goes to partial, not done: Thai is covered (THAI_FAMILIES in fonts.rs and thai_in_latin_font_falls_back_to_installed_thai_font, storytold#1909), but Khmer and Indic have no fallback list and there are still no per-script golden tests. Unchanged because they read correctly: TYPE-218-1 (TextShape is still Point and Box), TYPE-218-2 (no direction UI), TYPE-218-4 (the axes in the shell are the brush's), TYPE-218-8 (no triple or quadruple click) and TYPE-218-9. * scorecard: Tools and Distribution rows are out of date too TOOL-213-7 ("Live stroke only for Brush and Eraser"): canvas.rs strokes_live covers Brush, Pencil, Eraser, Blur, Sharpen, Smudge, Dodge, Burn and Sponge, and live_retouch_command adds the Clone Stamp, Healing Brush and History Brush. TOOL-214-8 ("ruler only feeds straighten"): image.rotation.arbitrary opens seeded with the measured ruler angle and the direction that straightens it; arbitrary_rotation_starts_at_the_ruler_angle checks 5.7106 deg within 1e-3 and the direction for three lines. TOOL-214-5 goes to partial, not done ("No context menu in canvas.rs or transform_tool.rs"): TRANSFORM_MENU does open on a right click during a transform, with Free Transform, Scale, Rotate, Skew, Distort, Perspective and Warp, and three tests cover it — but the row also asks for Flip and Rotate 90/180, which are not there. DIST-220-2 ("release.yml only x86_64 and i686 msvc"): the Windows matrix has { arch: arm64, target: aarch64-pc-windows-msvc }, and windows-arm64.yml installs that MSI on a Windows 11 ARM64 runner, checks the binaries are ARM64 and runs the CLI natively. Signing takes the same path as x64 (DIST-2). Unchanged because they read correctly: the whole Automation section (ABI_VERSION is still 1 with Kind::Filter only, there is no ONNX dependency, no such xtask check, and agent_tasks.rs really does run ten scripted tasks), TOOL-213-12, TOOL-214-4, TOOL-214-6, DIST-220-1 and DIST-220-3. * scorecard: Workspace and UI and File compatibility rows are out of date too UI-217-9 ("prefs.rs UiScale unused"): prefs::UiScale offers Auto plus 75 to 300 percent, display_scale resolves it and sync_display_scale applies it with ctx.set_zoom_factor; being a preference, it is remembered. The zoom_factor assert the note cites is in a canvas-zoom test, checking that zooming the document does not scale the interface. FILE-216-1 ("No corpus step in ci.yml; in progress in storytold#190"): ci.yml has a corpus job on every PR and on main, pinned, sha256-verified and cached by the pins. storytold#190 is merged. UI-217-22 and UI-217-23 go to partial, not done: - layer.stampVisible merges the visible layers into a new layer and leaves the originals, and layer.stampDown exists, but neither has a menu entry and Alt+Merge is still missing. - layer.copyToDocument copies layers into another document keeping masks, styles and blending (storytold#589), but there is still no copy/paste layer clipboard. UI-217-11 keeps missing; only its note changes, from "five themes" to the eight ThemeKind has. No Soft dark/Soft light, previews or outlined knobs. Unchanged because they read correctly: UI-217-20 (guides are still Vec<f32> and there is no rulers preference), FILE-215-6 (SmartObject.transform is still an Affine), FILE-215-19 (no JPEG Options dialog), and the rest of both sections. --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rytold#2888) (storytold#2907) The Elliptical Marquee tested and wrote every pixel one at a time (Surface::write_pixel), single-threaded, with no shared full tiles. The add/subtract/intersect modes read both selections over the whole canvas into f32 buffers. On 6000x4000 an ellipse took ~0.6-1.1 s and a mode combination ~0.3-0.5 s. ellipse_surface builds the shape per row: a run of fully selected pixels (bounded analytically, checked with the exact f32 tests at both ends) and edge pixels that get the original per-pixel coverage test. Tiles are built in parallel, and fully selected tiles share one tile. combine_surfaces maps two GRAY8 selections tile by tile through a 256x256 byte table, sharing tiles that come through unchanged. Both produce exactly the old bytes (equivalence test against the old per-pixel code). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Frame tool (K, its own toolbar slot after the Crop group) draws a rectangular or elliptical frame; the options bar picks the shape, Shift constrains to a square or circle and Alt draws from the centre. A frame drawn over the selected pixel layer's content takes that layer in; otherwise it is an empty frame. Frames are built as Frame from Layers builds them: a group clipped by a layer mask (rectangular or anti-aliased elliptical). The new engine command layer.new.frame does it in one Session::edit, so one undo step. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
) * feat(ui): add guide layout dialog with live preview * test(ui): restore path helper used by save dialog assertions * chore(i18n): avoid conflicts with appended catalog entries * chore: avoid append conflict for contributor credit * Merge main: one people.toml entry for Skyfuller1303, drop the duplicate joined helper 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>
… (storytold#2917) * perf: Duplicate Layer recomposites only where the copy draws (storytold#2883) Duplicating a layer reported no damage, so the canvas recomposited the whole document after every Duplicate Layer (P8/P9). The copy itself was already cheap (tiles are shared Arcs). Report the copy's change bounds instead, through the step_damage walk extended to bounded additions: self-contained added layers (not an adjustment, not a new clipping base, no video or excluded channels) contribute their change bounds; anything else still recomposites everything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf: pasting a layer recomposites only where it draws (storytold#2901) Paste (and Paste in Place, Paste Into, Paste Outside) inserted the pasted layer through Session::edit, which reports no damage, so every paste recomposited the whole document (P10/P11). Capture the document before the paste and report the new layer's change bounds with note_insert, as Duplicate Layer does (storytold#2884). Anything that isn't self-contained (a new clipping base, video, excluded channels) still recomposites everything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Image Size: Photoshop's resampling, measured Photoshop 25.4's Image Size was measured as a black box (synthetic 16-bit images in, exact RAW out); every method but Preserve Details' enlargement is linear, so each was taken apart from its impulse responses. - Bicubic is the Keys cubic at a = -0.75, not Catmull-Rom. - Bicubic Smoother (stretched x1.15, a = -0.625) and Bicubic Sharper (x1.05, a = -1), with a growing with the reduction, and Bicubic Automatic. - Reducing widens the filter by 1/s; below 1/4 the image is first averaged down to four times the target. Bilinear is never widened and averages whole pixels below 1/2. Positions are rounded to 1/128 pixel and stop at the edge pixel's centre. - Two passes, the smaller scale first, clipped and stored between them; the bicubic filter is designed for the height's scale and serves both axes. Bicubic Automatic splits a mixed resize into Sharper, then Smoother. - Image Size and Fit Image take automatic, bicubicSmoother, bicubicSharper and bicubicAutomatic; the dialog lists Photoshop's methods and opens on the interpolation preference. On 225 probes the mean error falls from 240-550 to under 0.5 levels of 32768; 8-bit results are identical to Photoshop's for 99 % of values. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Format: one rustfmt change main needs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rytold#2665) (storytold#2932) The Pro toolbar drew each tool glyph at 16 px in a 30 px button (the generic icon-button proportion), which reads small and thin next to Photoshop's toolbar. Pro themes now draw tool glyphs at 20 px; the other themes keep the icon-button size. Claude-Session: https://claude.ai/code/session_01CVtZHu998NDhf4Z8QvS5pd Co-authored-by: ngrafton2006 <ngrafton2006@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
…e timeouts (storytold#2487) (storytold#2507) * MCP bridge: reconnect before idle sockets die, retry reads, actionable timeouts (storytold#2487) The app closes idle control connections after 30s (IO_TIMEOUT) while the bridge kept one persistent connection, so the first call after a pause failed with "operation may have completed". The bridge now drops and reconnects before sending once the connection has been idle 25s, so a request is never written to a socket the app may have dropped. The default call timeout drops from 60s to 55s, strictly below the app's 60s reply deadline, so the bridge's own error — naming the method and the likely causes — is the one callers see instead of a race between the app's deadline reply and the caller's own cutoff. Idempotent read methods (ui.inspect and friends) get one automatic retry on a transport failure; edits keep storytold#1007 semantics and are never resent. A timed-out call is never retried (it would only double the wait). * MCP bridge: caller-vouched read retries, per-failure wording, docs contract (storytold#2487 review) The retry decision moves from a hand-maintained method list — most of whose entries were engine command ids that never travel as control-protocol methods, so they could never match — to the call sites that know a call is a read: call never resends (storytold#1007), call_read retries once on a transport failure, and the MCP server opts its pure-read tools in. Failures are worded by what they prove: a connection that could never be established is "the request was not sent", not "may have completed"; a read timeout says a read may be retried safely instead of demanding a state inspection. The 55s client cap, the read-retry exception, and the idle reconnect are now documented in the control protocol and the security book.
…ld#1481) (storytold#2948) As in Photoshop's pop-up menus, typing a letter in an open dropdown list jumps to the next option starting with it (the same letter again cycles through them), letters typed within a second match a longer prefix, and Enter closes the list on the option shown. Matching is case-insensitive on the translated labels (Unicode lowercase, no byte slicing). While a dropdown list is open it owns the keyboard like an open menu, so its letters no longer reach the tool shortcuts. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Linux: StartupWMClass is the window's app id The desktop entry said StartupWMClass=photocraft, but the window carries the app id ai.storyteller.photocraft (APP_ID) on Wayland and under X11 alike, so nothing that matches a window to its launcher by that key ever matched it. VectorCraft, GridCraft and WordCraft already use the app id. A test reads the entry and compares it with APP_ID. * Contributors: add atoslins (Atos Lins) --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
…3562) * test(automation): ten more MCP acceptance tasks (11-20) * docs(scorecard): FILE-216-6 note counts 20 tasks
…old#3601) (storytold#3607) Studio, Studio Light and Classic showed only the hint 'Drag to draw a gradient'. The Gradient arm was gated by t.pro; it now runs in every theme. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…n't overrun (storytold#3580) (storytold#3616) A translated label beside a fixed-width control could run over its neighbours: the Layers Lock/Fill row guarded only the redundant "Lock:" caption, so "Fill:" ("Заливка:" in Russian, longer still as pt-br "Preenchimento:" / fr "Remplissage :") overlapped the lock icons. Rather than a per-row fit check, add the one shared primitive for fixed chrome: `widgets::elided_label(ui, text, max_w)` lays the text out no wider than its room, cuts it with '…' and puts the full string on hover (built on the existing `tab_strip::elided`). The Opacity/Fill scrub labels now elide to the room beside their field, so any translation shrinks instead of overlapping, in every locale. Documented the rule in docs/ui-design.md: wrap by default; beside a fixed control, elide; a redundant caption may drop on a narrow dock (the lock icons carry the meaning). Test: widgets::tests::elided_label_never_exceeds_its_room. Full photocraft-ui-egui suite: 2077 pass; fmt and clippy clean.
…torytold#208) (storytold#3624) The marquee already picks the selection mode from the modifier held at the press (shift adds, alt subtracts, shift+alt intersects) rather than constraining the shape. marquee_tests.rs covered add and subtract but not intersect; this adds that case, the one gap BUG-208-4 still names. The test fixes an existing selection, holds shift+alt at the press (then releases so the drag stays a plain rectangle, isolating the mode from the square/centre shape constraints) and drags a second rectangle. It samples three points -- one in the overlap, one only in the first rectangle, one only in the dragged rectangle -- so it passes for intersect and fails for replace, add or subtract. Test only; no production change. Co-authored-by: photocraft-agent UNCONFIGURED <unconfigured@invalid.local>
…ld#3638) (storytold#3640) With Auto Center Preview off, or in Classic Mode, dragging a crop edge or corner snapped the pointer, which grabs the handle up to the 8 px grab radius away from it. The snap line showed on the target while the frame edge stopped as far from it as the grab offset, so recropping toward the pixels a Delete Cropped Pixels-off crop kept left a transparent strip along the top and sides (or cut a few pixels off). The crop handle snap (snap_ui::crop_handle) now covers every resize: it snaps the handle of the frame crop_ui would make and moves the pointer by the difference, 1:1 without Auto Center Preview and holding the ratio on a corner as before. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ding between two (storytold#2946) (storytold#3572) anchor_factors resolved the x and y axes independently and with different matching rules (x looked for a capitalised "Left"/"Right" or the exact word, y for a lowercase "top"/"bottom" prefix). A misspelling matched one rule and missed the other, so "topleft" anchored top-centre and "TopLeft" left-centre -- anchors the command does not offer -- with no error. Match the anchor exactly against the nine documented values via one ANCHORS table; anything else is a BadParams error naming `anchor` and listing them, as storytold#507 settled for other command params. The nine valid values keep the exact factors they had, and an omitted anchor still means center. Test covers the nine offsets, the omitted default, and the misspellings from the issue ("topleft", "TopLeft", "TOPLEFT", "bottomleft", "bottomright", "middle", "", "NOPE"). Fixes storytold#2946. Co-authored-by: photocraft-agent UNCONFIGURED <unconfigured@invalid.local>
…Photoshop (storytold#3437) (storytold#3590) * Edit › Transform › Skew: corners and sides move along one axis, like Photoshop (storytold#3437) In Skew mode a corner drag scaled the box and the side handles moved freely. Checked in Photoshop: a corner moves alone, horizontally or vertically (whichever way the drag mostly goes); the top and bottom handles slide only horizontally and the left and right ones only vertically; Option moves the opposite corner or edge the other way. On editable type a corner slides its whole edge instead, so the text stays a parallelogram and remains live type. Free Transform is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Skew mode: a drag outside the box doesn't rotate it; ⇧ changes nothing (storytold#3437) Checked in Photoshop. A click outside still commits. 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>
…isplayInfo (storytold#3577) Photoshop 27.11 refused two of PhotoCraft's re-exported corpus files (ag-psd layer-mask/src.psd, psd-tools effects/stroke-without-vector-mask) with "Could not import the clipboard because an unexpected end-of-file was encountered". Both carry transparency in the merged image plus an alpha channel: PhotoCraft wrote two extra channels but named and described only one in resources 1006, 1045 and 1077. In every corpus file that has them (177, Photoshop's included), those resources hold one entry per extra channel, the merged transparency first. - Export: a "Transparency" entry leads the names and DisplayInfo when the merged image carries transparency and there are alpha channels. - DisplayInfo (1077) records are 13 bytes, as in all 166 corpus files that have one; PhotoCraft wrote and read 14 (the older 1007 layout), which shifts every channel after the first. - Import skips the transparency's entry, so an alpha channel keeps its own name and options ("Alpha 1", not "Transparency"). Corpus oracles and round trips are unchanged (io 148/170, psd-tools 237/309, Photoshop 137/258; every round trip renders the same). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…er the contour (storytold#3578) Measured on Photoshop 27.11 (scripted: a 128 px square with one glow, sampled pixel by pixel across its edges; outer and inner, both techniques, sizes 10 and 20, spread 0/40 %, choke 0/30 %, Range 50 %): - A Precise glow is 1 - D / (size + 1) beyond the solid spread/choke part, D being the centre-to-centre distance to the shape's edge. PhotoCraft's ramp ended at `size` and the default Range then doubled it, so it stayed solid for half the size and fell off steeply: up to 122 levels off. The raw coverage is now 0.5 at the edge falling to 0 at size + 1, so the shared Range step (v / range) gives Photoshop's ramp at the default 50 %; other Range values are unmeasured. - A centre-source inner glow is exactly 1 - the edge glow, taken after the contour and Range. PhotoCraft inverted before them: the softer centre glow was 115 levels off, the precise one 64. All 16 glow cases are now within 2 levels (10 were 58 to 122 off); softer edge and outer glows are unchanged. On the GPU: the same ramp in fs_mglow, and an "invert after the contour" mode (p0.y = 2) in fs_mfinish for glows only (Satin keeps its own invert). The 44 GPU parity tests pass. Corpus: the two Photoshop precise-glow oracles go from 0.31 / 0.37 to 0.05 worst error (curved anti-aliased shapes, still not under 2/255); nothing else changes. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ew (storytold#3720) (storytold#3731) With the pointer on the gray pasteboard, wheel/Alt-wheel/pinch zoom anchored on the pointer, so each notch slid the image away from it and out of the window. The anchor is now the nearest point of the image (clamped in document space, so rotated and flipped views work); over the image nothing changes.
…#3738) The menu items ran the engine commands with no params: Save Selection silently saved into a new alpha channel and Load Selection failed with "missing `channel`". The form dialogs already existed in view_cmds but the two ids were missing from wraps(), so they were never shown. Load Selection now only offers the layer mask when the active layer has one. Fixes storytold#3713 Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…3745) Since the shared docking migration (storytold#3097) the tool column's scroll area filled the toolbar's whole height, so a window taller than the tools pushed the colour chips, Quick Mask and Screen Mode to the bottom. Photoshop keeps them right under the last tool. The scroll area now shrinks to its tools and only a column taller than the window scrolls, with the chips kept at the bottom as before. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…on (storytold#3737) (storytold#3739) * test(ui): scaffold reported menu viewport coverage (storytold#3737) * chore: opt FredApps into public profile credit * Menus keep clear of the taskbar when the window position is unknown (Wayland) (storytold#3737) On Wayland winit can't report a window's position, so egui has no inner_rect/outer_rect, only the monitor size. work_area::visible_part then returned the whole window, and fit_window never maximized an oversized window, so long menus (Layer, View, Window...) were sized to the full window and ran under a bottom panel. Without a position, cut the window to the monitor size less the taskbar (the most that can be visible wherever the window is), and use the content size as the outer size for fit_window. Replaces the scaffold test with one that fails on main. --------- Signed-off-by: FredApps <153173854+FredApps@users.noreply.github.com>
storytold#3742) Adds the 1,287 rows the other catalogs have and Italian lacked (filter and adjustment parameters and choices, Camera Raw, Lighting Effects, History and Layers labels, metadata, dialogs), fixes mistranslations in existing rows (Facet, Gradient stops, Save As Preset, Canvas, Spot Color) and uses one term per concept (Predefinito, Traccia, clic, scorciatoia, filmato). Factors the Korean generated-label check into missing_generated_filter_labels and runs it for Italian too. Adds docs/localization-it.md and updates localization-parity.
Measured on Photoshop 27.11 (scripted: a 128 px square with one Bevel & Emboss, light from the top at 30°, sampled pixel by pixel across its edges; Chisel Hard and Soft, inner and outer, size 10 and 20, depth 100 and 300): - A chisel facet's slope is depth × size / (2 (size + 1)): it rises to the same depth × size / 2 as a Smooth bevel, over size + 1 px. PhotoCraft gave it the full depth, about twice as steep (facet 217 where Photoshop shows 188). - Inner Hard's ramp runs from half a pixel beyond the edge to size + ½, so the facet is flat right up to the edge pixel; Inner Soft starts a pixel further out (a steeper edge pixel) and is not blurred; Outer Hard ends a pixel further out; Outer Soft is a pixel shorter with a steeper edge pixel. Outer chisel bevels also shade the pixel where their ramp reaches zero (it still slopes), as emboss styles already did. All six inner and outer chisel cases are now within 1 level (they were 27 to 154 off); Smooth bevels are unchanged. Emboss and Pillow chisel are not modelled yet (they keep the blur). On the GPU: the same ramps in fs_mbevelh (Chisel Soft flag in p0.z), the emboss-only blur, and the outer reach-past flag in p3.x; the 44 GPU parity tests pass. Corpus: every Photoshop chisel oracle improves (bevel-inner-chisel-hard 20.1 -> 6.5 % differing pixels, outer-chisel- hard 13.6 -> 5.2 %, inner-chisel-soft 22.3 -> 9.3 %, emboss and pillow chisel a few points); pass counts unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
storytold#3757) The Character panel (and the Properties Character section) gains Photoshop's row of eight OpenType buttons: Standard Ligatures, Contextual Alternates, Discretionary Ligatures, Swash, Stylistic Alternates, Titling Alternates, Ordinals and Fractions. Each shows pressed while its feature is on at the selection (else the layer) and toggles it through the existing type.openType.* command, on the same scope as the other Character controls (the selected characters while typing, else every selected type layer). Scorecard TYPE-218-5 stays partial: the Numerals and Stylistic Sets submenus are still missing. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
…storytold#3312) photocraft_io::named_for_file(doc, name) serialises the document under the file it is being written to, so a .pcraft manifest carries the identity the save is about to adopt rather than the previous one — the same rule every other format follows on import, where document.name comes from the file it was read from. A bare extension names no file and changes nothing. Applied once, in the .pcraft branch of export_stages, which export and export_with both funnel through: the desktop Save (services.rs:242), Export (services.rs:384), the web build (web.rs:355) and the engine file command (file_cmds.rs:295) are covered by one site, and the previews rendered before the bytes describe the same document as the manifest. Scope per review: the automation funnels (files::save / save_bytes) already landed in storytold#3319 with their own named_for_output, so this carries no crates/automation change; its scenarios are covered by crates/automation/tests/native_save_name.rs on main.
switch87
force-pushed
the
manifest-name-follows-save
branch
from
October 12, 2026 09:01
0fd2795 to
cf130a6
Compare
|
The PR description looks broken overall: it contains stray terminal escape characters (colored diff output), pasted command output, and missing inline code/names, which makes it hard to read. Could you clean it up and rewrite it as plain Markdown? |
Owner
Author
|
Thanks for flagging this, and sorry for the mess. I've rewritten the description as plain Markdown. Note that the branch this PR points at doesn't contain the JXL commits, so the diff shows unrelated upstream history; the actual work on JPEG XL read support is happening upstream in storytold#2789. |
switch87
pushed a commit
that referenced
this pull request
Oct 24, 2026
…torytold#7822) * Color pickers: pick on press and follow small drags (storytold#7793) The Color Picker dialog's field and slider, and the Color panel's field and hue strip, only applied the colour on dragged()/clicked(). egui holds dragged() back until the pointer moves past its click threshold (~6 pt) or is held a while, so a press picked nothing at first and small adjustments jumped. Add widgets::pick_pos, which reads the pointer from the frame the button goes down (is_pointer_button_down_on), and use it on all four surfaces. The Color panel now restyles selected type only when the picked colour changes. Tests: color_picker_ui::field_and_slider_pick_on_press_and_follow_small_moves, panels::color_tests::color_panel_field_and_hue_strip_pick_on_press. * #1 Reverted accidental scoreboard change when regenerating it storytold#2 Fixed color picker not checking for pointer press, but any mouse click * Update crates/ui-egui/src/widgets.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Color pickers: rustfmt pick_pos * test(ui): use main's rgb_to_hsb in the colour picker tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds read support for JPEG XL (
.jxl) images to PhotoCraft, using the pure-Rust decoderjxl-oxide.Format::Jxlvariant.FF 0A) and the ISOBMFF container (JXLbox signature).crates/codecs/src/codecs/jxl.rs, behind an optionaljxlCargo feature (optional dependency onjxl-oxide).FormatCapsfor JXL: read is enabled when thejxlfeature is on, at 8 and 16 bits per channel. Write is intentionally unsupported until a pure-Rust encoder exists..jxlis added to the Open dialog's extensions.Limitsbefore allocating.How it works
The
jxlfeature is optional. Official builds would enable it; without it,Format::Jxlstill exists so files are recognized, but opening one reports that JPEG XL support isn't compiled in.Status
This PR lives in my fork and is not ready for review. The branch it points at (
manifest-name-follows-save) does not contain the JXL commits, so the diff shows unrelated upstream history instead. JPEG XL read support is being worked on upstream in storytold#2789 (open PRs storytold#2868 and storytold#3841), so this is kept only for reference.Refs storytold#2789