14.19: Filter › Scanlines, its own filter, on Swift's cdde327 kernel

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 10:46:16 -04:00
1 parent d06fb55bc4
commit 1a129b9a23
26 files changed
+905 -479

No files matched your search

+2 -1
View File
@@ -177,7 +177,7 @@ Platform adaptations, each forced by a missing Qt analog:
- 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 (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; `aHugeLayersSampleIsMadeLine truncated
- 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 gradieLine truncated
- 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; TwLine truncated
- Quit mid-edit and Scanlines (13.17, Swift 1.4.4; since 14.19 Scanlines is a filter of its own, below). 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 raLine truncated
- 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 Line truncated
- 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 fieLine truncated
- 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 Line truncated
@@ -198,6 +198,7 @@ Platform adaptations, each forced by a missing Qt analog:
- Camera Raw's Shadows and Highlights (14.16, Swift 1.4.8: `0b4c56c`): `kernels/AdjustPixels.c` is 0b4c56c's, unchanged: each follows a bump, u(1 − 2u)², over its half of the tones (Shadows from black, Highlights from white), two and a half times the bump to lift Shadows or pull Highlights down and once the other way, so tones never swap, and black, middle gray and white stay. Tests: `CameraRawToneOrderTests` (Swift's ramp kept in order at ±100 and the dark tones lifted; every level of a gray ramp against the curve written apart, each way at full and partial amounts and both together, Highlights first).
- The Layers panel's gray (14.17, Swift 1.4.8: `06313a9`): `layersLookEditable` is Swift's: `canEditLayers` without what lasts only while the mouse is down (a stroke, a warp, a drag's transform edit, a pixel move) or a moment (`isProjectBusy`, read as `showsBusy`, which shows after 250 ms). The list's rows and their eyes (an effect's eye keeps `canEditLayers`, as Swift makes each effect row anew with it), the footer's buttons (new layer, folder, adjustments, delete with an active layer), the effects menu (with a layer that has pixels) and the mask button (one layer selected, without a mask) follow it, so nothing flashes gray for a click; every action still asks the session's own gate, which refuses meanwhile. The effects menu's folder term is left out as dead: a folder never holds pixels, so the pixel term refuses it. Tests: `LayersLookEditableTests` (each moment looks editable while the gate stays shut, each lasting state grays, the footer and list mid-stroke and under a pending transform, the effects menu's and mask button's own terms); the busy cases in `BlendModePickerTests`, `LayersPanelTests`, `NativeLayerListTests` and `LayerEffectRowTests` now wait for the indicator, and a click before it changes nothing.
- Export As (14.18, Swift 1.4.8: `9287cf1`, `450af5f`, `b1db93d`): `ProjectController::exportAs(start, done)` replaces `exportJPEG`; `ExportFormat` (png, jpeg, pdf, Swift's raw values through `rawValue`, `allExportFormats`) starts the sheet, else the last format exported this run (`m_lastExportFormat`, PNG at first), and the File menu's Export JPEG… opens it on JPEG; Export As… (Ctrl+Alt+Shift+W, a new shortcut definition) on the last. The flow is the JPEG one: the render in a worker, the sheet, then the format's panel (Export PNG, Export JPEG or Export PDF, suggesting `.png`, `.jpeg` or `.pdf`), the write in a worker, one alert titled "Couldn’t export" for a failure past the render. `UI/ExportAsSheet` (Swift's sheet, renamed from the JPEG one): titled Export As; the format as three checkable buttons; the size as two `PickerField`s keeping the proportions (a number applies on Return or leaving, else both fields show the size again; a scrub or menu replaces their typing) and a scale menu of 25, 50, 75, 100 and 200%, every scale held between a hundredth and the side and surface limits; Quality for JPEG alone (saved only from a JPEG export), the page size in inches at the document's DPI for PDF alone, the background swatch ("Export Background") for JPEG and PDF; "Transparency kept · sRGB" for PNG, else "sRGB"; a change of format or size asks again only when the settings change, as Swift's `task(id:)`. The encoding resizes first (`ImageExporter::resized`: smooth, the same raster at the same size, `tooLarge` past the limits) and encodes by format (`pngData`; `jpeg`; `pdfData` over `flattened` for the preview). `JPEGPreview` takes the export's size beside its image (`setImage(image, pixels, updating)`), so Fit and 100% follow the chosen size, and draws Swift 1.4.8's checkerboard (20-point tiles of grays 0.30 and 0.35, anchored at the image) behind what it shows. Swift's PDF comes from `CGContext`'s PDF writer; `QPdfWriter` rounds a page to whole points, so `ImageExporter::pdfData` writes PDF 1.4 by hand: one page whose MediaBox is the print size in points (72 / resolution a pixel, 72 when none), the background as a filled rectangle when given, the pixels as a Flate RGB image with a gray soft mask, straight colour (adaptation; checked against poppler's `pdfinfo` and `pdftoppm`). A Flate stream that `qCompress` cannot make (it returns nothing when memory runs out) is `ExportError(render)`, never an empty stream. A zoom set before the first image (Ctrl+1 while the sheet still encodes) centres that image when it lands, as Swift's preview, made with its first result, centres on appearing. The page's inches round as Swift's formatter: its shortest digits, halves to even (45 pixels at 72 DPI are 0.62 in). A size field reads Swift's `Int` field (`typedInteger`, checked against Swift 6.0.3): a sign, decimal digits of any script read by code point, a fraction and an exponent (its leading zeros dropped), the number truncated toward zero and exact within Int64 (`1e3` is 1000, `12.7` is 12, `-9.223372036854775808e18` is Int64's least, `1e-400` is 0, `128e` is 128), grouping refused; known difference: ICU's lenient parse also reads a leading number before other text (`7abc` is 7, `1,000` is 1, `--5` is −5), where the twin puts the size back; and ICU rounds some long fractions before Swift truncates them (`199.99999999999999` is 200, `0.99999999999999999` is 1, while `9007199254740993.5` stays …993 and `12.99999999999999` stays 12), a rule the measured cases do not pin down, so the twin truncates the digits as typed. Encoding finds no memory as `ExportError(render)` at `encode`'s door, so Export As's PNG worker never meets a `bad_alloc`. Equivalent mutant: the parser's `point <= 0` as `< 0` (a point at zero leaves no digits, which read as 0). Tests: `ExportPDFTests` (the page, its size, the straight pixels and alpha, rows and channels, the background, a refusal, resizing, flattening), `ExportPDFFailureTests` (a binary of its own: a 360-pixel PDF, small enough that Qt converts it on the calling thread (a pool thread that cannot start hangs Qt 6.4's conversion), under limits from 64 KB to 4 MB throws or inflates whole, both reached, and a PNG likewise throws or matches the unstarved file), `ExportAsSheetTests` (formats and their rows and bytes, the size and its limits, the checkerboard, the page against Swift 6.0.3's formatter), `JPEGPreviewTests::aZoomBeforeTheFirstImageCentresIt`, `ProjectExportTests` (each format through the controller, the menu's Export As on the last format), the entry in `CompositorMenusTests` and `KeyboardShortcutsTests` (its chord, group and place). Equivalent mutants: none found.
- Filter › Scanlines (14.19, Swift 1.4.9: `06c3a22`, `1187736`, `cdde327`): Scanlines leaves Dither for a filter of its own. `kernels/DitherPixels.c/.h` are `cdde327`'s, with 13.17's adaptation carried on: Swift's GCD blocks become static band functions with context structs (`scanlines_tone_band`, `line_scan_band`, `blur_band`, `spread_band`, `column_band`, `split_band`), each failure flag an `atomic_int`, every band through `dither_bands`; the arithmetic is Swift's word for word, and `dither_glow`'s new easing near white came with it. `ScanlinesTests::theKernelDrawsSwiftsOwnBytes` holds that: Swift's own file, built unchanged with clang's blocks and libdispatch in the `swift:6.0.3` image, drew five scenes (37 by 53 and 70 by 300, every setting, both directions of Displace, 32 bands of columns, lines and rows) and a glow, and the twin's bytes hash the same (FNV-1a). `Document/Scanlines.h/.cpp` twin Swift's file: `ScanlinesSettings` with Swift's fields, defaults (glow 0 now, thickness 70, smoothness 50) and clamps, Line Spacing and Color Split rounded, `apply` handing the kernel the normalized settings and Two Colors' bytes (black on white otherwise), then 13.17's `glowing` moved here unchanged. `DitherStyle` loses `scanlines` (`usesPixelSize` is ASCII alone, `drawsMarks` every style without tones) and `DitherSettings` its four fields. `FilterKind::scanlines` ("Scanlines") sits after Dither in Swift's order, so the Filter menu lists it there; it previews whole, as Dither does. Its two colours reuse Dither's picker (`openDitherColorPicker`, `setDitherColor`, Swift's), choosing the edit's own settings, and the titles drop "Dither" (`Color Picker (Light Color)`, `(Dark Color)`). The sheet's `scanlines` (`UI/FilterSheet+Scanlines.cpp`) is Swift's `scanlinesControls`: twelve rows with Swift's ranges, units and help, then the Colors menu and swatches, which `colorChoices` builds for Dither too (the screen's and the lines' colour help for Scanlines). Equivalent mutant: `glowing` run at glow 0 (it adds nothing). Tests: `ScanlinesTests` (Swift's eight, Swift's own bytes, each setting reaching its field, every clamp, clear pixels, the eased glow against a full-size blur, the menu place and a commit, the sheet's rows, ranges and fields, both pickers), the order in `FilterModelTests`, `FinishingFilterTests`, `DitherSheetTests` and `CompositorMenusTests`, the acceptance run's Scanlines row through the menu.
- 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 <title>"), 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.