diff --git a/AGENTS.md b/AGENTS.md index 14f435e..27dfb10 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -175,12 +175,12 @@ Platform adaptations, each forced by a missing Qt analog: - Blending (13.9a: `6dc2219`, `8ab32c9`, `5a8f6ce`, `e6c964b`). Held, proven: clipping stacks and live-mask coverage at device resolution (the twin's `LiveMaskRenderer` has no `bounds`: its group, alpha and coverage surfaces are the painter's device since 4.x, so Swift's `resolution` and `pixelSize` have no twin); an adjustment in any mode, over the document and inside a stack, blended at full coverage, its alpha restored, then mixed through mask and opacity (`adjust`'s `blended` goes through `LayerRenderer::composite`, which hand-blends every mode QPainter lacks through `HslBlend`, so Swift's `SeparableBlend.blend` has no twin); a clipping stack composited in its base's mode in every mode (`drawComposite` hands `m_stackModes` to `LayerRenderer::composite`, the same path), on the canvas, in export and in Copy Merged alike. Ported: Soft Light by Photoshop's formula, as Core Image computes it (2bs + b²(1 − 2s) up to half, 2b(1 − s) + √b(2s − 1) above), in `SeparableBlend::channel`, and `needsSurface` names Soft Light, so `HslBlend` blends it by hand everywhere a layer, a stack or an adjustment is drawn; `LayerRenderer::compositionMode` loses its Soft Light case (dead now). Compared first: Qt's `CompositionMode_SoftLight` is the PDF's (W3C) formula, which equals Photoshop's below a source of half and above it wherever the backdrop passes 0.25; under that it takes the PDF's cubic for √b and runs up to 17 levels darker (backdrop 5/255, source 1: 19 where Photoshop gives 36). Swift's 0.9 over 0.5 gives 170 under either formula. Known difference: Swift's Core Image is "within 5 levels of Photoshop's"; the twin is Photoshop's own formula, so it may differ from Swift's by those few levels. Tests: `BlendingTests` (Swift's `clippingStacksBlendInTheirBasesMode` for Linear Dodge, Color Dodge and Multiply: Swift's numbers, Copy Merged equal to export, the canvas within a level; adjustments in Difference and Linear Light over the document and Vivid Light and Subtract inside a stack, by the numbers, at full and half opacity, in all three places; Swift's `softLightMatchesPhotoshop` plus a dark backdrop and the lower branch; Swift's `softEdgedLayersBlendTheSame` for Soft Light, Overlay, Multiply and Color Dodge: a radial soft layer over noise, Copy Merged equal to export, the canvas within a level), `BlendModeTests::everyBlendModeMatchesThePdfFormulas` (its Soft Light reference is Photoshop's now; its backdrop of 0.1 under sources past half catches the PDF's cubic (at 0.25 the cubic equals √0.25)), `ClippingStackTests::stacksAndCoverageTakeTheDevicesPixels` (a half-unit stripe stays sharp at a device scale of two, in a stack and through coverage from a source further down). Equivalent mutant: Soft Light's `source <= 0.5` for `< 0.5` (both branches give the backdrop at a half). Judgments: stacks and coverage at device resolution, adjustments in any mode and a stack in its base's mode were held (proven by the tests above); Soft Light ported. Mutants: 5 on Soft Light, 4 caught, one equivalent (above). - The Metal canvas (13.9b: `cdf8674`, `4b04323`, `5708ed3`, `93de9e7`, `54dd707`, `91cb0b1`, `f773f7b`, `4c5e1a6`, `d04f159`, `b1b7490`). OmaPhoto stays a CPU canvas (the port's decision): Swift's GPU canvas, its `GPUCanvas`, `GPUNoise`, `MetalWarp` and `CanvasLinesOverlay` and the GPU branches of `EditorCanvas`, have no twin; the twin's canvas is the Core Graphics canvas those commits compare against. Ported: Grain and Add Noise anchored to the document (`54dd707`): `LiveMaskRenderer::adjustmentRegion` (Swift's `adjustmentRegion`) maps what the painter shows into the document, and the canvas's `drawLayers` sets it from its centre mapping and zoom, so the smooth path counts noise in frame pixels from the document's corner and Grain in document pixels, as export does, and the crisp path's surface maps to document pixels, so a partial repaint keeps one field; the blur's scale still comes from the unmapped region. This retires 9.5b's and 12.8c's known differences (Grain and noise following the view, a partial repaint's own field); `CanvasDisplayTests::grainFollowsTheViewOnTheCanvas` is gone. Export and `drawLiveComposite` already draw in document pixels and set no region, as Swift's. Tests: `AdjustmentCanvasTests` (Swift's `matchesEveryAdjustment`: every kind through a ramp mask at 80% over a photo and a translucent layer, the canvas within a level of the export over its checkerboard; Swift's `patternsStayWithTheDocument` for Add Noise and Grain: equal to export at 100% and 200%, panned and not, and at 80% a whole-pixel pan moving the pattern with the image and a partial repaint showing the whole's pattern within two levels of resampling), `CanvasEditContractTests` (Swift's `aNewMaskRevealsTheWholeLayer`, upright and turned: a one-pixel mask on a scaled layer changes no covered pixel, the canvas as the export; `matchesWhileMovingPixels`, moving and duplicating on a masked layer at 60% under a soft selection: the preview equals the commit and the export outside the ants, the hole only for a move; `matchesWhileDraggingAGradient` in a selection at 70%: the pending gradient equals its commit outside the overlays, nothing outside the selection; `5708ed3`'s failed fill cancelling with `brushError`, no step, the document unchanged; `matchesALiveMaskOutsideAClippingStack`: a turned source with a layer between, the canvas as the export), `CanvasContractFixtures.h` (the scene, the export over the canvas's checkerboard, a gap that skips the overlays' bands, the ramp mask). Judgments, each Swift contract held unless named above: `cdf8674`: rotation, flips, scale, sharp successive reductions, premultiplied blending in every mode with layer and ancestor opacity, the checkerboard anchored to the document, the crisp composite at high zoom, sampling modes and rounded-up reduction extents, premultiplied blending in sRGB, the surround, shadow and border, placed own masks and multiplied folder masks on pixel layers, filter, Levels and Hue/Saturation previews in the layer's place: held; the CPU paths are the reference and are pinned by `LayerRendererTests` (placement, sampling rules, halvings, overhang), `DownsampleTests`, `BlendModeTests`, `CanvasDisplayTests` (checkerboard, shadow and border, crisp from 200%, the rich scene against the export), `FolderOpacityTests`, `FolderMaskClipTests`, `MaskPlacementTests`, `LevelsCanvasTests`, `HueSaturationCanvasTests`, `VignetteCanvasTests`. `4b04323`: a moved or duplicated selection's preview and commit: held and now proven on a masked, translucent layer (above); the rounded offset is pinned by a move of 5.6 landing at 6 (`EffectsCanvasTests::movingPixelsKeepTheEffects`). Swift's laziness (`PixelMove.applyOffset`, `holed`, `liftedTarget`) only spares the GPU canvas a rebuild per move; the twin rebuilds eagerly in `movePixels`, which realises the tiles before display and commit by construction, and a failure there cancels the move with `brushError` (`PixelMoveTests::aPixelMoveHoldsTheSessionAndAFailedMoveCancels`). `5708ed3`, `91cb0b1`: live paint, erase and mask paint, paint past the old bounds under the old mask (`TiledStrokeTests`, `BrushPreviewTests`, `MaskPaintAnywhereTests`), Smudge and Liquify previews through the mask, opacity and placement (`SmearCanvasTests`, `FolderOpacityTests`): held. `GradientEdit.Fill` and `applyFill` are the GPU's laziness; the twin fills in `refreshGradient`, before display and commit by construction, a failure cancelling with `brushError` (now tested), and the commit publishes what was filled. `MetalWarpTests.matchesTheCPU`: the twin's `WarpStroke` is the CPU reference (`SmudgeLiquifyTests`); nothing to port. `93de9e7`: a one-pixel white mask revealing its layer: held (QPainter's smooth draw clamps an image's edge, as Core Graphics draws it; now tested); the live paint grid's extent and edge sampling: held (`TiledStrokeTests`, `DownsampleTests`, `TiledLayerTests`). `clampedToExtent` is GPU-only. `54dd707`: every adjustment kind with alpha, masks and opacity, blurs at the drawn scale and direction, adjustment results equal to the CPU reference, deterministic seeds: held (the canvas runs the CPU filters themselves; now tested for every kind); Grain and Add Noise anchoring: ported (above). `f773f7b`: non-adjacent live-mask sources (now tested), distortion previews (`DistortCanvasTests`, `DistortMaskTests`, `EffectsCanvasTests`), text previews keeping and handing on effects (`TypedTextCanvasTests`), drafts above the active layer (`ShapeCanvasTests`, `TypedTextCanvasTests`; on a stack's base the draft is drawn by `drawOwnWithDraft` inside the stack's group, as Swift's: it joins the base, alpha included, under the clipped layers: `CanvasEditContractTests::aDraftOnAStacksBaseSitsInsideTheStack`), new text when the active layer cannot draw, effects and placed masks kept while brushing, painting masks, dragging gradients and moving pixels (`EffectsCanvasTests`, its `movingPixelsKeepTheEffects` and `aGradientKeepsTheEffects` for Swift's `movingWithEffects` and `gradientWithEffects`, `MaskEffectsCanvasTests`, `BrushCanvasTests`, `MaskPaintAnywhereTests`): held. `distortShape`, the now-internal `imageCorners` and `editedText`/`handOffTextEffects` are GPU plumbing or a refactor of what the twin already draws. Known difference: Swift moves the pixel grid and a new text frame into an overlay view below its transform overlay, so the grid draws over the document's border hairline and the text frame under the other overlays; the twin draws the grid inside the document under the hairline and the text frame last. Both sit above the pixels and take no input; the order shows only on the hairline's edge pixels (a 13% white line), and no other overlay shows while a text frame is dragged. A port was tried and left: QPainter's antialiased hairlines make the order unprovable to a level. `4c5e1a6`, `d04f159`: texture retention, eviction and blits are GPU mechanisms; the CPU's sharp reductions are `DownsampleCache`'s, pinned by `DownsampleTests`. `b1b7490` only words a test's diagnostics. Tests tried and left: a distortion's preview against its commit differs on noise (the commit resamples the warp at its trimmed place), which Swift's tests never claim (they compare two previews); `DistortCanvasTests` pins the preview. Mutants: the region's mapping and use in `LiveMaskRenderer::adjust` and each term of the canvas's `adjustmentRegion`: 6, all caught (the width and height terms only once the partial repaint was compared). - Shift on a pixel drag and Select All from the list (13.10): `CanvasView::moveSelectionTool` (`EditorCanvas+Selection.cpp`) twins Swift's `60bde4f`: with Shift held a Ctrl-drag of selected pixels zeroes the shorter axis of the offset, lengths compared (`std::abs`), a tie keeping across, as `dragSelection` already does for the outline; read from each move's own modifiers, as Swift's `event.modifierFlags`. `451281e`: Swift's table now hands Select All to the canvas (`session.selectAll()`, guarded on a document) instead of selecting every row, so the twin's `NativeLayerList::selectAllRows` (12.5b's round-3 fix) and the menu's `focusedList` branch are gone: a focused list falls through to `session().selectAll()`, which guards on the document itself. A rename's editor is a `QLineEdit`, so it keeps its own Select All first, as Swift's field editor. Tests: `SelectionCanvasTests::shiftKeepsMovedPixelsOnALine` (across, down, a tie, a long left beating a short down, without Shift, the release keeping the last offset), `SelectMenuTests::selectAllInAFieldIsTheFields` (a real Ctrl+A on the focused list selects the canvas, history "Select All", layers and the active one unchanged, the list keeping focus; Swift's `selectAllInTheLayersPanelSelectsTheCanvas`). Mutants: 5 on the Shift branch, all caught (the `abs`-less comparison caught after the left-beats-down step was added). No survivors. -- Why a brush cannot paint (13.11, `133c34a`): `EditorSession::paintRefusal` (`EditorSession+Brush.cpp`, declared in `EditorSession+Brush.h`) twins Swift's `paintRefusal`: none while `canEditLayers` refuses, without an active layer or when `canPaint` holds; then several layers, a folder's pixels, a hidden layer or one in a hidden folder, a mask turned off, an adjustment's pixels, an empty selection, in Swift's order and words, with Ctrl+D for ⌘D and the curly quotes kept. `beginBrush` splits Swift's guard: a tool that cannot paint the target (Spot Healing or Clone Stamp on a mask, a tool that is no brush) returns silently, a refused target sets `brushError` to the refusal (none when busy, so a waiting error is cleared, as Swift's `brushError = paintRefusal`). `beginWarp` sets the mask's words or the refusal. Fix beyond Swift: Swift's gradient sets the refusal only past its first guard, which is reached with a refused target only while a gradient is pending, where `canEditLayers` refuses and the refusal is always nil; so Swift's gradient says nothing on an empty selection, which its commit message says it explains. The twin's `beginGradient` sets the refusal at the first guard (no pending gradient, `canPaint` refusing); Swift's second assignment, always nil there, is left silent. Like the brush, the gradient's refusal while the editor is busy is none, which clears a stale alert where Swift's first guard returns untouched. `invertSelection` leaves no selection when the inverse is empty (Select All then Inverse), one `Inverse` step, as Swift's. Tests: `PaintRefusalTests` (every refusal, and the order at each seam that two reasons can share: two layers before a folder, a folder before its hiding, hiding before a mask turned off, an adjustment before an empty selection, a folder's or adjustment's mask past the folder or adjustment; the mask-off and adjustment tests exclude each other, so their order is unreachable; a layer inside a hidden folder; the gradient and the Smear; a busy session clearing, another tool silent), `SelectionTests::inverseOfEverythingDeselects` (Swift's). Equivalent mutants: `paintRefusal`'s `canPaint()` term (when `canPaint` holds every later test is false) and the empty-selection test without `isEmpty` (reached only when `canPaint` refuses for no listed reason, which needs an active layer outside `selectedLayerIDs`, which no session call makes). Painting at the layer's own resolution (13.11, `d3170ab`): `BrushStroke::Clone` is Swift's tuple `{image, placed, inGrid}`: the image painted through the tip and where it sits, in document pixels or in the stroke's own grid. `clonePixels` maps tile pixels through the grid (and `pixelToDocument` unless `inGrid`) into `placed`, scaled to the image, so a translation alone still copies or shifts by the fraction as before. `BrushStroke::gridRect` (in `BrushStroke+Commit.cpp`, beside `transform`, as Swift's) carries a document offset into the grid through the inverse map's linear part (`CGSize.applying`), turned, scaled and flipped as the layer. `cloneSample` takes the stroke and the offset: the stroke layer's own pixels placed at `gridRect(sourceRect, offset)`, or, sampling all layers, the composite at document size placed `offset` away; `blurSample` takes the stroke: the layer's pixels or mask drawn nearest at the margin of a region `ceil(3σ)` round `sourceRect`, σ the bar's Radius (13.16; Swift's tenth of the brush, 1.5 to 30, at 13.11) divided by the grid's document size per pixel (`sqrt|det|`) and capped at half the longer side, a mask on its edge tone first, softened through `PixelAdjust::gaussianBlur` (a mask's edge repeated, pixels clear past theirs) at σ × fit, in the pieces the brush reaches since 13.16, where fit shrinks the sample to a budget of `min(maxSurfacePixels, max(16 M, four canvases))`; it returns the region as `placed`, `inGrid`. Swift's old mask sample (the placement's black box, `drawCoverage`) is gone with it. `beginBrush` makes the stroke first, then samples into it, as Swift's; a sample that fails returns without a stroke. The Smear's commit places its copy at document size. The brush and eraser: Swift's commit only changes its CPU fallback's stamp (`stampDiameter`), dead in the twin (8.1); the held judgment holds: the twin's coverage is rendered in the stroke's grid, so `NativeResolutionPaintTests` passes Swift's erase cases (scaled down, unequal, turned, and on the canvas scaled back up) and the blank layer's document resolution without any change to `BrushStroke::grid` or `BrushCoverage`. Tests: `NativeResolutionPaintTests` (Swift's seven, plus the offset carried through a flipped and a turned layer, pixel for pixel), `BlurToolTests` (`theSampleFollowsTheRadius`, `theSampleIsInTheLayersOwnPixels`: twice as fine, and a tiny layer's cap; `aHugeLayersSampleIsMadeCoarser`: 16 M and four canvases; `aMaskSampleKeepsItsEdgeTone`), the tests' document clones through `documentClone` (`BrushFixtures.h`). Cancel and Apply, and a scaled layer's row (13.11, `67cc31e`): `TransformInspector` puts Cancel and Apply in a row of their own (`pendingTransform`, spaced 12) that shows only while the edit waits (`transformEdit()->persistent`: Ctrl+T, Transform Selection, a distortion begun so; a value typed into such an edit joins it, while a value typed with no edit open applies when its field finishes, 13.13); a handle drag's edit leaves it unseen. Swift's `.opacity(0)` keeps the buttons in the layout and their keys alive, so the row keeps its place under a `QGraphicsOpacityEffect` faded over 0.12 s with CSS's ease-out (`QEasingCurve` Bézier 0, 0, 0.58, 1, SwiftUI's `.easeOut`), passes clicks through (`WA_TransparentForMouseEvents`, Swift's `allowsHitTesting(false)`) and takes no focus while unseen; Return and Escape still reach the edit through the bar, and a remapped key through `NativeShortcut` (the buttons stay `isVisible`). Known difference: Qt has no per-widget accessibility hiding short of hiding the widget, so a screen reader still finds the unseen buttons (Swift's `accessibilityHidden`). `sizeLabel(const ImageLayer &)` (declared in `NativeLayerList.h`, defined in `NativeLayerList+Cell.cpp`, Swift's extension in its UI file) gives the row "W × H px", and once the width differs from the asset's by a twentieth of a percent or more, " · " and the percent to one place at most, ties to even (a box 1062.5 wide over 1000 pixels is 106.2%), in the user's locale as Swift's `.formatted`; a blank layer has no pixels to measure. Tests: `TransformInspectorTests::cancelAndApplyShowOnlyWhileAnEditWaits` (a drag's edit unseen, its click falling through and Return applying; a waiting edit fading in, clicked; Cancel), `theFieldsTakeTheSpareRoom` (the row's place), `NativeResolutionPaintTests::layersPanelSaysHowFarALayerIsScaled` (Swift's, the threshold either side, a rounded tenth, ties either way, a blank layer, the scaled row in the list; `NativeLayerListTests` holds unscaled rows). Mutants (13.11): paint refusal 8 (2 survivors pinned by a folder's mask and an adjustment's mask, 2 equivalent as above), gradient 2, Smear 1, Inverse 1; `blurSample` 19 + 5 reruns (the unbuildable `1.0` replaced by `abs` for `sqrt`; the determinant's sign pinned by a layer turned 45°, the coarse sample's placement, size and σ × fit by an exact comparison of a 4200 × 3900 layer's sample), one survivor left untested: the `maxSurfacePixels` cap on the budget, reached only past a 50-million-pixel canvas with an 800 MB sample; `cloneSample` 4, `clonePixels` 3 (the scale pinned by `aGridCloneIsScaledOntoItsPlace`), `gridRect` 2, `beginBrush` 2 (a blank layer's Blur begins no stroke); `TransformInspector` 14, equivalent: the `pending == m_pending` guard (only restarts a fade toward the same end) and `setStartValue` (an animation without one starts from the property's value); `sizeLabel` 6 + 2 reruns, all caught. +- Why a brush cannot paint (13.11, `133c34a`): `EditorSession::paintRefusal` (`EditorSession+Brush.cpp`, declared in `EditorSession+Brush.h`) twins Swift's `paintRefusal`: none while `canEditLayers` refuses, without an active layer or when `canPaint` holds; then several layers, a folder's pixels, a hidden layer or one in a hidden folder, a mask turned off, an adjustment's pixels, an empty selection, in Swift's order and words, with Ctrl+D for ⌘D and the curly quotes kept. `beginBrush` splits Swift's guard: a tool that cannot paint the target (Spot Healing or Clone Stamp on a mask, a tool that is no brush) returns silently, a refused target sets `brushError` to the refusal (none when busy, so a waiting error is cleared, as Swift's `brushError = paintRefusal`). `beginWarp` sets the mask's words or the refusal. Fix beyond Swift: Swift's gradient sets the refusal only past its first guard, which is reached with a refused target only while a gradient is pending, where `canEditLayers` refuses and the refusal is always nil; so Swift's gradient says nothing on an empty selection, which its commit message says it explains. The twin's `beginGradient` sets the refusal at the first guard (no pending gradient, `canPaint` refusing); Swift's second assignment, always nil there, is left silent. Like the brush, the gradient's refusal while the editor is busy is none, which clears a stale alert where Swift's first guard returns untouched. `invertSelection` leaves no selection when the inverse is empty (Select All then Inverse), one `Inverse` step, as Swift's. Tests: `PaintRefusalTests` (every refusal, and the order at each seam that two reasons can share: two layers before a folder, a folder before its hiding, hiding before a mask turned off, an adjustment before an empty selection, a folder's or adjustment's mask past the folder or adjustment; the mask-off and adjustment tests exclude each other, so their order is unreachable; a layer inside a hidden folder; the gradient and the Smear; a busy session clearing, another tool silent), `SelectionTests::inverseOfEverythingDeselects` (Swift's). Equivalent mutants: `paintRefusal`'s `canPaint()` term (when `canPaint` holds every later test is false) and the empty-selection test without `isEmpty` (reached only when `canPaint` refuses for no listed reason, which needs an active layer outside `selectedLayerIDs`, which no session call makes). Painting at the layer's own resolution (13.11, `d3170ab`): `BrushStroke::Clone` is Swift's tuple `{image, placed, inGrid}`: the image painted through the tip and where it sits, in document pixels or in the stroke's own grid. `clonePixels` maps tile pixels through the grid (and `pixelToDocument` unless `inGrid`) into `placed`, scaled to the image, so a translation alone still copies or shifts by the fraction as before. `BrushStroke::gridRect` (in `BrushStroke+Commit.cpp`, beside `transform`, as Swift's) carries a document offset into the grid through the inverse map's linear part (`CGSize.applying`), turned, scaled and flipped as the layer. `cloneSample` takes the stroke and the offset: the stroke layer's own pixels placed at `gridRect(sourceRect, offset)`, or, sampling all layers, the composite at document size placed `offset` away; `blurSample` takes the stroke: the layer's pixels or mask drawn nearest at the margin of a region `ceil(3σ)` round `sourceRect`, σ the bar's Radius (13.16; Swift's tenth of the brush, 1.5 to 30, at 13.11) divided by the grid's document size per pixel (`sqrt|det|`) and capped at half the longer side, a mask on its edge tone first (Swift's `if let` on the mask and its `?? 0` for a missing document are dead, since the mask's asset is the sample and a stroke needs a document, and left out), softened through `PixelAdjust::gaussianBlur` (a mask's edge repeated, pixels clear past theirs) at σ × fit, in the pieces the brush reaches since 13.16, where fit shrinks the sample to a budget of `min(maxSurfacePixels, max(16 M, four canvases))`; it returns the region as `placed`, `inGrid`. Swift's old mask sample (the placement's black box, `drawCoverage`) is gone with it. `beginBrush` makes the stroke first, then samples into it, as Swift's; a sample that fails returns without a stroke. The Smear's commit places its copy at document size. The brush and eraser: Swift's commit only changes its CPU fallback's stamp (`stampDiameter`), dead in the twin (8.1); the held judgment holds: the twin's coverage is rendered in the stroke's grid, so `NativeResolutionPaintTests` passes Swift's erase cases (scaled down, unequal, turned, and on the canvas scaled back up) and the blank layer's document resolution without any change to `BrushStroke::grid` or `BrushCoverage`. Tests: `NativeResolutionPaintTests` (Swift's seven, plus the offset carried through a flipped and a turned layer, pixel for pixel), `BlurToolTests` (`theSampleFollowsTheRadius`, `theSampleIsInTheLayersOwnPixels`: twice as fine, and a tiny layer's cap; `aHugeLayersSampleIsMadeCoarser`: 16 M and four canvases; `aMaskSampleKeepsItsEdgeTone`), the tests' document clones through `documentClone` (`BrushFixtures.h`). Cancel and Apply, and a scaled layer's row (13.11, `67cc31e`): `TransformInspector` puts Cancel and Apply in a row of their own (`pendingTransform`, spaced 12) that shows only while the edit waits (`transformEdit()->persistent`: Ctrl+T, Transform Selection, a distortion begun so; a value typed into such an edit joins it, while a value typed with no edit open applies when its field finishes, 13.13); a handle drag's edit leaves it unseen. Swift's `.opacity(0)` keeps the buttons in the layout and their keys alive, so the row keeps its place under a `QGraphicsOpacityEffect` faded over 0.12 s with CSS's ease-out (`QEasingCurve` Bézier 0, 0, 0.58, 1, SwiftUI's `.easeOut`), passes clicks through (`WA_TransparentForMouseEvents`, Swift's `allowsHitTesting(false)`) and takes no focus while unseen; Return and Escape still reach the edit through the bar, and a remapped key through `NativeShortcut` (the buttons stay `isVisible`). Known difference: Qt has no per-widget accessibility hiding short of hiding the widget, so a screen reader still finds the unseen buttons (Swift's `accessibilityHidden`). `sizeLabel(const ImageLayer &)` (declared in `NativeLayerList.h`, defined in `NativeLayerList+Cell.cpp`, Swift's extension in its UI file) gives the row "W × H px", and once the width differs from the asset's by a twentieth of a percent or more, " · " and the percent to one place at most, ties to even (a box 1062.5 wide over 1000 pixels is 106.2%), in the user's locale as Swift's `.formatted`; a blank layer has no pixels to measure. Tests: `TransformInspectorTests::cancelAndApplyShowOnlyWhileAnEditWaits` (a drag's edit unseen, its click falling through and Return applying; a waiting edit fading in, clicked; Cancel), `theFieldsTakeTheSpareRoom` (the row's place), `NativeResolutionPaintTests::layersPanelSaysHowFarALayerIsScaled` (Swift's, the threshold either side, a rounded tenth, ties either way, a blank layer, the scaled row in the list; `NativeLayerListTests` holds unscaled rows). Mutants (13.11): paint refusal 8 (2 survivors pinned by a folder's mask and an adjustment's mask, 2 equivalent as above), gradient 2, Smear 1, Inverse 1; `blurSample` 19 + 5 reruns (the unbuildable `1.0` replaced by `abs` for `sqrt`; the determinant's sign pinned by a layer turned 45°, the coarse sample's placement, size and σ × fit by an exact comparison of a 4200 × 3900 layer's sample), one survivor left untested: the `maxSurfacePixels` cap on the budget, reached only past a 50-million-pixel canvas with an 800 MB sample; `cloneSample` 4, `clonePixels` 3 (the scale pinned by `aGridCloneIsScaledOntoItsPlace`), `gridRect` 2, `beginBrush` 2 (a blank layer's Blur begins no stroke); `TransformInspector` 14, equivalent: the `pending == m_pending` guard (only restarts a fade toward the same end) and `setStartValue` (an animation without one starts from the property's value); `sizeLabel` 6 + 2 reruns, all caught. - Masks as Photoshop's (13.12, `b3419ab`): `addMask` with a selection now reveals it (white inside, black outside, the coverage as drawn) and, `revealing` false, hides it (the coverage inverted), one `Reveal Selection` or `Hide Selection` step; the row menu's Reveal All and Hide All call it as before, so Hide All with a selection hides the selection. `LayerMaskMenu` reads Alt from `QGuiApplication::keyboardModifiers()` when clicked, Swift's `NSApp.currentEvent`, so Alt-click adds the opposite, with Swift's help in Alt's words. The session keeps `viewsMaskAlone` (a getter and a setter that notifies on a change, the badge's write; `EditorSession.h`, defined in `EditorSession.cpp` beside Swift's `didSet`) and `maskAloneLayer()` (Swift's computed property: the view asked, the mask targeted, the active layer with a mask); `isMaskSelected` gains Swift's `didSet` as a private `setIsMaskSelected` (declared in `LayerMask+Session.h`, since `EditorSession.h` sits at 499 lines; defined in `EditorSession.cpp`), which every former write of `m_isMaskSelected` now calls, so targeting the pixels, another layer, a deleted mask or a load ends the view. `maskAloneLayer` and `toggleMaskAlone` live in `LayerMask+Alone.cpp`, since `LayerMask.cpp` already held 495 lines. `toggleMaskAlone` targets the mask through `selectLayerTarget` and shows it unless it already showed; where that refuses and leaves another target, so does the toggle, but with the project busy and the same mask already targeted the refusal is silent and the view still flips, as Swift's does. The canvas's `DisplayState` gains `maskAlone` (the mask asset's `ImageIdentity`, whether or not enabled, since the composite may skip it), and `drawLayers` hands off to `drawMaskAlone` (`EditorCanvas+Masks.cpp`, beside `liveFolderMaskClip`, which moved there to keep `EditorCanvas.cpp` under 500 lines): the canvas filled with the mask's edge tone, then a brush stroke or gradient laid into that mask through `LayerRenderer::drawBrushPreview` (the painted mask's own raster when it has one), else the mask at its displayed placement or the layer's transform, gray where it shows; the crisp zoom path draws through `drawLayers` too. The list's mask thumbnail: Alt's release without a drag calls `toggleMaskAlone`, only inside the thumbnail after a move under `startDragDistance` (4.7's rule; Swift's `dragMaskCopy` toggles on any left-up before a 3-point drag) (Shift no longer toggles there, as Swift's), the tooltip names Alt-click, and the targeted ring is white while that mask shows alone (`setTargeted`'s `alone`; Swift's `maskAloneLayer?.id == layerID` is left out as dead: the ring shows only on the active row, whose layer is the one shown alone). `LayerListFocusTests` and `LayerDragGestureTests` expected Alt-Shift to toggle the mask; they now expect Swift's view. `MaskAloneBadge` (in `UI/LayerMaskMenu`, Swift's file) is 26 points tall, 75% black with a 14% white rim, radius 13: the mask glyph at 11, "Layer Mask" 12 px semibold, the layer's name 12 px at 60% white, elided within 220 points, and an `xmark` button (a new `LayerIcon`, Swift's bold `xmark` as two round-capped strokes) with Swift's tooltip in Alt's words and accessible name, which ends the view. `ContentView` keeps one badge, shown while `maskAloneLayer` holds, in a layout nested in the canvas slot (it starts hidden through `synchronize`, which the constructor runs) (bottom, centred, 14 points up), so the room beside and below it stays the canvas's; Qt child widgets take their own clicks, so Swift's AppKit hosting has no twin. Swift's GPU fix (a mask stroke's grid starting at the mask's background) is the twin's behaviour already: `allocateTile` fills a mask tile with `maskBackground`. Tests: `MaskAloneTests` (Swift's three, plus another layer ending the view and the setter's signal, a stroke and a gradient shown as committed, a disabled and a placed mask, the crisp zoom, the badge's words, place, elision and clicks, the list's Alt-click, Shift ignored and the white ring; the view across undo and redo and when the shown mask is targeted again; a pending pixel gradient on the layer and another layer's pending mask gradient kept out of the view while their commits are posted), `SelectionEditTests` (Swift's renamed two and the scaled layer's hiding mask), `LayersPanelTests::theMaskButtonUsesTheSelection` (plain and Alt clicks with and without a selection, the tooltips, the row's Hide All). Mutants: the session 10 + 1 rerun (the active-layer term of `toggleMaskAlone` pinned by a busy refusal with another layer's mask targeted), equivalent: `maskAloneLayer`'s `m_isMaskSelected` and `layer->mask` terms (every path that clears either clears the view through `setIsMaskSelected`; Swift's guards, kept); `drawMaskAlone` 6 + 3 reruns (the placement pinned off the old box), the stroke's `isMask` and layer terms are live, not equivalent: `resolveGradient` only posts the commit, so a pending pixel gradient, or another layer's mask gradient, keeps its raster while the view shows (now tested); the canvas branch and state 2; the list 4 + 2 (a release off the thumbnail pinned); the button and badge 5 + 3 (margins and spacing pinned); `ContentView` 5 (the constructor's `hide` was dead and is gone). All caught otherwise. - Quit mid-edit and Scanlines (13.17, Swift 1.4.4). Quit (`1aad384`): `ProjectWorkspace::settlePendingEdits` twins Swift's: on the tab on screen a pending gradient is applied (`commitGradient`), then a pixel move (`finishPixelMove`), then a filter, Hue/Saturation, Levels, Color Range, the Expand/Contract/Feather panel (`setSelectionAmountOperation(nullopt)`) and the colour picker are cancelled (their Cancel, so nothing un-OK'd lands). It takes a completion callback (Swift's `async`), run from the event loop once both commits have landed, and only while the workspace lives (`QPointer`), the tab kept alive in the callbacks as every workspace flow keeps it. `confirmQuit` refuses at once while the workspace manages (Swift's guard moved there from `applicationShouldTerminate`; the twin's window close reaches `confirmQuit` directly, so this guard is what stops a second close mid-quit), applies open text, settles, and only then asks `canSwitch`; a refusal there (a busy project, a commit still running) is a warning on `omaphoto.app` where Swift beeps. Swift's `gradientEdit != nil` and `pixelMove != nil` tests before awaiting are left out: both twins return (posted) when there is nothing to commit. Swift's `finishAdjustmentEditing(commit: false)` is left out as dead: `cancelFilter`, `cancelHueSaturation` and `cancelLevels`, each called before it, call it first, and an adjustment's editor is always one of those three (its request and original are set and cleared with them; while the editor still renders, `finishAdjustmentEditing` does nothing in Swift either, and `canSwitch` refuses the quit). Tests: `QuitPendingEditsTests` (the tab on screen settled and another left as it is; a quit refused while an adjustment's editor is asked for and not yet rendered, then cancelled once open; Swift's `quittingAppliesAPendingGradient` and `quittingCancelsAnOpenDialog`; a pixel move applied as one `Move Pixels` step; Color Range putting the selection back, the selection-amount panel, the colour picker not taking its colour; an adjustment's editor putting the settings back; a workspace gone mid-settle calling nothing; closing the window applying the gradient before the Save question; a busy project refused with the warning, nothing asked or applied; a second quit while the first asks refused unsettled). Scanlines (`9c99853`, `cd2f998`): `DitherStyle::scanlines` ("Scanlines (CRT)") joins the fourth group; `usesPixelSize` (false for ASCII and Scanlines: no chunky pixels, no Pixel Size or Pixel Shape rows) and `drawsMarks` without Scanlines, as Swift's. `DitherSettings` gains `lineSpacing` (2–32, rounded, 4), `glow` (0–100, 35), `dots` (0–100, 0) and `wobble` (0–64 px, unrounded, 0), normalized as Swift's; the kernel's cell is the line spacing for Scanlines. `kernels/DitherPixels.c/.h` are cd2f998's, with one adaptation: Swift's kernel threads through GCD (`dispatch_apply`) and Apple blocks, which GCC cannot compile. `in_bands` keeps Swift's split (one band under 64 items, else 32) but takes a C function and a context; the three block bodies (the tone pass, the scanlines, the glow) are moved word for word into static functions with context structs (`tone_job`, `scan_job`, `glow_job`), and `__block int failed` is a C11 `atomic_int`. The bands run through `dither_bands`, a C hook defined in `Document/Dither.cpp` on `PoolMap::blocking` (QtConcurrent, so a filter worker lends its pool place). The arithmetic is unchanged; this is a forced difference (GCC has no blocks or GCD), agreed with the coordinator. `bandsDrawAsOneBandDoes` holds 32 bands against one: the top rows of a 300-row picture equal a 62-row picture's, which runs as one band in both passes. `glowing` twins Swift's: sigma `lineSpacing × 3 + 3`, shrunk by `max(1, floor(sigma / 4))`, blurred at `sigma / shrink`, scaled back and added through `dither_glow` at `glow / 100 × 2.5`. Core Image clamps to the extent before it shrinks, so the twin pads the image by the blur's reach (three sigmas, a whole number of shrink steps) with its edges repeated, the far sides filled to a whole number of steps so the shrink and the enlargement are both exactly the shrink (`theGlowScalesBackExactlyOffTheStep`), shrinks that with `QImage::scaled`'s smooth filter (an area average, where Core Image samples bilinearly), blurs with the edge repeated (`PixelAdjust::gaussianBlur`), scales back bilinearly and crops the middle; it stays within three levels of light of Swift's first commit's full-size blur (`theGlowIsTheBlurredLightAtItsAmount`, both axes). The sheet adds Line Spacing (px), Glow (%), Dots (%) and Wobble (px) with Swift's help under Scanlines, and Pixel Size and Pixel Shape follow `usesPixelSize`. Tests: `ScanlinesTests` (Swift's four `DitherTests`; the beam's rows for white, 0.35 gray and a striped average; the style's flags and that pixel size, shape and cell size change nothing; the four settings' clamps, the rows' units and ranges read from the sliders, rounding and defaults; Two Colors' screen and Original's black; clear pixels; every band of tall images, 32 bands and a short last one; the glow against a full-size Gaussian at 20% and 100%, its fade, and alpha; the sheet's rows, help, slider ends and fields), `DitherTests` and `DitherSheetTests` (the eleventh style in names, groups and the menu). Equivalent mutants: a pad of one sigma for three (the padded edge repeats the image's own edge, so the light differs by under a level), the shrunk blur without edge repetition (the pad keeps its reach off the edge), the kernel's `start < end` (a band past the end runs no line) and its band count (it only spreads the work). `settlePendingEdits` calls `done` directly: it already runs from the session's posted callback, after both commits. Untested path: the kernel's scan row that cannot be allocated (`failed`, a few kilobytes per band). Swift's beep on a refused quit is a warning on `omaphoto.app`. - Camera Raw curves as Photoshop's (13.18, Swift 1.4.5: `60d9117`): `kernels/AdjustPixels.c/.h` are 60d9117's: the tone curve runs on red, green and blue alike (`toneLut`, Swift's rename of `lumaLut`), and Refine Saturation means what Swift's now means: 0 the curve per channel, below 0 easing toward the colour scaled to the curved brightness (−100 brightness alone), above 0 pushing the curved colour from its luminance by 1 + the amount. `CameraRawCurveSettings::parametric` twins Swift's: unchanged with all four amounts at 0; otherwise 33 anchors, each `bend` of `bend` (shadows and highlights past the outer dividers, then darks and lights either side of the middle one, gamma curves of strength 1.66), run through the same curve as Image › Curves (`point`), so the regions meet without a corner. `bend` is a private static, as Swift's; `lumaTable` is `toneTable`. The twin's normalization of the dividers (5–90, +2 apart, 98 at most) still clamps what a drag writes. The graph's gestures (`UI/CameraRawColorControls+Curve.cpp`, beside the controls' file) are Swift's single `DragGesture`: a left press picks once (`parametricDrag`: within 18 points of the bottom the nearest divider, the first of equals; elsewhere the region whose tone range holds the press, a tone on a divider joining the region above; `pointDrag`: the point nearest by distance within 0.055, chosen; else a new point there, if fewer than 16, inside x 0.01–0.99 and over a hundredth from every point's x, sorted in, stored and chosen), then `continueDrag` runs at the press and on every left-button move: a point's output follows the pointer, an inner point's x stays a hundredth inside its neighbours, an end keeps its x; a divider follows, shadow below dark − 2 and dark below light − 2; a region's amount is its start minus the drag's height over the graph's times 200, within ±100, rounded. Left out as dead, since normalizing does each (`CameraRawCurveSettings::normalized`: amounts, dividers 5–90, +2 apart, 98 at most, and the repair's 0–1 outputs): Swift's 2–98 clamp of a divider, dark's `shadow + 2` floor and light's `dark + 2` floor, and the 0–1 clamps of a dragged or new point's output (a press inside the graph is inside 0–1 anyway). The new point's `x > 0.01` and `x < 0.99` are dead too: the ends sit at 0 and 1, and a point within a hundredth of any x is refused. `bend`'s `lower > 0` and `upper < 1` are dead (no tone lies below 0 or above 1). The release lets go. Qt can lose a release, so every press picks afresh. A double click is Qt's second press: it picks and drags as a press (Swift's drag sees it too), then on the point page removes the nearest inner point by x within 0.04 and drops the choice, and lets go of the drag (Swift removes at the tap's end, which ends the drag). Swift re-picks on each move while a press picked nothing; with the same start and points it picks nothing again, so the twin leaves that out. The chosen point (Swift's `selected`) draws in `Highlight`, the accent, and the readout shows only while it exists, following the drag; another channel drops the choice and the drag (Swift's `onChange`). The graph's help and Refine Saturation's are Swift's new words. Swift's `CameraRawMixerControls` gains the same `Drag` and `selected` state but never reads it: dead, left out. A fix beyond Swift: Swift holds an inner point exactly a hundredth from its neighbours, where the curve's repair (`x > kept.x + 0.01`) drops it (or drops the next point), and the drag's index then names another point; the twin holds it a billionth further inside, so the repair keeps it and the drag stays on it (`aPointIsPickedByDistanceAndKept`, `endPointsStayAndARealDoubleClickRemoves`, both failing before the fix; `CameraRawColorTests` pins the repair's strict `>` at exactly a hundredth, which the fix rests on). A fix beyond Swift: a preset's points drop the choice (TASKS asks for it; Swift's `applyPreset` keeps the index, which then lights whatever point holds it); Custom changes nothing and keeps it. Tests: `CameraRawCurveTests` (Swift's `parametricCurveIsSmooth`, `parametricCurveMatchesPhotoshop`, `curveDeepensColorLikePhotoshop`; each anchor against the bend written apart, and the smooth curve between; each region bending only its range, dividers at the ends bending nothing; the tone table through the point curve; Refine Saturation at 0 per channel through the table, −100 brightness with the hue kept, −50 halfway, +50 half again from luminance), `CameraRawCurveGraphTests` (a double click's 0.04 reach by x, and one in the divider strip moving the divider; regions lifted and lowered from their start, clamped and rounded, picked once by tone; dividers in the strip, nearest and first of equals, kept ordered and within the settings' limits; points by distance, kept through the drag, a new point added and dragged by the same press, ends moving their outputs alone; refusals near an edge, a point's x, at sixteen; the chosen point lit and read out as it moves, ends and new points included; a channel or preset dropping the choice, Custom keeping it, a held drag let go; double clicks removing an inner point and the choice, adding then removing on empty ground, ends staying; other buttons, a lost release, a release letting go; the help), and the existing `CameraRawColorTests`, `CameraRawCurveMixerTests`, `CameraRawGroupDrawTests`, `CameraRawGroupRowsTests`, `CameraRawGroupWordsTests` brought to the new rules and words. Equivalent mutant: `parametric`'s shortcut when all four amounts are 0 (33 anchors on the diagonal give the diagonal; it only spares the work). The kernel's refine branches, Swift's own code, were mutated too and all caught. Swift's `selected` survives a preset; TASKS asks for a preset to clear it, and a stale index lights a point the user never chose, so the twin clears it (a fix beyond Swift). An inner point dragged onto a neighbour vanished in Swift; fixed beyond Swift at the coordinator's request. -- Held keys, field values and resize snapping in the Move tool (13.13: `bca8f13`, `03d692c`, `686d8c7`, `c459f88`): `UI/HeldModifiers` twins Swift's class, a `QObject` singleton (`shared()`) with one signal, `changed()`. An application event filter reads key presses and releases and left-button presses and releases, Swift's `flagsChanged`, `keyDown`, `leftMouseDown` and `leftMouseUp`; a modifier key's own event sets or clears its bit, since Qt's event may not count it yet. Ctrl, Shift, Alt and Meta count (Swift's Command, Shift, Option, Control). Typing counts as none: Swift's `NSText` first responder is a focus widget with `Qt::WA_InputMethodEnabled`, so a `QLineEdit` and the canvas holding open text alike. `applicationStateChanged` stands for the two activation notifications (inactive clears; active reads `queryKeyboardModifiers`), and a `QMenu`'s `Hide` for `NSMenu.didEndTracking`. `TransformInspector` shows Auto Select as `transformAutoSelect != Ctrl` and the lock as `locksTransformRatio != Shift`, and a click while held writes the other way, as Swift's bindings; it synchronizes on `HeldModifiers::changed` too. Tooltips carry Swift's new words, Ctrl for Command. `transformPressLayer` follows Swift's: `autoSelect = transformAutoSelect != Ctrl`; Ctrl picks the layer under the pointer only with Auto Select off or with Shift (Ctrl+Shift adds either way). `TransformEdit.fromFields` sits last; `beginTransform(persistent, fromFields)` sets it, where Swift writes `transformEdit?.fromFields` in place (the twin's edit is read-only from outside). The bar's `change` opens a non-persistent `fromFields` edit; `finish` commits it, one step, when a field's scrub ends (`NumericScrub`'s `onEnd`) or the field loses focus. A focus borrowed by a menu, a popup or another window in front (`ActiveWindowFocusReason`, new: AppKit keeps the first responder when the app goes to the back) finishes nothing and keeps the typing. Escape in a field throws a `fromFields` edit away before handing the canvas the keys. The Swift path assumed: AppKit sends the Cancel button's Escape key equivalent (`configuredNativeShortcut(.escape)`) before the field editor, the only way `686d8c7`'s "Escape still throws the change away" can hold, since the field's own `onExitCommand` would end the focus and so apply the value. That path would also cancel a Ctrl+T edit from a field (and Return apply it); the twin keeps 6.1's reading there, a Ctrl+T edit waiting for the canvas's keys, a known difference held from 6.1. Flip H, Flip V and Sampling open a `fromFields` edit with no finish of their own, as Swift's do: it applies with the next field's finish, a canvas drag or anything that commits. `beginTransformDrag` commits a `fromFields` edit first, after `transformPressLayer` (so it picks nothing, as Swift's `picks`), so the two stay separate steps. Qt moves the focus before a press arrives, where SwiftUI's focus changes after `mouseDown`: a field left for a mouse press (`MouseFocusReason`) finishes from the event loop (a zero `QTimer`), so a typed value then a press over another layer drags the active layer under Auto Select, as in Swift (review 2's finding; `MoveBarPressTests`). `snappedResizePoint` lives in `Document/Crop.cpp` with Swift's: the edges each axis moves snap on their own to `transformSnapTargets` (the file's `nearest`, the first of ties), the nearer alone when proportional, the pointer nudged by one pixel's measured step; an upright layer only; guides set, or cleared when snapping is off, the drag is no resize or the layer turned. `dragTransform` snaps a resize unless Ctrl is held, proportional as `locksTransformRatio != Shift`. Dead or equivalent: `beginDistort` keeps `fromFields` where Swift's rebuild drops it, but a `fromFields` edit never reaches it (the canvas commits it before any handle drag). `snappedResizePoint`'s `before` read from `update(result)` or `update(point)` is equivalent (unlocked axes are independent, and proportional leaves one snap). The edge's tie (`<=`, the low edge) is reachable (the right handle half a pixel from its anchor, a one-pixel box) and held by `aTieTakesTheLowEdge`; the dragged handle's carried place by a drag past the anchor (`aLongDragSnapsTheEdgeItCarries`); Alt's centre resize by `altResizesFromTheCentreAndSnaps`. `QRectF::normalized` was dropped as dead (a resize keeps sizes positive and flips instead). Untested paths: the menu `Hide` and the activation's `queryKeyboardModifiers` (offscreen holds no real keyboard state). Tests: `MoveBarPressTests` (a typed value then a real press on another layer drags the active layer, two steps; Ctrl with a folder active and Auto Select on drags the folder past its box), `MovePressFixtures.h` (the canvas fixture the three press files share), `TransformPressTests` (Swift's `commandFlipsAutoSelectWhileHeld` both ways and `commandShiftClickAddsALayerWithAutoSelectOn`, Ctrl outside the active layer, a field edit applied before a canvas drag; the old "resizing never snaps" line now snaps), `ResizeSnapCanvasTests` (resize snapping on the canvas with Ctrl mid-drag, snapping off, Shift's proportion), `ResizeSnapTests` (Swift's three, a turned layer swept, ties, nearer edges, free corners, a still edge keeping its guide, the dragged side alone, a press away from the handle, snapping off, a move), `TransformInspectorTests` (applied fields, Return, Escape, Ctrl+T's edit, another window in front, held keys flipping the box and the lock), `ScrubbableTransformTests` (scrubs apply on release, one step each). Judgments: Swift's `03d692c` drops a test the twin can keep (the box flipped while Ctrl is held is `heldKeysShowFlippedInTheBar`). -- The Smear's 1.4.2–1.4.3 fixes (13.16, part one: `7164dd4`, `ad9ad7c`, `504129d`, and `095da2f`'s commit half): Liquify is Swift's new GPU stroke on the CPU, as the twin has no Metal: `WarpStroke` keeps the layer as it found it (`m_original`, a copy made in the constructor) and a source offset per pixel (`m_offsets`, two floats each, zero at first); each dab copies the offsets under it to `m_scratch`, reads them bilinearly from behind the brush's travel, less the travel times the weight, writes them back, and draws the pixel afresh from the untouched layer, bilinear, held to its edges, each byte rounded once: `warp_push` of `MetalWarp.swift`, float for float (bytes over 255, mixed, times 255). Swift allocates the two textures on the first push; the twin allocates them with the stroke, before any dab (nothing changes the copy between), so a failure is `ExportError(render)` where `beginWarp` catches it, where Swift's guard returns in silence; Smudge's carried colour, allocated at the first dab inside `beginWarp`, fails the same way, and a dab's scratch that finds no memory skips that dab with a warning on `compositor.rendering`, Swift's guard on its scratch texture (review 2: it ran outside any catch). The colour reads keep the shader's `size - 2` as they are: the offsets' read above needs two columns and rows of the dab's area (`ix`, `iy`), which a canvas a pixel wide or tall lacks, so no read leaves the image; the clamps first written here for such canvases were dead and are removed (review 2). Smudge is `ad9ad7c` in both of Swift's paths: dabs every 0.005 of the width (a pixel at least), each dab lays what it carries at weight × strength, and carries what it just left, nothing older. `BrushSettings.blurRadius` (5) is the Blur's own Radius: the sample's sigma follows the Radius (see part two for the merged formula). `BrushControls` shows Radius for the Blur mode alone, after Strength: a scrubbable label (a tenth a point, 0.5–50), a 100-point slider over 0.5–20 in hundredths (past 20 Qt rests it at its end, so Swift's `min(20, …)` is left out), and a 42-point `PickerField` with "px": a readable number applies on Return or leaving, clamped, a number that is not finite takes 5 (Swift's binding), other text goes back, arrows step one (Shift ten) from the session's value, clamped; it prints Swift's `fractionLength(0...1)`, never grouped. `BrushStroke::maxDiameter` is 2100, so `finishWarp`'s tip four wider than a 2000 brush is no longer refused; `finishWarp` appends a dab centre only every twentieth of the brush (the last always), as Swift's; the thinning is `WarpStroke::committedPoints`, so the test reads it directly (`aCommitKeepsATwentiethAndTheLastPoint`). Part two (`095da2f`'s Blur half, on 13.11's grid sampling, cc9bf95 merged into port-d): `BrushStroke::Clone` is 13.11's `(image, placed, inGrid)` plus `render`, a function that makes a part of `image` (Swift's `cloneRender`; Swift keeps it beside `clone`, the twin inside it, so one assignment sets both), and `clone` is private behind `clone()` and `setClone()`, which drops the pieces cut so far (Swift's two `didSet`s). `clonePixels(key, tile)` is Swift's `clonePiece`: the tile mapped into sample pixels through 13.11's chain (grid, `pixelToDocument` unless `inGrid`, then `placed` to the image's pixels, Swift's `scaleX` and `scaleY`), two pixels round, made whole outward (`toAlignedRect`, Swift's `integral`), held to the image; an empty part paints nothing on that tile (the fill is skipped, as Swift's `if let piece`); otherwise the piece is `render(part)` or a copy, cut once per tile key and kept in `m_clonePieces`, and the tile's sample is drawn from it through the same transform shifted by the piece's corner, so every path reads the pixels the whole image would give. A piece that cannot be allocated is `ExportError(render)`, thrown on the calling thread with `publish`'s other copies, where Swift's `cropping` returning nil paints nothing. `blurSample(stroke)` returns the sharp sample in the stroke's grid (13.11's `region`, `fit` and budget unchanged) with `render` blurring a part at `sigma × fit`, padded by the Gaussian's reach (`ceil(3σ)`) and cut back, clear past the sample for pixels (`QImage::copy` fills outside with zeros), its edges repeated for a mask (Swift's `clampedToExtent`), so a piece equals that part of the whole byte for byte (Swift allows one level). The Radius sigma is Swift's `min(min(50, max(0.5, blurRadius)) / perPixel, sidePixels / 2)`. Merge: port-c's direct `clone =` writes became `setClone` (`beginBrush`, `finishWarp`, `cloneSample`'s callers, the tests and `documentClone`), every `Clone` initialiser names `render` (GCC's `-Wmissing-field-initializers`), and port-c's Blur sample tests compare the sharp sample (`framed`) and its softening through `render` (`soft`), with each brush size's old sigma now set as the Radius. Equivalent mutants and untested paths for part two: a piece, a padded copy or a cut-back blur that cannot be allocated (`ExportError`) is untested, as no starvation binary reaches one copy here. Equivalent mutant: allocating the Liquify buffers for every mode (memory only). Held since review 2: the Radius field's non-finite branch ("inf" takes 5); the colour read from the original rather than the context is held by the stripes (`liquifyDrawsFromTheUntouchedLayer`, mutant caught). Left out: Swift's `placed.width > 0` guard in `clonePiece` (every `placed` is a layer's rect, the canvas's or a padded region, never empty), and `gaussianBlur`'s edge flag inside `render` (the pad holds the edges, so the blur's own edge rule never reaches a part; it passes `false`). Untested: the original's copy failing (no starvation binary reaches a single image copy here). Tests: `SmudgeLiquifyTests` (the oracle rewritten from Swift's new `smudge` and `warp_push`, every byte; pushes in from each edge over ramps; the session's smudge values pinned; a rim pixel the liquify moved), `WarpStrokeTests` (Swift's `liquifyStaysSharp` on Swift's photo, under one level where Swift measured 0.49; half-pixel pushes keep stripes crisp; Swift's `smudgeLeavesOneFadingTrail`), `LargeCanvasBrushTests` (Swift's `largestBrushCommits` for Liquify, Smudge and Blur on 5000 square at 2000; Swift's `blurPiecesMatchTheWhole` for pixels and a mask, exact; pieces cut once per tile, two pixels round, dropped by a new clone, none past the clone's end where the tile stays as it was; a thinned commit lands all a wide hard warp moved, straight and round a tight circle), `BlurBrushTests` (Swift's `radiusSetsHowFarItSoftens`, 6 and 42 columns, size alone; the clamps), `BlurToolTests` (port-c's sample tests on the Radius: the sharp sample and its softening, 0.5 and half the layer's side, 50 on a wide layer), `BrushControlsTests` (the Radius row, "inf" taking 5), `BrushTests` (2100 is taken, past it refused). Judgments: `7164dd4` drops Liquify from Swift's GPU-against-CPU test, and Swift's CPU fallback keeps resampling; the twin has one path, so it takes the GPU's (what every Mac with Metal runs), and its oracle replays the shader. +- Held keys, field values and resize snapping in the Move tool (13.13: `bca8f13`, `03d692c`, `686d8c7`, `c459f88`): `UI/HeldModifiers` twins Swift's class, a `QObject` singleton (`shared()`) with one signal, `changed()`. An application event filter reads key presses and releases and left-button presses and releases, Swift's `flagsChanged`, `keyDown`, `leftMouseDown` and `leftMouseUp`; a modifier key's own event sets or clears its bit, since Qt's event may not count it yet. Ctrl, Shift, Alt and Meta count (Swift's Command, Shift, Option, Control). Typing counts as none: Swift's `NSText` first responder is a focus widget with `Qt::WA_InputMethodEnabled`, so a `QLineEdit` and the canvas holding open text alike. `applicationStateChanged` stands for the two activation notifications (inactive clears; active reads `queryKeyboardModifiers`), and a `QMenu`'s `Hide` for `NSMenu.didEndTracking`. `TransformInspector` shows Auto Select as `transformAutoSelect != Ctrl` and the lock as `locksTransformRatio != Shift`, and a click while held writes the other way, as Swift's bindings; it synchronizes on `HeldModifiers::changed` too. Tooltips carry Swift's new words, Ctrl for Command. `transformPressLayer` follows Swift's: `autoSelect = transformAutoSelect != Ctrl`; Ctrl picks the layer under the pointer only with Auto Select off or with Shift (Ctrl+Shift adds either way). `TransformEdit.fromFields` sits last; `beginTransform(persistent, fromFields)` sets it, where Swift writes `transformEdit?.fromFields` in place (the twin's edit is read-only from outside). The bar's `change` opens a non-persistent `fromFields` edit; `finish` commits it, one step, when a field's scrub ends (`NumericScrub`'s `onEnd`) or the field loses focus. A focus borrowed by a menu, a popup or another window in front (`ActiveWindowFocusReason`, new: AppKit keeps the first responder when the app goes to the back) finishes nothing and keeps the typing. Escape in a field throws a `fromFields` edit away before handing the canvas the keys. The Swift path assumed: AppKit sends the Cancel button's Escape key equivalent (`configuredNativeShortcut(.escape)`) before the field editor, the only way `686d8c7`'s "Escape still throws the change away" can hold, since the field's own `onExitCommand` would end the focus and so apply the value. That path would also cancel a Ctrl+T edit from a field (and Return apply it); the twin keeps 6.1's reading there, a Ctrl+T edit waiting for the canvas's keys, a known difference held from 6.1. Flip H, Flip V and Sampling open a `fromFields` edit with no finish of their own, as Swift's do: it applies with the next field's finish, a canvas drag or anything that commits. `beginTransformDrag` commits a `fromFields` edit first, after `transformPressLayer` (so it picks nothing, as Swift's `picks`), so the two stay separate steps. Qt moves the focus before a press arrives, where SwiftUI's focus changes after `mouseDown`: a field left for a mouse press (`MouseFocusReason`) finishes from the event loop (a zero `QTimer`), so a typed value then a press over another layer drags the active layer under Auto Select, as in Swift (review 2's finding; `MoveBarPressTests`). `snappedResizePoint` lives in `Document/Crop.cpp` with Swift's: the edges each axis moves snap on their own to `transformSnapTargets` (the file's `nearest`, the first of ties), the nearer alone when proportional, the pointer nudged by one pixel's measured step; an upright layer only; guides set, or cleared when snapping is off, the drag is no resize or the layer turned. `dragTransform` snaps a resize unless Ctrl is held, proportional as `locksTransformRatio != Shift`. Dead or equivalent: `beginDistort` keeps `fromFields` where Swift's rebuild drops it, but a `fromFields` edit never reaches it (the canvas commits it before any handle drag). `snappedResizePoint`'s `before` read from `update(result)` or `update(point)` is equivalent (unlocked axes are independent, and proportional leaves one snap). The edge's tie (`<=`, the low edge) is reachable (the right handle half a pixel from its anchor, a one-pixel box) and held by `aTieTakesTheLowEdge`; the dragged handle's carried place by a drag past the anchor (`aLongDragSnapsTheEdgeItCarries`); Alt's centre resize by `altResizesFromTheCentreAndSnaps`. `QRectF::normalized` was dropped as dead (a resize keeps sizes positive and flips instead). Untested paths: the menu `Hide` and the activation's `queryKeyboardModifiers` (offscreen holds no real keyboard state). Tests: `MoveBarPressTests` (a typed value then a real press on another layer drags the active layer, two steps; Ctrl with a folder active and Auto Select on drags the folder past its box; a value left by a click in another field applies alone, one step), `MovePressFixtures.h` (the canvas fixture the three press files share), `TransformPressTests` (Swift's `commandFlipsAutoSelectWhileHeld` both ways and `commandShiftClickAddsALayerWithAutoSelectOn`, Ctrl outside the active layer, a field edit applied before a canvas drag; the old "resizing never snaps" line now snaps), `ResizeSnapCanvasTests` (resize snapping on the canvas with Ctrl mid-drag, snapping off, Shift's proportion), `ResizeSnapTests` (Swift's three, a turned layer swept, ties, nearer edges, free corners, a still edge keeping its guide, the dragged side alone, a press away from the handle, snapping off, a move), `TransformInspectorTests` (applied fields, Return, Escape, Ctrl+T's edit, another window in front, held keys flipping the box and the lock), `ScrubbableTransformTests` (scrubs apply on release, one step each). Judgments: Swift's `03d692c` drops a test the twin can keep (the box flipped while Ctrl is held is `heldKeysShowFlippedInTheBar`). +- The Smear's 1.4.2–1.4.3 fixes (13.16, part one: `7164dd4`, `ad9ad7c`, `504129d`, and `095da2f`'s commit half): Liquify is Swift's new GPU stroke on the CPU, as the twin has no Metal: `WarpStroke` keeps the layer as it found it (`m_original`, a copy made in the constructor) and a source offset per pixel (`m_offsets`, two floats each, zero at first); each dab copies the offsets under it to `m_scratch`, reads them bilinearly from behind the brush's travel, less the travel times the weight, writes them back, and draws the pixel afresh from the untouched layer, bilinear, held to its edges, each byte rounded once: `warp_push` of `MetalWarp.swift`, float for float (bytes over 255, mixed, times 255). Swift allocates the two textures on the first push; the twin allocates them with the stroke, before any dab (nothing changes the copy between), so a failure is `ExportError(render)` where `beginWarp` catches it, where Swift's guard returns in silence; Smudge's carried colour, allocated at the first dab inside `beginWarp`, fails the same way, and a dab's scratch that finds no memory skips that dab with a warning on `omaphoto.rendering`, Swift's guard on its scratch texture (review 2: it ran outside any catch). The colour reads keep the shader's `size - 2` as they are: the offsets' read above needs two columns and rows of the dab's area (`ix`, `iy`), which a canvas a pixel wide or tall lacks, so no read leaves the image; the clamps first written here for such canvases were dead and are removed (review 2). Smudge is `ad9ad7c` in both of Swift's paths: dabs every 0.005 of the width (a pixel at least), each dab lays what it carries at weight × strength, and carries what it just left, nothing older. `BrushSettings.blurRadius` (5) is the Blur's own Radius: the sample's sigma follows the Radius (see part two for the merged formula). `BrushControls` shows Radius for the Blur mode alone, after Strength: a scrubbable label (a tenth a point, 0.5–50), a 100-point slider over 0.5–20 in hundredths (past 20 Qt rests it at its end, so Swift's `min(20, …)` is left out), and a 42-point `PickerField` with "px": a readable number applies on Return or leaving, clamped, a number that is not finite takes 5 (Swift's binding), other text goes back, arrows step one (Shift ten) from the session's value, clamped; it prints Swift's `fractionLength(0...1)`, never grouped. `BrushStroke::maxDiameter` is 2100, so `finishWarp`'s tip four wider than a 2000 brush is no longer refused; `finishWarp` appends a dab centre only every twentieth of the brush (the last always), as Swift's; the thinning is `WarpStroke::committedPoints`, so the test reads it directly (`aCommitKeepsATwentiethAndTheLastPoint`). Part two (`095da2f`'s Blur half, on 13.11's grid sampling, cc9bf95 merged into port-d): `BrushStroke::Clone` is 13.11's `(image, placed, inGrid)` plus `render`, a function that makes a part of `image` (Swift's `cloneRender`; Swift keeps it beside `clone`, the twin inside it, so one assignment sets both), and `clone` is private behind `clone()` and `setClone()`, which drops the pieces cut so far (Swift's two `didSet`s). `clonePixels(key, tile)` is Swift's `clonePiece`: the tile mapped into sample pixels through 13.11's chain (grid, `pixelToDocument` unless `inGrid`, then `placed` to the image's pixels, Swift's `scaleX` and `scaleY`), two pixels round, made whole outward (`toAlignedRect`, Swift's `integral`), held to the image; an empty part paints nothing on that tile (the fill is skipped, as Swift's `if let piece`); otherwise the piece is `render(part)` or a copy, cut once per tile key and kept in `m_clonePieces`, and the tile's sample is drawn from it through the same transform shifted by the piece's corner, so every path reads the pixels the whole image would give. A piece that cannot be allocated is `ExportError(render)`, thrown on the calling thread with `publish`'s other copies, where Swift's `cropping` returning nil paints nothing. `blurSample(stroke)` returns the sharp sample in the stroke's grid (13.11's `region`, `fit` and budget unchanged) with `render` blurring a part at `sigma × fit`, padded by the Gaussian's reach (`ceil(3σ)`) and cut back, clear past the sample for pixels (`QImage::copy` fills outside with zeros), its edges repeated for a mask (Swift's `clampedToExtent`), so a piece equals that part of the whole byte for byte (Swift allows one level). The Radius sigma is Swift's `min(min(50, max(0.5, blurRadius)) / perPixel, sidePixels / 2)`. Merge: port-c's direct `clone =` writes became `setClone` (`beginBrush`, `finishWarp`, `cloneSample`'s callers, the tests and `documentClone`), every `Clone` initialiser names `render` (GCC's `-Wmissing-field-initializers`), and port-c's Blur sample tests compare the sharp sample (`framed`) and its softening through `render` (`soft`), with each brush size's old sigma now set as the Radius. Equivalent mutants and untested paths for part two: a piece, a padded copy or a cut-back blur that cannot be allocated (`ExportError`) is untested, as no starvation binary reaches one copy here. Allocating the Liquify buffers for every mode is caught: Smudge then needs room it lacks in `WarpFailureTests`. Held since review 2: the Radius field's non-finite branch ("inf" takes 5); the colour read from the original rather than the context is held by the stripes (`liquifyDrawsFromTheUntouchedLayer`, mutant caught). Left out: Swift's `placed.width > 0` guard in `clonePiece` (every `placed` is a layer's rect, the canvas's or a padded region, never empty), and `gaussianBlur`'s edge flag inside `render` (the pad holds the edges, so the blur's own edge rule never reaches a part; it passes `false`). Equivalent under starvation: the untouched copy's null check (the offsets, twice its size, fail after it). Tests: `SmudgeLiquifyTests` (the oracle rewritten from Swift's new `smudge` and `warp_push`, every byte; pushes in from each edge over ramps; the session's smudge values pinned; a rim pixel the liquify moved), `WarpStrokeTests` (Swift's `liquifyStaysSharp` on Swift's photo, under one level where Swift measured 0.49; half-pixel pushes keep stripes crisp; Swift's `smudgeLeavesOneFadingTrail`; the commit's points a twentieth apart, exactly that apart kept, a pixel at least), `WarpFailureTests` (a Liquify dab without scratch memory skipped, Smudge without room to carry and Liquify without room for its offsets a render error), `LargeCanvasBrushTests` (Swift's `largestBrushCommits` for Liquify, Smudge and Blur on 5000 square at 2000; Swift's `blurPiecesMatchTheWhole` for pixels and a mask, exact; pieces cut once per tile, two pixels round, dropped by a new clone, none past the clone's end where the tile stays as it was; a thinned commit lands all a wide hard warp moved, straight and round a tight circle), `BlurBrushTests` (Swift's `radiusSetsHowFarItSoftens`, 6 and 42 columns, size alone; the clamps), `BlurToolTests` (port-c's sample tests on the Radius: the sharp sample and its softening, 0.5 and half the layer's side, 50 on a wide layer), `BrushControlsTests` (the Radius row, "inf" taking 5), `BrushTests` (2100 is taken, past it refused). Judgments: `7164dd4` drops Liquify from Swift's GPU-against-CPU test, and Swift's CPU fallback keeps resampling; the twin has one path, so it takes the GPU's (what every Mac with Metal runs), and its oracle replays the shader. - Drops between projects: `ProjectWorkspace::draggedLayerSource` finds the tab whose document holds the dragged id (Swift reads the drag pasteboard; the twin reads the drag's `QMimeData`, which every drop target has), `canReceiveDrag(data, destination)` refuses only a layer's own tab, and `receiveProviders` copies several ids one after another, as Swift awaits each provider, so a two-row drop on the New button opens two tabs as Swift's does. `ContentView` takes such a drop over its whole area (`setWorkspace` hands it the workspace; `acceptsDrop` keeps Swift's guards: busy, the sheet, the importer, a rename; `levels` arrives with 9.x), shows the 3-point accent ring inside the canvas while targeted and hands the drop's document point on when it fell on the canvas. `ProjectTabButton` takes drops for its tab (accent fill and rim, "Add to "), and `NewCanvasButton`, the toolbar's New canvas button with the action as its default, for a fresh tab ("Open in a new project tab"), both refusing an Alt drag, which duplicates in the panel; Swift's transient `NewTabDropSlot` in the strip has no twin, the New canvas button is `NewProjectDropTarget` already. Files and pictures arrived with 10.4. - `CanvasThumbnail` draws a layer or a mask inside the canvas's box at `backingScale` 2 (the panel's pixmaps carry that ratio); a mask's ground is `LayerMask::background`, white or black by its edge's majority, as the canvas treats it (Swift's `edgeTone` mean went with 12.12b). `LayerIcons` draws the panel's SF Symbols as paths, in one palette colour, like `ToolIcons`. `BlendModePicker` is a `QComboBox` whose popup previews the highlighted mode through `previewBlendMode` for the layer it opened on (Qt calls `hidePopup` before `activated`, so the preview, the target and the highlight are cleared from the event loop, after a choice had its chance) and refuses a choice once another layer is active. `LayerAppearanceControls` is Swift's blend and opacity row: the slider's press and release are one opacity drag (the release is watched application-wide while the slider is down, so a popup opened mid-drag cannot keep the step open), the percent field applies once through its `FocusOut`, which Return and Escape cause (Swift's `releasesFocusOnCommit`), and steps with Up and Down (Swift's `isFinite` test and the steps' clamp are left out: `setLayerOpacity` refuses a value that is no finite number and clamps the rest), and another active layer makes the controls anew, as Swift's `.id` does. - `ProjectWorkspaceView` is the `QMainWindow`: the front tab's `ContentView` as its central widget, made anew when another tab comes to the front, as SwiftUI's `.id` rebuilds it, and disabled while the workspace manages; the window holds the shown tab (`std::shared_ptr`) until the old editor is deleted, as Swift's views hold their session, so a replaced or closed tab's session never ends under its editor (`ProjectWorkspace::tab` is public for this); a `QToolBar` with the New canvas action, the `ProjectTabStrip` (which takes the toolbar's free width) and the four zoom actions, since SwiftUI's `.toolbar` modifier lives in `ContentView` but the toolbar belongs to the window. Its actions follow the front session's `changed()`, and on that signal the window syncs every tab's button as well, because a busy front tab dims the other tabs' buttons, which their own sessions' signals do not reach; the strip syncs its buttons on the workspace's signal, each button on its own session's. The window's title is the front tab's, with Qt's `[*]` placeholder, so `setWindowModified` can show the mark that `isDocumentEdited` shows; `setWindowFilePath` carries `representedURL`. The editor's own minimum size is cleared inside the window, which owns the 800 × 520 minimum for the whole, toolbar included. Closing the window ignores the event and asks the workspace (`closeWindow`), which confirms tab by tab and then closes the window again with the property `closeConfirmed` set, since the workspace may not include `UI/` headers; a confirmed close is accepted and the mark cleared, so a window shown again asks again. `ProjectTabStrip` is a plain `QWidget` whose buttons are kept for the tabs that stay, made for new tabs and deleted for tabs that went, and placed by hand from `projectTabOverflow` (13.14): every button at its own size hint, so no title is squeezed below Swift's 35 points, laid out again when the strip is resized, the workspace changes or any tab's session changes; it fills no background, so the toolbar shows through (a stylesheet would do the same but freezes the widgets' palette, so a theme switch would pass them by); the unsaved dot is a character before the title. Files named on the command line go to `ProjectWorkspace::receive`, as Finder's Open With reaches the delegate. Layer drops on tabs and the New canvas button are 4.7's; file and picture drops arrived with 10.4. diff --git a/TASKS.md b/TASKS.md index ab9ca4f..f6581bd 100644 --- a/TASKS.md +++ b/TASKS.md @@ -480,7 +480,10 @@ Reviews for this section ran on codex `gpt-6.1-sol` at `max` (agent `codex-sol`) - 13.19d review: round 1 FAIL: the 8.6 bullet's whole-sample blur timing, the 13.11 bullet's typed values as persistent, `->` on optionals in `MaskAloneTests`. Fixed; the same `->` in `CanvasEditContractTests`, `ImageFileDropTests`, `RawDevelopTests` and `SessionRecord.h` went too. Their run found `SelectionSignalTests` stale since 13.11 (Inverse of everything deselects, so the `deselect` after it is silent): the test now says so. - 13.19d round 2: FAIL: other tests still read optionals through `->` and `*`, through aliases a text scan missed. Found by the compiler: in a throwaway container `std::optional`'s `operator->` and `operator*` were marked deprecated and every target built (`-O0`); 271 sites in 30 test files and headers, each now `.value()`, and the rerun finds none in `tests/` with every unit compiled. The full suite at `-j8` then failed `FilterSessionTests` and `SelectionFeatherTests` alone too: both still called `addMask(false)` to reveal a selection, the meaning 13.12 flipped (`0b0d36a`); now `addMask(true)`. 329 of 329 at `-j8`. - 13.19d round 3: PASS. - - [ ] 13.19e `70318de` (13.13 and 13.16 fixes and tests) built and run. + - [x] 13.19e `70318de` (13.13 and 13.16 fixes and tests) built and run. + - 13.19e work: `70318de`'s tests pass. Mutants: the field's deferred finish on a mouse press, both ways, caught; `committedPoints` at twice the spacing, without its last-point term, without `kept`, caught, and its `<=` and its one-pixel floor survived until rows for exactly the spacing and a small brush were added, now caught. `WarpFailureTests` (new, a binary of its own) starves a Liquify dab's scratch (skipped with its warning), Smudge's carried colour and Liquify's offsets (each `ExportError(render)`); each guard's removal caught; the untouched copy's null check is equivalent under starvation (the offsets after it are twice its size). `blurSample` drops Swift's dead `if let` on the mask and `?? 0` for the document. The 13.16 line named `compositor.rendering`; it is `omaphoto.rendering`. 330 of 330 at `-j8`. + - 13.19e review: round 1 FAIL: the starvation tests took any `ExportError` kind; no test made the Move bar's deferred finish run by itself; AGENTS still called allocating Liquify's buffers for every mode equivalent. Fixed: both tests compare the kind (`encode` and `tooLarge` mutants caught), `aValueLeftByMouseForAnotherFieldApplies` clicks Y with no canvas press (dropping the deferred finish caught), the AGENTS line says caught. + - 13.19e round 2: PASS. - [ ] 13.19f The fixes since `81894f7` already tested (`382c9c2` for 13.9a and 13.9b, `6735aad` for 13.7, 13.14 and 13.15, `5c8e18c` for 13.17 and 13.18) reviewed: review 3's scope. - [ ] 13.19g The full suite under `ctest -j16`; the shared state behind the five parallel failures found and isolated. - [ ] 13.19h The acceptance run and its shots. diff --git a/src/Document/BlurTool.cpp b/src/Document/BlurTool.cpp index 4288500..ffd73b1 100644 --- a/src/Document/BlurTool.cpp +++ b/src/Document/BlurTool.cpp @@ -55,7 +55,7 @@ std::optional<BrushStroke::Clone> EditorSession::blurSample(const BrushStroke &s const double margin = std::ceil(3 * sigma); const QRectF region = stroke.sourceRect.adjusted(-margin, -margin, margin, margin); // A huge layer's sample is made coarser, within several canvases. - const qint64 canvas = m_document ? qint64(m_document->width) * m_document->height : 0; + const qint64 canvas = qint64(m_document.value().width) * m_document.value().height; const double budget = double(std::min(DocumentLimits::maxSurfacePixels, std::max<qint64>(16'000'000, 4 * canvas))); const double fit = std::min(1.0, std::sqrt(budget / (region.width() * region.height()))); const int width = std::max(1, int(std::ceil(region.width() * fit))), height = std::max(1, int(std::ceil(region.height() * fit))); @@ -63,8 +63,8 @@ std::optional<BrushStroke::Clone> EditorSession::blurSample(const BrushStroke &s try { QImage context = BrushRaster::context(width, height, stroke.isMask); // Past its pixels a mask keeps its edge tone. - if (stroke.isMask && layer.mask) - context.fill(int(std::round(LayerMask::background(layer.mask->asset.thumbnail) * 255))); + if (stroke.isMask) + context.fill(int(std::round(LayerMask::background(asset->thumbnail) * 255))); QPainter painter(&context); // A painted asset flattens here; the catch covers it too. BrushRaster::draw(asset->image(), placed, painter); diff --git a/tests/MoveBarPressTests.cpp b/tests/MoveBarPressTests.cpp index 054b864..51387f2 100644 --- a/tests/MoveBarPressTests.cpp +++ b/tests/MoveBarPressTests.cpp @@ -8,6 +8,7 @@ class MoveBarPressTests : public QObject { private slots: void aTypedValueThenAPressDragsTheActiveLayer(); void ctrlKeepsAFolderAsAutoSelectPicksElsewhere(); + void aValueLeftByMouseForAnotherFieldApplies(); }; // Swift picks before focus moves: the active layer drags. @@ -69,5 +70,33 @@ void MoveBarPressTests::ctrlKeepsAFolderAsAutoSelectPicksElsewhere() QCOMPARE(session.activeLayerID(), std::optional(blue)); } +// No canvas press: the deferred finish alone applies the value. +void MoveBarPressTests::aValueLeftByMouseForAnotherFieldApplies() +{ + Canvas shown; + EditorSession &session = shown.session; + // Wide enough that Y shows beside X. + shown.window.resize(1000, 350); + TransformInspector bar(session, &shown.window); + bar.setGeometry(0, 300, 1000, 42); + bar.show(); + QLineEdit *x = bar.findChild<QLineEdit *>(QStringLiteral("transformX")); + QLineEdit *y = bar.findChild<QLineEdit *>(QStringLiteral("transformY")); + QVERIFY(x && y); + x->setFocus(); + QTRY_VERIFY(x->hasFocus()); + x->selectAll(); + QTest::keyClicks(x, "12"); + QVERIFY(session.transformEdit().value().fromFields); + const int steps = session.history.undoCount(); + QWindow *window = shown.window.windowHandle(); + const QPoint inY = y->mapTo(&shown.window, y->rect().center()); + QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, inY); + QTRY_VERIFY(y->hasFocus()); + QTRY_VERIFY(!session.transformEdit().has_value()); + QCOMPARE(shown.origin(), QPointF(12, 100)); + QCOMPARE(session.history.undoCount(), steps + 1); +} + QTEST_MAIN(MoveBarPressTests) #include "MoveBarPressTests.moc" diff --git a/tests/Tests.cmake b/tests/Tests.cmake index d9c13fa..c53f45a 100644 --- a/tests/Tests.cmake +++ b/tests/Tests.cmake @@ -42,6 +42,7 @@ compositor_test(CloneStampTests) compositor_test(CloneStampCanvasTests) compositor_test(SmudgeLiquifyTests) compositor_test(WarpStrokeTests) +compositor_test(WarpFailureTests) compositor_test(LargeCanvasBrushTests) compositor_test(BlurBrushTests) compositor_test(BlurToolTests) diff --git a/tests/WarpFailureTests.cpp b/tests/WarpFailureTests.cpp new file mode 100644 index 0000000..495923c --- /dev/null +++ b/tests/WarpFailureTests.cpp @@ -0,0 +1,90 @@ +#include "AddressSpaceLimit.h" +#include "BrushFixtures.h" +#include "Document/SmudgeLiquify.h" +#include "IO/ImageExporter.h" +#include <QtTest> +#include <malloc.h> + +// The Smear's buffers, starved of memory mid-stroke. +namespace { +WarpStroke warp(const QImage &image, BlurToolMode mode, const BrushSettings &settings) +{ + const ImageLayer layer(ImportedImage(image, image, QStringLiteral("Picture")), QPointF(0, 0)); + return WarpStroke(layer, image, layer.transform, QSizeF(image.size()), mode, settings); +} + +QImage stripes(int side) +{ + QImage image = BrushRaster::context(side, side, false); + for (int x = 0; x < side; ++x) + QPainter(&image).fillRect(x, 0, 1, side, x % 2 ? Qt::red : Qt::blue); + return image; +} +} + +class WarpFailureTests : public QObject { + Q_OBJECT +private slots: + // Large asks map memory, so the address limit counts them. + void initTestCase() { mallopt(M_MMAP_THRESHOLD, 64 * 1024); } + void aLiquifyDabWithoutScratchIsSkipped(); + void smudgeWithoutRoomToCarryIsARenderError(); + void liquifyWithoutItsOffsetsIsARenderError(); +}; + +void WarpFailureTests::aLiquifyDabWithoutScratchIsSkipped() +{ + const QImage image = stripes(2400); + WarpStroke stroke = warp(image, BlurToolMode::liquify, brush(2000, 0.5, 0, 0, 0, 1)); + stroke.append(QPointF(1200, 1200)); + { + // The dab's scratch, about 33 MB, cannot be had. + const AddressSpaceLimit limit(8ll * 1024 * 1024); + QTest::ignoreMessage(QtWarningMsg, QRegularExpression("a Liquify dab finds no memory")); + stroke.append(QPointF(1250, 1200)); + } + QCOMPARE(stroke.image(), image); + // With memory back the next dab pushes. + stroke.append(QPointF(1300, 1200)); + QVERIFY(stroke.image() != image); +} + +void WarpFailureTests::smudgeWithoutRoomToCarryIsARenderError() +{ + const QImage image = stripes(2400); + WarpStroke stroke = warp(image, BlurToolMode::smudge, brush(2000, 0.5, 0, 0, 0, 1)); + { + // The carried colour, about 64 MB, cannot be had. + const AddressSpaceLimit limit(8ll * 1024 * 1024); + try { + stroke.append(QPointF(1200, 1200)); + QFAIL("the carried colour was had"); + } catch (const ExportError &error) { + QCOMPARE(error.kind, ExportError::Kind::render); + } + } + QCOMPARE(stroke.image(), image); +} + +void WarpFailureTests::liquifyWithoutItsOffsetsIsARenderError() +{ + // Room for two 36 MB copies, not the offsets. + const QImage image = stripes(3000); + { + const AddressSpaceLimit limit(90ll * 1024 * 1024); + try { + warp(image, BlurToolMode::liquify, brush(100, 0.5, 0, 0, 0, 1)); + QFAIL("the offsets were had"); + } catch (const ExportError &error) { + QCOMPARE(error.kind, ExportError::Kind::render); + } + } + // Smudge needs neither. + { + const AddressSpaceLimit limit(52ll * 1024 * 1024); + QCOMPARE(warp(image, BlurToolMode::smudge, brush(100, 0.5, 0, 0, 0, 1)).image(), image); + } +} + +QTEST_GUILESS_MAIN(WarpFailureTests) +#include "WarpFailureTests.moc" diff --git a/tests/WarpStrokeTests.cpp b/tests/WarpStrokeTests.cpp index 8765a7f..f617adf 100644 --- a/tests/WarpStrokeTests.cpp +++ b/tests/WarpStrokeTests.cpp @@ -111,6 +111,18 @@ void WarpStrokeTests::aCommitKeepsATwentiethAndTheLastPoint() QCOMPARE(kept[6], stroke.points()[18]); // Two and a half from the previous, kept as last. QCOMPARE(kept.back(), QPointF(51, 10)); + // Exactly the spacing apart is kept: a 40 brush. + WarpStroke even = warp(image, BlurToolMode::liquify, brush(40, 0.5, 0, 0, 0, 1)); + even.append(QPointF(0, 10)); + even.append(QPointF(10, 10)); + QCOMPARE(even.committedPoints(), (std::vector<QPointF>{{1, 10}, {3, 10}, {5, 10}, {7, 10}, {9, 10}, {10, 10}})); + // A small brush still keeps points a pixel apart. + WarpStroke small = warp(image, BlurToolMode::liquify, brush(10, 0.5, 0, 0, 0, 1)); + small.append(QPointF(0, 10)); + small.append(QPointF(1.5, 10)); + small.append(QPointF(3, 10)); + QCOMPARE(small.points().size(), size_t(4)); + QCOMPARE(small.committedPoints(), (std::vector<QPointF>{{0.75, 10}, {2.25, 10}, {3, 10}})); } QTEST_GUILESS_MAIN(WarpStrokeTests)