mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-10 20:32:31 +00:00
dee552a817100cd8a4e4e2ca00e1bdb461a584b5
9303
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dee552a817 |
Merge pull request #9284 from davidmann4/fix/crop-click-snap
Crop: a click on the frame or a handle never snaps it (#9283) |
||
|
|
5d10956221 |
Merge pull request #9261 from davidmann4/fix/image-size-resolution-range
Image Size: refuse a resolution outside 1..30000 ppi instead of clamping it (#9260) |
||
|
|
7122a21fec |
Merge pull request #9670 from davidmann4/fix/svg-markup-check
SVG: a DTD entity can't hide nesting from the depth check (#9669) |
||
|
|
03f9f53b13 |
Merge pull request #9213 from davidmann4/fix/crop-hidden-resample-budget
Crop W x H budgets the hidden pixels it resamples (#9212) |
||
|
|
b81932c64a |
Merge pull request #9224 from davidmann4/fix/crop-drag-tool-switch
Crop: a tool switch during a frame drag ends the drag (#9223) |
||
|
|
b7b61addcd |
Merge pull request #9276 from davidmann4/fix/9275-crop-esc-mid-drag
Crop: Esc in full screen cancels a drag on the default frame (#9275) |
||
|
|
908afc7bb3 |
Merge pull request #5533 from davidmann4/fix/5532-blend-separators
Layers: separators between the Blend Mode groups (#5532) |
||
|
|
5b9900e52e |
Merge pull request #9310 from davidmann4/fix/crop-drag-autoscroll
Crop / Perspective Crop: a drag held past the canvas edge scrolls the view (#9307) |
||
|
|
0b9a113414 |
Merge pull request #9672 from davidmann4/fix/svg-fallback-cmap-cache
SVG: the fallback font search reads each font's character map once (#9671) |
||
|
|
3d60ad01f9 |
Channels: the panel menu has the channel commands, New Spot Channel among them (#11451) (#11557)
* Channels: the panel menu has the channel commands, New Spot Channel among them (#11451) The Channels panel's menu had no channel items, so New Spot Channel... and the rest could only be reached from a row's right-click menu. It now lists New Channel..., Duplicate Channel, Delete Channel, New Spot Channel..., Merge Spot Channel, Channel Options..., Split Channels and Merge Channels..., each acting on the targeted channel and greyed where it can't: Duplicate takes a single colour, alpha or spot channel, the composite of a one-channel image, the Quick Mask or a targeted layer mask. Channel Options... in Quick Mask mode now opens the targeted alpha or spot channel's options, not Quick Mask Options. The panel menus run their commands through the menu bar's enabled rules, so the Layers panel menu greys its dialog items the same way too. The labels are in every catalog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Channels: New Channel... and Merge Channels... ask first, from the panel and row menus (#11451) The panel menu's Merge Channels... ran channel.merge with no parameters, and the engine merged the first grayscale documents into RGB and closed them with no choice; New Channel... added a channel straight away. Both now open a dialog when called without parameters, as New Spot Channel... and Channel Options... already did: New Channel... reuses the Channel Options dialog (name, colour indicates, colour and opacity), and a new Merge Channels dialog picks the mode and the open grayscale document for each channel. The channel rows' menu had the same two items and goes through the same path. With parameters (scripts, the New button) both still run at once. The Merge Spot Channel test now checks pixels, so a delete couldn't pass it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Channels: Merge Channels waits for three grayscale documents and the dialog offers only modes they fill (#11451) With two grayscale documents open, Merge Channels... was enabled and its dialog repeated a document for the third channel; OK then failed in the engine and the dialog closed. The engine's rule now needs three documents of one size (no mode merges fewer), so the menus grey the item below that. The dialog offers only the modes the open documents can fill (RGB and Lab from three, CMYK from four), greys OK with the reason while the picks repeat, keeps itself open when OK is refused, and comes back with the engine's message if the merge itself fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Channels: the Merge Channels dialog keeps its picks by document id (#11451) The dialog kept the picked source documents by tab index, and background work can still run while it is open, so a document moved or closed in the meantime could leave another one at a picked index; OK would then merge, and close, the wrong document. The picks are now document ids, resolved to the current tabs when OK is pressed: a closed pick, or one that is no longer a grayscale document of the same size, is refused with the reason and the dialog stays open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9b02eac2c4 |
Export to files: a Custom JPEG quality in Artboards, Layers and Layer Comps to Files (#11481) (#11558)
* Export to files: a Custom JPEG quality in Artboards, Layers and Layer Comps to Files (#11481) The engine commands already took `quality` (0..12), but their dialogs offered no way to set it. Each dialog now has a Quality row with a Custom checkbox, shared with Batch: quality is sent only for JPEG with Custom on, so an export left alone writes exactly what it wrote before (the source JPEG's own quality, else the encoder default), and PNG, WebP (still lossless), PSD, TIFF and BMP never get one. Alt+Reset restores the Custom state too. The fields follow the reference dialog's order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: the export quality tests go through the dialog's own controls (#11481) Custom and Quality are now ticked and typed through their widgets, a new test switches one dialog from JPEG to PNG and WebP and back and checks the Custom quality survives, and Alt+Reset is clicked through the same path the app uses instead of calling reset_form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
88704d2350 |
Merge fix-up: the turned crop's hidden pixels after Layer 0 replaces the Background
Since #10867 the non-deleting crop turns a 2000 px layer to 2001 px bounds, so the budget refusal names 200100 x 200100 px. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4b61c8ef34 |
Merge commit '9bef3b2dc8b84c1b6cefc8ce0dc2b01d089cd9e8' into HEAD
# Conflicts: # crates/algo/src/resample/tests.rs |
||
|
|
5b1b6cf13d |
Merge fix-up: check entity markup in main's bounded embedded-SVG parse too
nesting_depth now returns a Result; main's parse_image_bounded_with gets the same entity-markup rejection as the other callers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d563b3100b |
Merge commit '9bef3b2dc8b84c1b6cefc8ce0dc2b01d089cd9e8' into HEAD
# Conflicts: # crates/engine/tests/it/svg_smart_object.rs |
||
|
|
1d36dc5f0d | Merge main into fix/5532-blend-separators | ||
|
|
82f5a11067 |
Address review: separators on main's ComboBox, without #5501
Merge main (
|
||
|
|
9bef3b2dc8 |
Merge pull request #6088 from davidmann4/fix/snapshot-followups
History snapshots: follow-ups to #3567 (#6087) |
||
|
|
0f15223e18 |
Merge pull request #7739 from davidmann4/fix/crop-swap-button-turns-frame
Crop: the options-bar swap button turns the frame instead of shrinking it (#7738) |
||
|
|
bac5c2802b |
Merge pull request #9405 from davidmann4/fix/svg-guard
SVG: a drawing the renderer panics on is an error, not a crash (#9404) |
||
|
|
a46eb590a9 |
Merge pull request #9410 from davidmann4/fix/svg-filter-cost-9409
SVG: refuse filters too costly to render (#9409) |
||
|
|
e59dcc9c44 |
GPU plan: cache adjustment LUT rows between plans (#11498) (#11539)
* GPU plan: cache adjustment LUT rows between plans (#11498) adjustment_program rebuilt the Levels/Curves tone tables on every plan (twice per refresh), about 1-2 ms per Levels layer each time. Keep the rows of Levels, Curves, Color Balance and Hue/Saturation ranges in a 32-entry most-recently-used cache (photocraft_compose::lut_rows, at most 32 x 3 x 16 KiB = 1.5 MiB), built outside the lock. Entries are keyed by a u64 hash of exactly what each row builder reads, fields destructured in full and floats by bits (-0.0 and 0.0 stay apart). Hue/Saturation master sliders don't add entries; Hue/Sat ranges and Color Balance don't depend on depth. Edit > Purge > All drops the cache, and a filled cache enables it. Release, per call: Levels 8/16-bit 1.10 -> 0.003-0.018 ms, Curves 0.16 -> 0.018, Color Balance 0.11 -> 0.001-0.015, Hue/Sat ranges 0.05 -> 0.008-0.018. The tables are identical, so rendering is unchanged. Tests: cached rows match fresh tables bit for bit at 8/16/32-bit, hits and LRU eviction on a local cache, key coverage, and Purge All releasing the cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * compose: rustfmt the LUT row cache tests (#11498) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU plan: test that plans read cached LUT rows and that curve points key them (#11498) Plant rows no builder makes under a fresh key and check the planner returns them, and check that moving a Curves point changes its key (review on #11539). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * compose: prevent in-flight LUT builds from undoing purge --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <FredApps@users.noreply.github.com> |
||
|
|
e4e2607c85 |
Preview spot inks over the RGB/CMYK composite as an overprint (#10276)
* PSD: a flat file's named extra channels are not its transparency A PSD without layer records had its first extra channel read as the merged transparency, so the first spot or alpha channel of a flattened document vanished and its ink punched a transparent hole in the image (psd-tools cmyk-alpha-spot.psd, cmyk-spot.psd). When the alpha channel names (1045, or 1006 when 1045 is missing) name every extra channel and the first is not "Transparency", there is none. The import also reads channel names from 1006 when 1045 is missing, and a spot whose DisplayInfo colour space is unknown takes its ink from the Alternate Spot Colors (1067). * channel_map: read 1067 records with as_chunks * Preview spot inks over the RGB/CMYK composite as an overprint Visible spot channels of RGB and CMYK documents print over the canvas composite with the Multichannel ink formula, in Channels panel order, instead of a tint; alpha channels and Quick Mask still tint over them. Spot channels open visible, and the PSD thumbnail (1036) prints them. flatten, export and the merged image stay free of spot inks. Ink colours go through the document's CMYK profile, and into an RGB document's own profile. Refs #10249 * channel_view: test solidity over a gray picture * Thumbnail: matte only inked pixels; test the merged planes directly Review feedback on #10276: pixels without spot ink keep their transparency in the thumbnail buffer, and the thumbnail test checks the saved merged process planes instead of a re-imported flatten. * Thumbnail spot inks: sample big boxes on a 4x4 grid Review feedback on #10276: reducing each spot channel no longer scans the whole document; a thumbnail pixel averages at most 4x4 samples, so a huge document costs about h x 4 row reads per spot. * Spot overlay: composite a proxy for the first draw of big documents The overprint needs the composite under the inks. Rendering the full-resolution composite to sample every factor-th pixel cost ~1 s for a 24 MP CMYK document. A proxy document picks the same pixels (nearest-neighbour downsample) for about 1/factor² of the work. Documents with layer effects, vector masks or feathered masks keep the full-resolution path. * Proxy: downsample rows in parallel; the overlay proxy leaves out unused channels The spot overlay's proxy shrank the spot channels, selection and Quick Mask that an RGB or CMYK composite never reads: about two thirds of its cost. downsample now reads its rows in parallel, 64 at a time. * Spot overlay proxy: keep the full-resolution composite under uncached pattern fills A pattern fill renders its tiles at their own size on a proxy, so the proxy would sample other pattern pixels. Unless the fill's matching cache is shrunk with the proxy, use the full-resolution composite. * Spot overlay proxy: hidden layers don't decide the composite path Hidden layers and the contents of hidden groups are not composited, so a hidden feathered mask or pattern fill no longer forces the full-resolution composite (the same rule as proxy_faithful). * Update crates/compose/src/multichannel.rs Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> * Spot overlay: only inked pixels replace the canvas; inks through the display transform Where every visible spot is 0 the overlay stays transparent, so the normal canvas (display LUT, proofing, 32-bit preview, full resolution) shows; an empty spot renders no composite. Printed pixels go through the canvas display transform (sRGB encoding of linear composites, monitor profile, Proof Colors), keyed into the texture. Ink colours come from one InkSpace per render: the document's profiles are parsed once, not per spot per damage rect. * test(compose): cover hidden spot preview and document approximation * fix(ui-egui): preserve solo spot channel display when process colours are hidden Clicking a spot channel row in the Channels panel targets the channel alone and hides all process colour channels (`vis == 0`). Overprint inks were previously extracted from `overlays` before checking `vis == 0`, leaving `overlays` empty and causing the solo channel view to fall back to an active layer mask (or return `None` if the layer had no mask). Keep the spot channel in `overlays` so `vis == 0` can select it as the solo grayscale picture, separating overprint inks only when process colours are visible (`vis > 0`). Adds a regression test targeting an existing spot channel in RGB and CMYK on an unmasked layer and editing it. * channel_view: document the spot overprint preview as an approximation * test(ui-egui): solo spot channel wins over an unrelated layer mask * channel_view: sRGB spot inks, lazy display transform, inked-area composite Review fixes for the spot overprint preview: - RGB spot colours are sRGB, like the CMYK, Lab and gray ones: InkSpace now converts them into an RGB document's own profile too. In a linear (EXR/HDR), Adobe RGB or ProPhoto document an RGB ink showed the wrong colour (brightened in a linear one once the canvas sRGB-encoded it). - The display transform is looked up only when the view is redrawn and inks are shown, not every frame before the cache check; display_transform_for borrows the proof state (proof_ref) instead of cloning a default CMYK profile. - Visible spot channels without ink, with nothing else shown, draw no texture: render returns None and ensure remembers it, instead of uploading an all-transparent document-sized texture. A damage re-render that finds nothing clears its rect, so erased ink never lingers. - Printing a few inked pixels composites only their bounding box when that is cheaper than the whole (proxy) composite, and a cached composite is borrowed instead of copied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e55e2f3577 |
Place decodes and fits in the background: dropping a large file no longer freezes the window (#10147)
* Place decodes, fits and renders in a background job File > Place Embedded / Linked started with Session::start, and Session::start_place_bytes for bytes with no path, now run the decode, flatten, resize and SVG render on a worker (prepare_place) and insert the layer when the job is applied (apply_place). Inline callers (execute, the CLI, headless MCP, actions) behave and return exactly as before. Part of #10031. * Dropped and placed files no longer freeze the window on desktop With background jobs on, File > Place and files dropped on the canvas start the place as a job (JobsUi.place). Free Transform begins when the layer has landed, a failed or cancelled place is reported and the drop queue goes on, and a drop that arrives meanwhile waits for it. The place shows its progress in the status bar instead of the modal dialog, so the view stays usable. Part of #10031. * Tests: compare place job results without the layer id Layer ids come from a counter shared by every session, so a job and an inline place in two sessions never get the same one. * Dropped files stay with the document they were dropped on Each queued canvas drop now carries its document's id. A file is placed only while that document is active: files dropped on a document in another tab wait until it is back in view, and are discarded with a message when it is closed. Web drops, read by the shell after the drop, go to the document the drop landed on. Before, the next queued file went into whichever document was active when the previous place finished, failed or was cancelled, in the background and inline paths alike. * Remove the unused file_open import * fix: address background placement review feedback * fix: preserve drop targets and interrupt placement pixel stages * fix: qualify placement inbox document identity * test: wait for background placement in app drop regression * fix: retain queued drops while external document jobs run * test: clone rendered pixels from drop-managed buffers * test: cover band rows channels and queued-drop resumption * Place decodes and fits in the background: dropping a large file no longer freezes the window Placing a file decoded, flattened and resized on the UI thread; a large PNG froze the window for seconds (#10031). `place_bytes` is split into a session read, `prepare_place` (decode, flatten, fit, translate, SVG render at placement scale) and `apply_place`, and runs through `jobs::run`, so File › Place and canvas drops run on a worker with progress and cancel. The window follows the job: status-bar progress, Free Transform when the layer lands, and drops queued per target document. Review fixes on the way: - a path and byte drop queue entry records its target DocId, so files dropped on one document never land in another after a tab switch, and closed documents discard theirs; - the closed-document notice stays up for a frame instead of being cleared by the next queued placement; - the queue is bounded (32 files / 256 MiB) so drops waiting on another tab cannot grow it without limit; - flatten and resize stop at the job's interrupt, and a place skips the modal progress dialog only when the status bar actually shows its progress; - the placement's `doc` is kept only for SVG input, through the importer's `is_svg_input`; - the interrupted-composite test uses an opaque fixture that varies by row and channel, and checks the composite against the layer's stored pixels. * Place reads its file in the job, and refuses a huge canvas as before file.placeEmbedded and file.placeLinked read the whole file in the command before the job started, so a path dropped on the canvas or picked in File > Place still blocked the window for the read. The place now carries the path and reads it in prepare_place: on the job's worker in the background, on the calling thread inline (wasm, background jobs off), with the same read_file limit and error text. Whether the source is a vector one is decided once it has been read. The job's flatten check returned Other("the document canvas is too large to flatten"), where the place used to give flattened_checked's BadParams for file.place naming the canvas size. Both now share check_flattenable, so the old error type and message are back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Path drops aren't capped, and a discarded drop wakes the queue The 32-file cap on the drop queue counted path drops, which hold no file contents while they wait, so dropping 40 files on the canvas placed 32 and refused 8 where main placed them all. The count cap now applies to browser drops only (each holds its whole file); the byte budget still bounds those. When place_next_dropped discarded a closed document's files it returned without asking for another frame, so another document's queued files waited for the next input event. It now requests a repaint when files remain, keeping the one frame that shows the notice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5c157e8400 |
Build the GPU compositor off the UI thread (#9969)
* Prepare GPU compositor in background * Add compositor startup performance scenarios * Generate scorecard in scratch CI * Fix pipeline build type inference * Address compositor startup review and document measurements * Run startup performance checks on scratch CI * Remove scratch performance job * Count failed GPU attempts in frame timing * Make shader fingerprint test portable * Address review: run first-document scenario as a guarded group; share COMPOSITOR_BUILDING constant * Handle compositor startup benchmark failures * Fix frame timing after upstream merge * Keep GPU startup failures on CPU fallback * Record the failed GPU attempt of a deferred redraw, and bound --reps A redraw deferred after a failed GPU composite carries that attempt's time to its end, where it is recorded once beside the CPU time. Perf::record clears the attempt so a direct CPU refresh doesn't inherit the last one's. perf_scenarios caps --reps. * Refresh cached canvases after compositor build * Expose canvas cache key to GPU completion handler * ci: drop the GPU compositor probe job AGENTS.md: feature and fix PRs don't add or change CI, and app workflows only call the shared craft-common-actions workflows. The probe example stays as a manual tool (cargo run -p photocraft-gpu --example compositor_build_probe). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c441fd350c |
perf(paint): sampled, textured and wet-edge dabs rasterize on all cores (#11369)
* perf(paint): sampled, textured and wet-edge dabs rasterize on all cores (#11350) The general dab loop runs its rows on the thread pool, with the same pixels as serially. It reaches the pool from about 16,000 pixels, a quarter of the round path's threshold. * test(paint): the parallel-vs-serial depth test checks colour, not alpha, for painted pixels The opaque background's alpha (1.0) is 0.5 away from 0.5, so the check passed even if nothing was painted (CodeRabbit on #11369). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(paint): read pixels with as_chunks (clippy chunks_exact_to_as_chunks) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6326117c53 |
Web build: big operations that won't fit fail with an error instead of trapping, and a trap says so (#10129)
* Raster: ask the allocator whether a big operation fits; PSD save reserves its whole output New photocraft_raster::headroom: ensure(need) tries to reserve the need plus a margin and frees it at once (web only; native always passes), below(bytes) tells whether less than that can be allocated, and simulate_available fakes the answer for tests and diagnostics. PsdFile::to_bytes reserved only the pixel data, so embedded smart-object data and other tagged blocks grew the output by doubling, which on the web is one unrecoverable huge realloc. It now reserves the exact encoded length. * Big operations that won't fit in memory fail with an error instead of trapping Place, Image Size, a smart-object re-render and Save now ask the allocator whether their estimated need can be allocated before they start (photocraft_engine::memory_gate, estimates per operation, photocraft_io::estimate_export_bytes for saves). A refusal changes nothing and says what the operation needed and what to do (save, Edit > Purge > All, close documents). A standing notice warns when less than 512 MiB can be allocated, with hysteresis, so the user can save before anything traps. Notices can now be sticky, which keeps them past the auto-hide delay. * Web build: a trap says so instead of freezing the page, and ?free-mb simulates low memory index.html holds a hidden, static 'PhotoCraft stopped' message with a Reload link. web.rs reveals it when the module traps (the window error event, or an unhandled rejection from a spawned future), tells running out of memory from an internal error by the panic message a chained panic hook kept, and skips the Leave site prompt once stopped. ?free-mb=N makes the memory gates and the low-memory warning see only N MiB, for checking them in a browser. * Docs: web build memory handling in the gaps list and the parity assessment * Engine: use is_multiple_of in the megabyte formatter * Cargo.lock: the web shell depends on photocraft-raster * Web boot page: carry the stopped message too, as the test that keeps the two pages equal requires * Save check counts one copy of the document, and runs once per save The encoded file's buffer is reserved fallibly, so a save it doesn't fit in fails with an error instead of trapping; counting it as well refused saves that would have fitted. On the background-save path the check ran in write_document and again in start_save, which other callers reach directly; it now runs only in start_save there. * Review fixes: warning check allocates nothing, PSD merged image counted, automation saves checked The low-memory warning's periodic check reads the size of linear memory instead of reserving a block: wasm memory never shrinks, so a trial every few seconds raised the high-water mark by up to 768 MiB for nothing. The operation checks keep their trial reservation. PSD and PSB saves always build a canvas-sized merged image (a placeholder without maximum compatibility) and hold it raw and encoded; the save estimate now counts it. The synchronous automation save checks memory before encoding, as File > Save does, and its test asserts the memory error. The trap listeners go in before the app is built, so a trap while building it shows the stopped message too, and a memory access out of bounds reads as unexpected rather than as running out of memory. * Cover layered PSD preflight limitation * Fix layered PSD test iteration * Docs: layered PSD preflight limitation in the gaps list * Web OOM gate: gate direct exports, truthful advice for unbounded PSD sources (#10024) * Web OOM gate: estimate layered PSDs per layer instead of refusing every real PSD The PSD check admitted a file only when its color-mode data, image resources and layer section were all empty. Every Photoshop file has image resources (at least the resolution), so on the web nearly every real PSD was refused for Place, PSD smart objects could not render, and Image Size was refused for documents holding one, whatever the free memory (u64::MAX never fits). The color-mode data and image resources hold no pixels, so they are now only skipped. A layered file reads its layer count from the layer info (signed, absolute value) and adds a canvas-sized surface and mask per layer to the flat estimate. Files that are not 8-bit RGB or grayscale, keep their layers in a Layr tagged block, or whose sections can't be read stay unbounded and refused, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Web stopped page: match trap messages ignoring case Safari reports the allocation-failure abort as "Unreachable code should not be executed" and Firefox says "out of memory", which the case-sensitive matching missed: the trap was not recognised, or was shown as an unexpected stop instead of running out of memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Export estimate: count the merged image OpenRaster writes The ORA writer keeps the layers but also flattens the document for mergedimage.png, a 16 B/px composite of the canvas, which the estimate left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a3c0b7258c |
Select Subject: keep parts that run off the image edge and detached parts near the subject (#10160)
* Select Subject: keep parts that run off the image edge and detached parts near the subject The backdrop pass trained its colour model on the whole frame outside the first result's box and fixed it as background, so a subject cut off by the edge (a portrait's torso) taught the backdrop its own colour and was dropped. Frame pixels of a side that the rest of the frame cannot explain are now left out of the model and left to the cut; a floor or table running up the sides stays backdrop. The first pass also dropped every part under a fifth of the largest, losing a head behind a thin neck or a tail. Non-speck parts next to the main one are kept. * Select Subject: stray edge pixels stay backdrop; cache test checks the recompute A lone odd pixel in the frame (noise, a dead pixel) is not the subject running out of the picture: only patches of at least half a frame square are released. The #2865 cache test asserted a bit-identical selection after painting a corner pixel. One changed pixel can move the cut's edge slightly (the original algorithm already did on other noise), so it now checks that the mask was recomputed and still selects the subject. * Test subject selection against edge and nearby clutter * Keep narrow edge clutter out of subject selection * Preserve cropped subject components at image edges * Keep edge poles and diagonal streaks out of subject selection A detached part counted as near the subject when its bounding box overlapped the main part's margin box. A thin diagonal streak from a corner (a cable, a rail) has a box that reaches the margin without any of its pixels doing so, and was kept as part of the subject. A part is now near only when some of its pixels lie inside the margin box. A patch that runs along an image edge for two frame widths was released from the backdrop model whatever its colour, so a pole or door frame along a side edge next to the subject's shoulder was left to the cut, which could grow the selection along it. A wide patch is now released only where its extent along the edge overlaps the first-pass blob's margin box. Beside the subject that box is cut out of the frame, so the pole only remains above or below it and is not released, while a torso cut off below a head (the case the release exists for) still lies under the head's box. Requiring the subject's colour for wide patches too, or requiring them to touch the box, would drop exactly that torso: its colour is not in the head-only blob and it is far from the head's box. The margin box computation moves into margin_box so subject_at_edge and its tests share it. Tests pin the diagonal streak, the pole beside the shoulder (on all four edges, with the cropped torso still released) and clutter that is too far or too small. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f127459ca3 |
perf(algo): build Remove Tool target masks with a bounded separable dilation (#10179) (#10194)
* perf(algo): build Remove Tool target masks with a bounded separable dilation (#10179) * docs(algo): remove::dilate no longer always runs two full-grid passes * test(algo): shrink the square dilation tests Exhaustive masks up to 3x3 instead of 4x4, drop the 300x200 named grid (the smaller grids already clip at r = 200, so its oracle-cost guard goes too), and run the path-choice tests on 600x400 and 240x240 grids. * test(algo): a mask with more set cells than bounds keeps matches the oracle Covers the branch where bounds drops its cell list (more than KEEP set cells) and the direct path rescans the box. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fcb4f4e494 |
Grow and Similar: budget the colour index and report allocation failures (#9825)
* Select: Grow and Similar index colours under a byte budget and fail instead of aborting ColorMatcher::new returns a Result, the colour tree is a flat node array reserved at its exact size, and Grow/Similar's mask and frontier stack use fallible allocation so a failure keeps the selection. * Select: a 1 GiB colour index on 32-bit, and Sample All Layers reports a failed composite The web build's 256 MiB index budget (about 4.2M colours) refused Grow and Similar on a noisy 24 MP photo whose index fits easily; 1 GiB admits every distinct colour of such a photo. The 64-byte charge per colour is an average, not a worst case, so the comment now says so, and the point list the tree keeps gives back its doubling slack once the dedupe set is gone. Sample All Layers renders the composite inside compose, so the selection tools now pass an allocation checkpoint before it, like the other commands that hand big work to another crate, and a failure there keeps the selection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3317aede3b |
Fix Paint Bucket target with Gradient-style crosshair (#7994)
* Mark Paint Bucket fill target with a lower-right crosshair * Unify Paint Bucket artwork and crosshair in one cursor bitmap * Match Paint Bucket target crosshair to Gradient cursor * Keep Paint Bucket cursor errors concise and fix Windows Clippy * Simplify blend preview condition for strict Clippy * Fix deprecated IDs in Linux bucket cursor test * Paint Bucket: a cursor error clears when the cursor recovers The failure branch wrote its error to the status bar, but nothing cleared it after a later scale worked, and the same failure could not report again. The open dialog stays until it is closed: it reports what happened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: envy-ai <bart.kelsey@osumc.edu> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
405b56c3c2 |
Merge pull request #11320 from davidmann4/integration-batch-17
Integration batch 17: 14 epic PRs |
||
|
|
16eef63294 |
Type: a duplicated type layer is renamed after its new text (#9023) (#10352)
* Type: a duplicated type layer is renamed after its new text (#9023) A type layer's name follows its text until the layer is renamed (#483), but only while the name is exactly the text. A duplicate is "Title copy" (or "Title copy 2"), so editing its text kept the old name. A name that is the text plus Duplicate Layer's " copy" or " copy N" now counts as named after the text too, so the copy takes its new text's name; any other name is still kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Type: a copy of a "… copy" type layer follows its text too (#9023) Duplicate Layer names a copy of "Draft copy" "Draft copy 2", so the check now compares the names' roots (the name before " copy" or " copy N") rather than adding " copy" to the text's name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Type: a copy keeps its "… copy" name until its text changes (#9023) A type layer named after its text was renamed after every edit through the type helpers, so with "Title copy" now counting as named after its text, Change Type Size, a style-only or kerning edit, Convert to Paragraph or Point Text, Area Type and Type on a Path renamed the copy "Title". The name now follows the text only when the text changed; a layer named exactly after its text gets the same name as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Type: a "… copy" name the user typed is kept when the text changes (#9023) A type layer named "Title copy 2" by hand, with "Title" as its text, counted as a Duplicate Layer copy and was renamed after its next text edit. The document now remembers the names the user typed (Rename Layer, a name Duplicate Layer's dialog didn't offer, type.edit's name) for the session, and such a name is kept. A name Duplicate Layer made still follows the text, and so does one an undone rename brings back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Type: a name given in the Layers panel or by type.create is the user's (#9023) The Layers panel renames in place with layer.setProps, and type.create takes a name: neither recorded it in typed_names, so a copy-shaped name typed there ("Title copy 2" on text "Title") was replaced by the next text edit. Both now record it, as Rename Layer and type.edit's name do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Type: a name typed for a duplicate into another document is the user's (#9023) Duplicate Layer into a new document and Copy Layers to Document take a name but returned without recording it, so a copy-shaped name typed there was replaced by the next text edit. A name other than the copy's own is now recorded in the destination document, as the in-document duplicate does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
daa9a01955 |
Release: refresh contributor credits for each release (#8737) (#11426)
About > Contributors still showed a snapshot from 2026-10-10, because nothing regenerated contributors/contributors.json before a release. Opt in to the shared refresh from craft-common-actions: a read-only credits job regenerates the snapshot for the release commit, and the shared release workflow installs it before every source build. The snapshot is not committed back. |
||
|
|
4943e2c3da |
fix(ui): About credits table columns can be dragged wider or narrower (#11182)
* fix(ui): About credits table columns can be dragged wider or narrower The Contributors and Models tables already open fitted to the dialog, the name column taking the spare width, and scroll sideways only when there is not room for every column. Their columns were fixed, though: no edge could be dragged. They are resizable now, down to 28 points (the flexible column to its minimum). Widening a column grows the table, as in a spreadsheet, and only then does it scroll sideways; cells clip instead of overlapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * About: refit credits tables when what they measure changes The columns became resizable, so the table keeps its own widths. A longer name, a changed cell or a resized window left the old widths in place and clipped text. The table now goes back to its fit when a column measure or the clip rect width changes, which scrolling never moves. Adds tests that drag every column edge, change the fit and scroll a widened table, as CodeRabbit asked for the drag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: a fitted credits table fills the window exactly The table ends on the window edge, which the layout rounds to whole points, so the check allows that edge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5e7a097d10 |
Merge commit '20d561715b' into integration-batch-17
# Conflicts: # crates/engine/src/cmd/transform_cmds.rs |
||
|
|
37f9d3b7d4 |
Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts: # crates/codecs/src/codecs/png.rs |
||
|
|
33e20b5707 |
Add recipe-driven checks for PSD exports (#10307)
* Add recipe-driven PSD export checks * Exercise PSD recipe runner with the headless CLI * Make PSD oracle tests isolate temporary paths * Keep inspect output unchanged; PSD write runner as an xtask library - info --detail adds text runs, anti-aliasing and full layer-effect settings; the control channel and MCP inspect output is unchanged. - The CLI test uses xtask's new library target instead of #[path] includes. - The runner records revision and dirty state as unknown outside a git checkout. * psd-write-oracle: name the layer in each check, document versioning, add recipe tests * Clean up PSD oracle test fixtures on failure with tempfile::TempDir and add docstrings |
||
|
|
7f151b6542 |
algo: filter admission counts no more workers than tiles
Since
|
||
|
|
f54d605e2c |
Add recipe-driven checks for PSD exports (#10307)
* Add recipe-driven PSD export checks * Exercise PSD recipe runner with the headless CLI * Make PSD oracle tests isolate temporary paths * Keep inspect output unchanged; PSD write runner as an xtask library - info --detail adds text runs, anti-aliasing and full layer-effect settings; the control channel and MCP inspect output is unchanged. - The CLI test uses xtask's new library target instead of #[path] includes. - The runner records revision and dirty state as unknown outside a git checkout. * psd-write-oracle: name the layer in each check, document versioning, add recipe tests * Clean up PSD oracle test fixtures on failure with tempfile::TempDir and add docstrings |
||
|
|
834e253377 |
Canvas: a right-click that moved a few pixels still opens its menu (#10183) (#10351)
* Canvas: a right-click that moved a few pixels still opens its menu (#10183) egui reports a secondary click only within its click distance of the press, and a high-DPI mouse moves that far just from the button going down: the right-click then counted as a drag, and the Brush Preset picker, the canvas tool menus and the layer menu did not open. A right press released over the canvas now opens them however far the pointer moved, unless a gesture used that drag (Alt+right brush resize, a right-erase stroke). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Canvas: the Hand pans with the left and middle buttons, not the right (#10183) The Hand tool panned on a drag with any button. Now that a right-click that moved still opens the canvas menus, a right drag with the Hand panned and then opened its view menu on release. In Photoshop the Hand pans with the left button and its right-click is the view menu, so a right drag now opens that menu without panning; the left button, Space and the middle button pan as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Canvas: a right-click moved and released in one frame opens its menu (#10183) When the move past the click distance and the release reach egui in the same frame (a slow frame), it reports neither a click nor a drag stop, so the menu still did not open. The canvas now keeps its own right press: one that went down over it and comes up over it that frame is a right-click too, unless a gesture used it (Alt+right resize, a right-erase stroke). The menus open where the button came up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Canvas: a right-click counts only when it was pressed on the canvas itself (#10183) The canvas's own record of a right press checked only that the press was inside the canvas's rectangle. A right press on a popup over the canvas (the Brush Preset picker, a tool's canvas menu) that moved off it and came up in the same frame reopened the popup where the button came up, and every right-click on a canvas scrollbar opened the picker or the tool's menu. A right-click past egui's click distance now needs the press to land on the canvas itself, as a still right-click does: under no popup or window, and not on a scrollbar or another control drawn over the canvas. The release must come up on the canvas the same way. A right press that an Alt+right brush resize or a right-erase stroke used still opens nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Canvas: a right button that took part in a pan is no right-click (#10183) Making the Hand pan with the left and middle buttons only went too far: Space and a right drag no longer panned with any tool, and the selected Hand stopped panning with the back and forward mouse buttons. Only the selected Hand's right button has another use, its view menu. Now the Hand pans with a drag of any button, except a drag with the selected Hand's right button on its own, which still opens the view menu where the button comes up. A right button that took part in a pan no longer counts as a right-click when it comes up. Letting go of Space before the right button at the end of a Space+right pan used to erase a dab (Erase preference) or open the Brush Preset picker; it now just ends the pan. A left pan with the Hand with the right button pressed in, either way round, no longer opens the view menu. A right-click is the right button on its own: a right press made while another button is down, or one joined by another button, only counts as the still click egui itself reports. A right press moved during a left brush stroke opens no picker again, as before #10183. The canvas now follows its right press on every frame, panning or not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Canvas: Escape while the right button is down cancels the right-click (#10183) Pressing Escape during a right drag that had moved past the click distance did not cancel it: when the button came up, the Brush Preset picker or the canvas tool menu still opened. The same happened after Escape closed a Shift+Alt+right HUD Color Picker and the button was then let go somewhere else. Before #10183 nothing opened in either case, because a moved right drag was never a click. Escape pressed while the canvas follows a right press now cancels that press, so its release opens nothing. The canvas reads the Escape from the frame's own key events, so a shortcut or a popup that already took the key does not hide it. A still right-click that egui itself reports is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Canvas: Space and a right drag pan with the Hand selected too (#10183) With the Hand tool selected, holding Space and dragging with the right button opened the Hand's view menu instead of panning. Before #10183 it panned, as Space and a right drag still do with every other tool. Space is the pan itself, so with Space held a right drag now pans with any selected tool, the Hand included, and the pan uses the press: no view menu opens when the button comes up. Without Space, the selected Hand's right drag still opens its view menu without panning. Ctrl or Shift with the Hand's right button are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Canvas: a moved right-click points where the button went down (#10183) A right-click on a guide with the Move tool that moved a few pixels past the click distance, across the guide, opened the menu of the layers under the pointer instead of the guide menu. The canvas menus found their target where the button came up, and by then the pointer was off the guide. Before #10183 such a click opened nothing. A right-click that the canvas counts itself, past egui's click distance, now points where the button went down: that is what was right-clicked. Every canvas menu finds its target there (a guide, the layers under it, the pasteboard, a pending crop) and opens there too, so the Brush Preset picker, the canvas tool menus and the Hand's view menu open at the press for such a click. A still click that egui reports itself is unchanged and opens at the pointer as before. The tests for moved right-clicks now expect the picker where the button went down. The comment on right_click_at now spells out the whole rule, including that a still press held past egui's click time counts when the button comes up, as a system context menu does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Canvas: a right drag past a few pixels is no right-click (#10183) The canvas counted a right press released on it as a right-click however far the pointer had moved while the button was down. A right drag of hundreds of pixels that no gesture used opened the Brush Preset picker or a canvas menu, back at the press, far from the pointer. Before #10183 such a drag opened nothing. With the Hand selected, a fast Space+right flick, too quick for egui to see as a drag, opened the Hand's view menu instead of panning. A right press now counts as a click only while the pointer stays within three times egui's click distance of it, 18 points by default. That covers the few pixels a high-DPI mouse moves as its button goes down, or a slow frame's move, and is well short of a deliberate drag. Every pointer position between the press and the release counts, also the moves inside one frame, so a drag out and back near the press is no click either. Within that distance menus still open at the press. The tests for moved right-clicks now move within that distance, about 14 points instead of 30 to 70. The tests where a gesture, Escape, a popup or another button stops the click also move only a few pixels now, so that the distance alone does not make them pass. The comment on right_click_at states the limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Canvas: letting go of Space mid right drag keeps the Hand panning (#10183) With the Hand selected, Space and a right drag pan. Letting go of Space while the right button was still down ended that pan: the view went on gliding with the button held (flick panning), and moving the mouse no longer panned. Before #10183 the mouse kept panning until the button came up, because the selected Hand's right drag always panned. A right drag that Space made a pan now stays a pan until the right button comes up, as the held Hand already does for the left button. Letting go of Space first keeps the mouse panning and starts no glide, and the release still opens no view menu. Without Space, the selected Hand's right drag still opens its view menu without panning. Other tools are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Canvas: a scroll or zoom while the right button is down cancels the right-click (#10183) A right-click that moved a few pixels opens its menu where the button went down, and finds what was right-clicked there: a guide, the layers under it, the pasteboard. The canvas kept that point on the screen. When the view scrolled or zoomed while the button was held, a different part of the image was under that point by the release, so the menu was for whatever had moved there, not for what was pressed. The canvas now notes its view when a right press goes down: the zoom, the centre, the rotation, the flip and where the canvas is. If any of it changes before the button comes up, by the wheel, a zoom or anything else, the release is no right-click and opens nothing. A still click that egui reports itself is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Canvas: the Hand's right drag past a few pixels pans again (#10183) With the Hand selected, a right drag within a right-click's reach opens its view menu without panning. Once the cap made a longer right drag no right-click, that drag did nothing at all: it neither panned nor opened the menu. Past the reach it is a drag, so it pans as it did before #10183, and its release opens nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Canvas: each painting tool's Shift+right-click menu reads the moved right-click (#10183) Main gave the Pencil, Clone Stamp, Eraser, Dodge, Sponge, Paint Bucket, Gradient, Mixer Brush and Healing Brush their own Shift+right-click menu, and the HUD Color Picker now opens with the current colour under the pointer. Both go through the canvas's right-click record: a Shift+right-click that moved a few pixels opens the tool's menu where the button went down and nothing else, a long Shift+right drag opens nothing, Spot Healing keeps its picker, and a HUD press released after a small move, on the HUD or just off it, opens no menu. These tests hold that on the rebased branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
dde01384d2 |
perf(io): stream large PNGs row by row into the layer's tiles (#10259) (#10305)
* perf(io): build JPEG/PNG tiles straight from the decoded rows, in parallel (#10259) Surface::try_write_rows_with fills tiles row by row on all cores; image_surface converts from the borrowed decoded rows into it (no per-band copy, no staging Image). Opaque sources skip the prune scan. import_flat_with checks cancel after the decode and the tile build and reports progress. * fix(io): store an F16 target as F32 so its opaque alpha is 1.0 An image without alpha converted to an F16 target got the alpha 0.0: the opaque-alpha path copied two bytes of 1.0f32. The surface keeps 16-bit float as 32-bit float anyway, so image_surface now converts an F16 target to F32. * Format fused tile import integration * perf(io): decode PNG rows straight into the layer's tiles (#10259) * test(codecs): PngSuite rows test skips files without a PNG signature (#10259) * perf(io): stream PNG rows only for images of 32 MiB or more (#10259) * Update crates/codecs/src/codecs/png.rs Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> * fix(io): name the image, not the failed reservation, in PNG/JPEG allocation errors (#10259) A failed surface reservation is one tile or row band, so reporting its bytes as "a N MB image" was misleading (usually "0 MB"). Report the image's pixel size. * refactor(codecs): the caller picks the PNG row-group height (#10259) decode_png_rows takes the rows per group instead of a fixed 256, and io passes TILE_SIZE, so the groups can't drift from the tile rows they fill. A group of 0 counts as 1. * Complete PNG streaming integration with upstream TIFF imports * Preserve F16 conversion and route default imports through interrupt wrapper * fix(io): check free memory before PNG tiles, and cancel TIFF imports after the decode (#10259) import_png_streamed refuses an image whose tiles and one row group the machine can't hold before the first tile exists, as tiff_to_document does; the whole-image path (the base PNG path among it) checks the layer's tiles before allocating them, for images above the streaming size, so a small image still opens where the reserve alone would have refused it. import_flat_with now honours the interrupt after a successful TIFF import, as the whole-image path does; a decode error is still reported as itself. * fix(io): pass lazy memory probe to PNG importer * fix(io): handle scratch read errors during flat import * test(io): respect early TIFF import cancellation * fix(io): thread ImportOptions decode limits into PNG row streaming After merging main, the whole-image open path respects ImportOptions::flat_max_pixel_bytes via decode_limits, but the streamed PNG path still used DecodeOptions::default(). Pass opts through so both paths share the same decoder limits. * fix(io): silence clippy needless_update on DecodeOptions; rustfmt DecodeOptions only has limits + keep_orientation, so specifying both and ..Default::default() trips -D clippy::needless_update. Also apply rustfmt to the decode_png_rows call that gate's fmt check rejected. --------- Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: FredApps <FredApps@users.noreply.github.com> Co-authored-by: FredApps <fred@apps.local> |
||
|
|
3e27572e7a |
Artboards: clipping groups move between boards together; pastes off the boards and into masks land right (#11441)
* fix(artboards): move a clipping group between boards as one (#10512 follow-up) Dragging the base of a clipping mask onto another artboard left its clipped layers on the old board, released, covering that whole board (auto_nest moved the base alone and clip_group::settle released the layers that lost it). auto_nest now carries the layers clipped to a moving base along, in order, directly above it on the new board (or at the top level), so settle keeps them clipped. A clipped layer never changes board on its own, moved or not: it shows only over its base, so it goes where the base goes. When any layer of the group can't go (Prevent Auto-Nesting, the group depth limit), the whole group stays. Still one history step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(paste): keep a paste from off the boards visible (#10538 follow-up) Copying a layer that isn't on any artboard and pasting with a layer on board B selected put the paste inside B, beside that layer, outside B's area, so B hid it. The clip has no source board, so board_shift doesn't move it onto B, and layer_place::insertion_point only checked the content centre when the anchor was the board itself. insertion_point now applies the same centre rule beside a layer on a board: a layer edited right after it is made (Place::AutoEditable: Paste, Place, Type, Shape) whose content is centred off the anchor's board goes above the board at the top level, as it already does with the board itself selected. Content centred on the board (Paste at the view centre there) still lands inside it. Place::Auto (New Layer, fill and adjustment layers) is unchanged: those layers are empty or cover the canvas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(paste): shift a mask or channel paste between artboards (#10538 follow-up) Pasting into a layer mask, alpha channel or Quick Mask (paste_to_target) still placed the pixels with paste_offset alone, so with the target layer on another artboard they no longer landed where a layer paste does: at the old spot on the source board (in place) or the middle of the view. paste_to_target now applies board_shift first, as paste does, for Paste and Paste in Place alike; Paste Into / Outside keep centring on the selection (board_shift skips clampToCanvas: false). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(artboards): name the auto-nest plan entry type (clippy type_complexity) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the board shift for a mask paste into a position-locked artboard; an off-board paste above an artboard lands unclipped - board_shift skips the position-lock guard when the paste adds no layer (a mask paste), since mask edits don't check position locks. - insert_new: a layer placed directly above an artboard is never clipped to it, and goes past the clipped layers above the slot. - Tests for both; each fails without its fix. * Paste into an alpha channel or the Quick Mask keeps document coordinates; only new content above an artboard is unclipped An alpha channel or the Quick Mask covers the whole document and is on no board, so the selected layer's board no longer shifts a paste into it. The unclip-above-an-artboard rule in insert_new now applies only to Place::AutoEditable (new content placed off a board), so a clipped layer put Place::Above a board (the layers after a pasted board, a duplicate) keeps its flag as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <FredApps@users.noreply.github.com> |
||
|
|
a3ea9a4277 |
Guides: integrate edit-session previews and interaction-check scenarios (#9190)
* feat(ui): integrate edit-session guide drafts and interaction-check scenarios * fix(ui): repaint guide previews after automation field changes * fix(guides): retain drafts after command refusal * Restore the upstream Git-source check and keep guide replay local * Document pending source approval and shared replay workflow * Clarify that shared CI support for guide replays is pending * Vendor the guide edit dependency for source review Keep the edit-session revision and runtime behavior. Track its source, licenses, checksums and lifecycle tests in-tree in response to the source review in #9190. Apply the workspace lint policy to the standalone crate. * Move edit-session into ui-egui as a module (drop vendor/) The edit-session code now lives in crates/ui-egui/src/edit_session.rs, with its tests in edit_session/tests.rs, following the project's module layout. This removes vendor/edit-session, the path dependency, the workspace member, the NOTICE entry and the xtask layers entry. --------- Co-authored-by: Anton Pepperok <perchneko.anton@gmail.com> Co-authored-by: Perchenko Anton <perchenkoanton@iMac-Perchenko.local> Co-authored-by: FredApps <FredApps@users.noreply.github.com> |
||
|
|
a4a10166df |
Merge remote-tracking branch 'origin/main' into integration-batch-17
# Conflicts: # docs/gaps.md |
||
|
|
a38e544faf |
Make Select and Mask refinement and previews tile-sparse (#10732)
* Make Select and Mask refinement and previews tile-sparse * Preserve finite mask extent when copying sparse pixels * Keep solid preview coverage shared during conversion * Preserve alpha sharing and document sparse refinement limits * Refine mixed tiles in bounded parallel batches * Skip guide sampling for constant refinement halos * Balance sparse refinement batches using measured queue size * Preserve default alpha and cancel sparse refinement cooperatively * Address review follow-ups: cancellation after completed flush, stroke assertions, and docstrings * Fix merge: bind tile bytes to avoid temporary borrow in matting --------- Co-authored-by: FredApps <fredapps@users.noreply.github.com> |
||
|
|
b613b411b8 | Merge remote-tracking branch 'origin/main' into integration-batch-17 | ||
|
|
88beb84064 |
Address CodeRabbit: same-named actions in Batch/Droplet, Save and Close format
- Batch and Create Droplet dialogs get a Set dropdown (each set once, each action name once) and send the selected action's set, so a loaded library with one name in two sets runs the chosen set's action; createDroplet now honours `set` with a named action (dialogs.rs:512 finding). - Droplet capture rewrites an ordinary call to the action's name only while that name is unique in the exported list; otherwise it keeps the exported index, so a call to the second of two same-named actions still reaches it (outside-diff finding on actions_cmds.rs:337). - Save and Close refuses a source whose bytes Batch would write in another format (a Cineon source is encoded as PSD), instead of writing PSD bytes under the source's .cin name (batch_run.rs:918 finding). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
384ff664d6 |
perf: JPEG/PNG open bench with per-phase timings and peak memory (#10259) (#10271)
* perf: JPEG/PNG open bench with per-phase timings and peak memory (#10259) Adds the flat_open_bench example (import, admission, decode, orient, tiles, first composite and editable per case, each case in its own process for its peak RSS), registers it and scenarios P83 to P86 (not enforced) in perf/budgets.toml, and re-exports image_to_document (doc-hidden) so the bench can time tile construction. No change to import behaviour. * fix(compose): preserve special Fill semantics for CMYK adjustments * Clarify flat-open benchmark RSS provenance * Run flat-open benchmark provenance regression in CI * Place benchmark regression module after production items * chore: drop leftover CMYK proptest seeds from measurement PR Keeps #10271 scoped to the flat JPEG/PNG open bench. The compose seeds belonged to the CMYK Fill work that already landed via #11001. --------- Co-authored-by: FredApps <fredapps@users.noreply.github.com> |
||
|
|
274f6d3e26 |
MCP bridge: waited commands poll the app's job, with progress and cancel (#8130) (#8134)
* MCP headless: waited commands run as jobs, with progress and cancel (#8129) Headless command_run and command_batch steps that wait (the default) ran job-capable commands inline while holding the session lock. Cancelling a long Gaussian Blur did not stop it: it still added its history step, and doc_inspect and jobs_list queued behind it. No progress was reported. A waited job-capable command now starts as a background job (Headless::command_begin) and the server polls it with the lock released (Headless::job_poll), so other calls are served meanwhile. With a progress token it sends notifications/progress (strictly increasing, at most ten a second; for a batch, finished steps plus the current step's fraction). notifications/cancelled cancels the job and leaves the document unchanged; a drop guard does the same when rmcp drops the request future. Headless keeps the outcome of each waited job as sync_jobs sees it end, since jobs.list keeps only the last few. Both backends share one batch loop, which runs no further step once the request is cancelled. Tool hints: jobs_list is read-only, doc_select is non-destructive and idempotent, and the scoped exports (which refuse to overwrite) are non-destructive. doc_save's description lost a run of spaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: cancel a waited job whose request was dropped before it started The Abandon guard was created only after command_begin returned. A request dropped while the start still waited for the session lock left the job it then started running, and its `waited` entry behind. The guard is now armed inside the locked closure that starts the job, so dropping the unread output cancels it. Review feedback on #8132. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: job_poll applies finished jobs before checking the job A caller polling Headless::job_poll directly, without sync_jobs in between, kept seeing Running after the worker finished, because the session only drops a finished job from its running list when its jobs are polled. job_poll now syncs first. The MCP path already synced in headless_op. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: a cancel that loses the race returns the job's result job_abandon ignored whether Session::cancel_job cancelled anything and always dropped the stored outcome, so headless_wait answered `Cancelled` for a job another request had already applied to the document. It now returns how the job really ended: Cancelled only when it cancelled the job, the job's result when it had already ended, and an error when the job cannot be cancelled (printing) and is still running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: waited commands run as jobs, with progress and cancel (#8129) Headless command_run and command_batch steps that wait (the default) ran job-capable commands inline while holding the session lock. Cancelling a long Gaussian Blur did not stop it: it still added its history step, and doc_inspect and jobs_list queued behind it. No progress was reported. A waited job-capable command now starts as a background job (Headless::command_begin) and the server polls it with the lock released (Headless::job_poll), so other calls are served meanwhile. With a progress token it sends notifications/progress (strictly increasing, at most ten a second; for a batch, finished steps plus the current step's fraction). notifications/cancelled cancels the job and leaves the document unchanged; a drop guard does the same when rmcp drops the request future. Headless keeps the outcome of each waited job as sync_jobs sees it end, since jobs.list keeps only the last few. Both backends share one batch loop, which runs no further step once the request is cancelled. Tool hints: jobs_list is read-only, doc_select is non-destructive and idempotent, and the scoped exports (which refuse to overwrite) are non-destructive. doc_save's description lost a run of spaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: cancel a waited job whose request was dropped before it started The Abandon guard was created only after command_begin returned. A request dropped while the start still waited for the session lock left the job it then started running, and its `waited` entry behind. The guard is now armed inside the locked closure that starts the job, so dropping the unread output cancels it. Review feedback on #8132. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP bridge: waited commands poll the app's job, with progress and cancel (#8130) A waited command_run or command_batch step of an engine command now starts it with engine.execute {wait: false} (sent once) and, when the app runs it as a background job, polls jobs.list until it ends. A job can run past the bridge's 55s reply timeout, its progress is forwarded, and cancelling or dropping the request sends jobs.cancel. UI command ids keep wait: true. A failed poll or a job no longer listed is reported with its id rather than guessed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP bridge: keep the cancel guard armed until jobs.cancel returns; clarify the docs The cancel branch disarmed the Abandon guard before awaiting jobs.cancel, so a request future dropped during that await sent no cancel. The control-protocol table now says polling applies only to a waited command_run. Review feedback on #8134. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: job_poll applies finished jobs before checking the job A caller polling Headless::job_poll directly, without sync_jobs in between, kept seeing Running after the worker finished, because the session only drops a finished job from its running list when its jobs are polled. job_poll now syncs first. The MCP path already synced in headless_op. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: a cancel that loses the race returns the job's result job_abandon ignored whether Session::cancel_job cancelled anything and always dropped the stored outcome, so headless_wait answered `Cancelled` for a job another request had already applied to the document. It now returns how the job really ended: Cancelled only when it cancelled the job, the job's result when it had already ended, and an error when the job cannot be cancelled (printing) and is still running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP bridge: a refused jobs.cancel is not reported as a cancellation bridge_wait discarded the app's reply to jobs.cancel and always answered `Cancelled`. The app refuses to cancel a job that is no longer running, so a job that finished between the jobs.list poll and the cancel was reported as cancelled although its edit may be applied. A refused cancel now answers that the job was not cancelled and points to jobs_list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP bridge: a cancel during a failed poll still cancels the job When notifications/cancelled arrived while a jobs.list poll was in flight, and the poll and its retry then failed, bridge_wait disarmed the Abandon guard and returned the poll error without checking the waiter, so no jobs.cancel reached the app and the edit ran on after the caller cancelled it. That error path now sends jobs.cancel first when the request was cancelled. A failed poll without a cancel still leaves the job running and names it. Reported in review of #8134. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP bridge: a cancel doesn't wait for a slow jobs.list poll bridge_wait checked for a cancel only between polls, so a cancel that arrived while the app was slow to answer jobs.list waited for the poll, up to the 55s call timeout, before jobs.cancel was sent. The poll now races the cancel. That needs BridgeClient to drop the connection of a call abandoned before its reply: it kept the one cached connection, so jobs.cancel queued behind the unanswered poll on the same socket (the app serves a connection's requests in order), and a half-sent request could share it. A call now takes the connection out of the cache and returns it only once answered, as a timeout or lost reply already dropped it. The poll-error check from the previous commit stays for a cancel that races a failing poll; its test is replaced by one for the slow poll, which covers the reviewed case. A BridgeClient test checks that a dropped call's connection is not reused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(automation): retain cleanup after failed bridge cancellation * chore: restart CI for PR #8134 * test(automation): classify the smart object replacement MCP tool * test(automation): synchronize dropped bridge waits and accept reconnections * Fix CI after merging upstream * Fix CI after merging upstream * Deduplicate Arabic closure messages after upstream merge * fix(engine): bound active and cancelling background workers * Fix cancellation and draining in bridge merge * Test release-aware draining cleanup * Fix draining regression warning --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Khawar Zamman Wani <wanikhawar@Khawars-MacBook-Air.local> Co-authored-by: FredApps <fredapps@users.noreply.github.com> |