13.19d: 2190a1b verified; sizeLabel's ties tested; tests read optionals through value(); two mask tests brought to 13.12
This commit is contained in:
1 parent
ab34fd6ace
commit
3757eb8910
40 files changed
+233
-220
No files matched your search
@@ -89,7 +89,7 @@ Platform adaptations, each forced by a missing Qt analog:
|
||||
- Distro checks (11.4): `scripts/distro-check.sh arch|fedora|fedora44|nixos|resolute` builds `scripts/distro/<name>/Dockerfile` (for `resolute`, `Dockerfile.dev` on Ubuntu 26.04; `fedora` is 43 since 11.4d, `fedora44` the same file on 44 since 12.16) and runs dev.sh's configure, build and full test run in it, warnings as errors, as uid 1000 (root ignores the read-only folders and unreadable files the tests make), the tree mounted read-only and `HOME` and `TMPDIR` on a tmpfs (a sync on the container's overlay waits on the host's disk); NixOS builds the flake's package (nixpkgs pinned by the committed `flake.lock`) as root, since Nix writes its store, then runs the tests in its `devShells.default` as uid 1000 (`chroot --userspec`, with `HOME` and `TMPDIR` put back, since `nix develop` points `TMPDIR` at a folder root made); the shell sets the plugin path, the model's data path with shared-mime-info's beside it (a NixOS system has the database in its profile through `xdg.mime.enable`; without it Qt knows no camera RAW type, missed until 12.16) and a fontconfig file of DejaVu with fontconfig's own rules from the store, since NixOS keeps them in `/etc/fonts/conf.d`, which a container lacks. Every image carries DejaVu Sans, Serif and Mono, the faces the tests name (Fedora splits them into three packages). Ubuntu 24.04 is the dev image itself. What newer Qt and GCC asked for: `QImage::flipped` from Qt 6.9 where 6.4 has `mirrored`; `Q_OBJECT` on every class a test finds with `findChild` (Qt 6.11 refuses one without); `DistortWarp` projects points itself (`project` in `Distort.cpp`), since `QTransform::map` clamps w for points past the horizon from Qt 6.10 (6.10.2 and 6.11.2 probed; 6.4 divides), where Swift divides whatever its sign, and `DistortTests` pins it past the horizon (on 6.4 the two agree, so a mutant there is equivalent); a local pragma for GCC 16's false `-Wmismatched-new-delete` in `DownsampleTests`' replaced operators. Tests follow the systems where only a message or a count differs: libheif 1.23's longer messages, Qt 6.9's TIFF plugin logging libtiff's errors as criticals, fonts named rather than counted, the Type icon's letters in the system's own sans (Fedora's is Noto: the icon sheet compares them only under DejaVu Sans), and `QT_FORCE_STDERR_LOGGING` for the launched app, since a journald-built Qt logs to the journal when stderr is no console. The five starvation tests place their failure by Qt 6.4's own allocations and skip on any other Qt (`placesStarvation` in `AddressSpaceLimit.h`); the limit itself holds there. `ProjectExportFailureTests` opens a panel and an alert once in `initTestCase`, before any limit, as the app's window has before any export: a snapshot that fails opens its alert while the limit holds, and on every system that alert was the process's first font lookup and the panel's first open, costs the app never meets there. In the NixOS shell, which reads fontconfig's whole `conf.d` without a cache, the lookup's failed allocation segfaulted inside fontconfig (`FcHashTableFind`, found under gdb); on the dev image a panel test run alone ran out inside the panel's first open. `theJPEGRenderTakesTheSnapshotWithoutACopy` holds the JPEG render's snapshot to a move under a 2 MB limit, as `thePanelOpensWithoutCopyingTheSnapshot` holds the PNG panel's. The flake leaves out `dev.sh`'s `build/`, which nixpkgs' CMake hook would take for its own build folder, and the `Compositor-Ref` spec, which a `path:` flake would copy whole.
|
||||
- Spot Healing (8.4): `HealPixels.c` compiles in place beside the other kernels. `SpotHealingMode`, `allSpotHealingModes` and `rawValue` (Swift's names) sit in `BrushStroke.h`, and `BrushSettings` gains `healing` and `healingMode`. While the stroke lasts its tiles show Swift's wash, gray 0.12 at 45% through the coverage, never erasing. `heal` takes the painted bounds from the kernel's `heal_coverage_bounds`, draws the source into a region about three spots wider, and hands the kernel the coverage packed row by row, as Swift's gray context holds it (the twin copies each tile's rows where Swift draws each coverage image); the seed is `QRandomGenerator::global()`, Swift's `UInt32.random`. Each touched tile is then its base with the healed region copied over, blended with the base through `PixelAdjust::blend` under a selection, where Swift clips the context. The kernel's failure is `ProjectError(tooLarge)`; pixels or packed coverage that cannot be allocated are `ExportError(render)`, as Swift's contexts. The session's `beginBrush` takes Spot Healing on pixels alone, never on a mask, as Swift's guard; `finishBrushImmediately` heals before the commit, one `Spot Healing` step; `spotHealingMode` is a getter and a setter that notifies; Tab steps the type. `isBrushTool` is Swift's four tools: the circle, the right-drag tip, the brackets, the digits and `BrushControls` serve each, and a press with Clone Stamp or the Smear begins nothing until 8.5 and 8.6 bring them. `ContentView` keeps one bar across the four, as Swift's single `isBrushTool` branch keeps its view; the bar titles itself by tool (Spot Healing, Clone Stamp, Smear, Eraser, Brush), shows Paint and Erase for the Brush alone and the three types for Spot Healing alone, and names opacity Strength for the Smear; the clone's Aligned and Sample arrive with 8.5, the Smear's modes with 8.6. Equivalent mutant: the healed tile starting from its context rather than its base, since the region holds the whole wash and the copy replaces it.
|
||||
- Clone Stamp (8.5): `Document/CloneStamp.h` holds `CloneSettings`, `CloneStamp.cpp` Swift's extension: `setCloneSource` (a point that is no number is refused; a new source restarts the alignment), `cloneStrokeOffset`, `cloneSamplePoint` and `cloneSample`, whose context that cannot be allocated logs on `omaphoto.app` where Swift's `try?` is silent. The session keeps `cloneSource`, `cloneSettings` (a setter that notifies), `cloneOffset` and `parkedBrushTips` by `tipFamily` (Clone Stamp 1, the Smear 2, the rest 0; the two parked tips start at Swift's 40, 0, 1). `selectTool` parks size, hardness and opacity when the family changes and reads the tip it restores with `at`: the session starts in family 0 and leaving a family parks it, so Swift's `if let` always finds one. `beginTransform` and Transform Selection set the Move tool directly, as Swift's do, and park nothing. `beginBrush` takes Clone Stamp on pixels alone, asks for a source first (`brushError`: "Alt-click where Clone Stamp should copy from first."), samples the document as the stroke starts and fixes the offset; Tab toggles the sample; the step is `Clone Stamp`. `BrushStroke::Clone` is Swift's tuple. `publish` makes each tile's view of the sample on the calling thread (`clonePixels`): under a scale or turn QPainter draws it bilinear, Swift's medium quality; a whole translation copies it; a fractional one is drawn at the floor and shifted by `BrushRaster::shiftedByFraction`, since QPainter snaps an unscaled image. `fillClone` then composites it source-over through coverage × opacity × selection, each byte rounded once, where Swift clips the context to the coverage. Swift's `!isMask || isBlur` holds since 8.6: a mask takes a clone only from Blur. The bar shows Aligned (a `QCheckBox` written on `clicked`, so the session's own changes write nothing back), This Layer and All Layers, and "Alt-click to set the source" in `PlaceholderText` while there is no source. On the canvas an Alt-press with Clone Stamp sets the source and paints nothing; Swift's cursor refresh there is dead (Alt is held, so the crosshair stays) and left out. With a source the pointer hides unless Alt or Space is held. `updateBrushCursor` gives the overlay the source crosshair in view points and, between strokes and without Alt, `clonePreview` (the source around the stamped point at screen resolution, at most 1024 pixels, kept under Swift's key; Swift's `.medium` there is overridden by `LayerRenderer`'s own quality and left out) with `cloneTip` (one click painted by the brush engine, kept by size and hardness). `BrushCursorOverlay` gains `marker`, `preview`, `previewOpacity` and `tip`, images compared by `cacheKey` as Swift's `!==`; under an antialiased ellipse clip at the brush's opacity it draws the preview and cuts it to the tip inside `AdjustmentSurface::draw`, Swift's transparency layer, so the tip applies at the drawing's resolution however far a capped preview is enlarged, and draws the crosshair white 3 under black 1 with round caps. A modifier change refreshes the brush cursor, as Swift's monitor does. Equivalent mutants: `displayedTransform` in `cloneSample` and `clonePreview` (it differs from the stored transform only in a transform edit, which leaves Clone Stamp for Move, or a filter edit, which refuses strokes; the preview would show it only with a filter sheet open over Clone Stamp).
|
||||
- The Smear (8.6): `SmudgeLiquify.h/.cpp` hold `BlurToolMode` and `WarpStroke`, Swift's float arithmetic to the letter (the positive guards of `weight` included) on a document-sized copy drawn through the layer's displayed transform; `image()` is that copy itself, where Swift snapshots it after each append, since the twin reads it only between appends. `beginWarp` and `finishWarp` twin the extension: a warp takes pixels alone (`brushError`: "Smudge and Liquify work on a layer's pixels, not its mask."), and the commit paints the copy along the stroke with a hard tip four pixels wider, `replacesWithClone` so clear pixels clear, one step named by the mode. `BlurTool.cpp` holds `blurSample`, in the stroke's own grid since 13.11 and in pieces since 13.16 (see those bullets). Core Image's Gaussian is `PixelAdjust::gaussianBlur`: separable, weights exp(−i²/2σ²) over three sigmas, normalised, in float on the stored premultiplied values (Swift's Core Image context is unmanaged), each byte rounded once, rows in parallel. Where Core Image approximates a large blur, the twin's is exact; it costs 20–110 ms for a 4K document on 32 cores (sigma 1.5 to 30), paid as a Blur stroke begins, since Swift's sample is taken whole. `BrushStroke` gains `isBlur` (a mask takes a clone only from Blur), `replacesWithClone` (a copy by coverage: `fillClone` keeps 1 − coverage of the old pixel) and `editName` (the step's name when set). `clonePixels` resamples every clone, a gray one too, into premultiplied colour, so a sample's edge keeps the share of the pixel the image covers, and `fillClone` composites that alpha into a mask as CoreGraphics draws a gray image's edge: a white mask half a pixel past the canvas stays white. The session keeps `blurMode` and `warpStroke`; `beginBrush` hands Liquify and Smudge to `beginWarp`; `continueBrush`, `cancelBrush` and `finishBrushImmediately` take the warp first; exactly Swift's six places read it: `canEditLayers`, `canUseHistory`, `canStartProjectOperation`, `selectLayer`, `selectTool`, `cycleToolMode` (`selectLayers` and `selectLayerTarget` read the brush stroke alone, as Swift's). Tab steps the Smear's modes. The bar shows the three modes with Swift's help; the status line says "Drag to soften", "Drag to smudge" or "Drag to push pixels" (`hint` takes the Smear's mode). On the canvas the warp's copy stands in for its layer across the canvas, through the layer's mask at 2048, its opacity and blend mode, and a step repaints the view (no dirty rect, as Swift's); drags, releases, Escape, wheels, pinches and the tip drag treat a warp as a stroke. Equivalent mutants: `displayedTransform` in `beginWarp` (painting refuses transform and filter edits), the radius rounded to nearest (pixels past its floor lie outside the brush), `u >= h` for `u > h` (weight 1 either way), a warp without dabs going on to commit (it appends nothing), copy mode weighing by coverage times opacity (a warp commits at full opacity), `fillClone`'s format guard (a clone is always sampled in colour), and the Gaussian's branch-free interior moved by a pixel (it reads one tap weighing under 1% of the centre).
|
||||
- The Smear (8.6): `SmudgeLiquify.h/.cpp` hold `BlurToolMode` and `WarpStroke`, Swift's float arithmetic to the letter (the positive guards of `weight` included) on a document-sized copy drawn through the layer's displayed transform; `image()` is that copy itself, where Swift snapshots it after each append, since the twin reads it only between appends. `beginWarp` and `finishWarp` twin the extension: a warp takes pixels alone (`brushError`: "Smudge and Liquify work on a layer's pixels, not its mask."), and the commit paints the copy along the stroke with a hard tip four pixels wider, `replacesWithClone` so clear pixels clear, one step named by the mode. `BlurTool.cpp` holds `blurSample`, in the stroke's own grid since 13.11 and in pieces since 13.16 (see those bullets). Core Image's Gaussian is `PixelAdjust::gaussianBlur`: separable, weights exp(−i²/2σ²) over three sigmas, normalised, in float on the stored premultiplied values (Swift's Core Image context is unmanaged), each byte rounded once, rows in parallel. Where Core Image approximates a large blur, the twin's is exact. `BrushStroke` gains `isBlur` (a mask takes a clone only from Blur), `replacesWithClone` (a copy by coverage: `fillClone` keeps 1 − coverage of the old pixel) and `editName` (the step's name when set). `clonePixels` resamples every clone, a gray one too, into premultiplied colour, so a sample's edge keeps the share of the pixel the image covers, and `fillClone` composites that alpha into a mask as CoreGraphics draws a gray image's edge: a white mask half a pixel past the canvas stays white. The session keeps `blurMode` and `warpStroke`; `beginBrush` hands Liquify and Smudge to `beginWarp`; `continueBrush`, `cancelBrush` and `finishBrushImmediately` take the warp first; exactly Swift's six places read it: `canEditLayers`, `canUseHistory`, `canStartProjectOperation`, `selectLayer`, `selectTool`, `cycleToolMode` (`selectLayers` and `selectLayerTarget` read the brush stroke alone, as Swift's). Tab steps the Smear's modes. The bar shows the three modes with Swift's help; the status line says "Drag to soften", "Drag to smudge" or "Drag to push pixels" (`hint` takes the Smear's mode). On the canvas the warp's copy stands in for its layer across the canvas, through the layer's mask at 2048, its opacity and blend mode, and a step repaints the view (no dirty rect, as Swift's); drags, releases, Escape, wheels, pinches and the tip drag treat a warp as a stroke. Equivalent mutants: `displayedTransform` in `beginWarp` (painting refuses transform and filter edits), the radius rounded to nearest (pixels past its floor lie outside the brush), `u >= h` for `u > h` (weight 1 either way), a warp without dabs going on to commit (it appends nothing), copy mode weighing by coverage times opacity (a warp commits at full opacity), `fillClone`'s format guard (a clone is always sampled in colour), and the Gaussian's branch-free interior moved by a pixel (it reads one tap weighing under 1% of the centre).
|
||||
- The Gradient (8.7): `Document/Gradient.cpp` twins Swift's extension. `GradientEdit` is a struct in a `std::optional`, where Swift's is a class; its raster is a `std::shared_ptr<BrushStroke>`, as a pixel move's, because the canvas draws it while the commit renders, and the raster's pointer stands for Swift's `gradientEdit === edit` (a new line on the same target keeps the raster, as Swift keeps the edit). `commitGradient` takes a completion callback, posted on every path; `resolveGradient` posts the commit, Swift's `Task`, so a switch of tool, layer or target applies it once the caller returns; `invertPixels` and `beginFilter` commit it first and call themselves again from its callback, as Swift awaits it. `fillGradient` fills the canvas through `paintCanvas` with a `QLinearGradient`, or a `QRadialGradient` centred on the start with the line as its radius; Qt's pad spread is CoreGraphics' `drawsBeforeStartLocation` and `drawsAfterEndLocation`, and two stops interpolate alike in both. `gradientColors` makes a mask's colours gray from the palette's red, where Swift makes `DeviceGray` colours. `undo` first discards a pending gradient and `canUndo` counts one, as Swift's. The palette's model came forward from 8.10 into `ColorPalette.cpp` (`PaletteColor`, `foregroundColor` with `setForegroundColor` through `setBrushSettings`, `backgroundColor`, `canEditPalette`, `paletteColor`, `setPaletteColor`, `swapPaletteColors`, `resetPaletteColors`), and `fillSelection` reads it; the Type bar's share of `setPaletteColor` came with 8.9a, the picker and the swatches came with 8.10a. On the canvas (`EditorCanvas+Gradient.cpp`) a press within ten points of the end, then the start, grabs it, else begins a line; Shift turns the dragged end to a multiple of 45° around the other; Escape cancels and Return or Enter applies, after the selection's keys; losing focus drops the drag. `TransformOverlay` draws the line in place of the handles while one is pending, as Swift's: a radial's dashed rim at the line's length, the line black 3 under white 1, each end a white disc with a black rim, a gray ring and its colour. A pending gradient on a folder's mask clips the folder's layers at once through `liveFolderMaskClip`, as a brush stroke does. `GradientControls` is the tool's `ToolHeaderBar`: the shapes as checkable `QToolButton`s in a `QButtonGroup`, `GradientSwatch` (white, gray checks of four points, the colours, clipped to a rounded rect, the rim unclipped as Swift's `strokeBorder` overlay), the style `QComboBox`, Reverse, the slider in thousandths from 1% and the percent field (a `SelectionAmountField`, whose own clamp stands for the binding's), the Mask note in `PlaceholderText`, and Cancel and Apply while pending. Equivalent mutants: the gray branch of `gradientColors` and of `fillSelection` (a mask's palette is black or white), and the swatch's rim clipped too (3/255 at its corner pixels, measured). A press that marks a drag without an edit was equivalent until 8.10b's Alt eyedropper read the drag; a test holds it now.
|
||||
- The Shape tool (8.8): `Document/ShapeTool.h/.cpp` twin Swift's file, `ShapeTool+Session.h` its session half. `ShapeKind`'s raw values, `shapeKind(text)` for the manifest and `path` stand for the enum and its `path(in:cornerRadius:)`; `LayerShapeStyle`, `LayerShape` and `ShapeDraft` keep Swift's fields, `ShapePreview` is Swift's tuple. A `LayerShape` holds its image's `ImageIdentity`, Swift's `===`, since `ImportedImage::image()` is never cached in a model type; `ImageLayer.shape` joins equality, and `liveShape()` compares identities, so any other change to the pixels leaves a shape that is no longer live. `addPixelLayer` takes the style and ties it to the asset it makes, where Swift passes a `LayerShape` made from the same image. `shapeKind`, `shapeCornerRadius` and `shapeLineWidth` are getters and setters that notify, `shapeDraft` a getter. `finishShape` truncates each side before multiplying, as Swift's `Int()`, in doubles; a Line click leaves a dot as wide as the line, since the box grows by the thickness, as in Swift. `strokedLine` gives a line's round-capped outline, filled on the layer and on the draft, because QPainter draws a pen a pixel wide through its thin-line path, whose diagonals cover less than CoreGraphics' stroke; a zero-length line is a disc, since CoreGraphics caps it as a dot (PDF's rule for round caps) where Qt draws nothing. `redrawShape` pins any mask where it shows (Swift's `placement == nil` test changes nothing: a placed mask's `maskTransform` is its placement) and logs on `omaphoto.app` where Swift's `try?` is silent; `shapeTransformPreview` is `const` with a mutable cache, reads the pixel size from the asset a live shape shares, and logs on `omaphoto.rendering`. Left out as dead or redundant: `path`'s `max(0, …)` (its `radius > 0` test sends a negative radius to the plain rectangle), `shapeLineEnds`'s kind test and the draft drawing's `guard let ends` (only a line's drag sets an end, and its box with it), `finishShape`'s zero-width fallback in `unit` (the box is a pixel wide by then), `redrawShape`'s `max(1, …)` (a valid transform is a pixel wide), and the canvas drag's `lastDragPoint == nil` (a pan never begins a draft). Hooks: `selectTool` cancels a draft, Tab steps the kind, `commitTransform` redraws each moved shape, the record carries a live shape's style (`hierarchyRecord`), `installProject` loads shapes and `applyDocumentSize` does not, as Swift's rebuild leaves them out after a crop or a resize; where Swift rebuilds a layer (a brush or raster commit, Invert, a filter, a floating merge) the twin drops the shape in place; the canvas resizer passes it on, the image resizer not; a duplicate or a copied layer keeps it. The manifest's `shape` key is Swift's `Codable`: the kind's raw value, the numbers, `lineWidth`, `start` and `end` only when set, points as two numbers; a wrong type or a missing key refuses the project. On the canvas (`EditorCanvas+Shape.cpp`) a press begins, a move drags (Shift squares or snaps a line, Alt grows from the centre), the release finishes, Escape cancels after the selection's keys, Shift-U steps the kind with the Shape tool and chooses it otherwise, losing focus cancels. The draft draws above the active layer, inside the live mask renderer's `drawOwn`, composed aside through `LayerRenderer::composite` so folder masks clip it; a line at its width times the zoom, a point at least, with round caps. Swift's draw reads the draft under AppKit's observation tracking; the twin's `synchronizeDisplay` compares `ShapeShown` (draft, colour, width, the layer it sits above) and redraws the view as that observation does, where the brush repaints its stroke's rect alone; `DisplayState` counts a blank layer while a draft sits on it, so moving or hiding the target redraws too. A rounded rectangle resized with the Move tool draws `shapeTransformPreview` in place of its pixels. `ShapeControls` is the tool's `ToolHeaderBar`: the kinds as checkable `QToolButton`s in a `QButtonGroup` (a choice drops the draft first), Width for lines (a slider of 1–100 showing at most 100, a `SelectionAmountField` of 1–5000, "px") and Radius for rectangles (0–200 and 0–5000, with Swift's help); the sliders show the session's number without writing it back; the Fill swatch came with 8.10a. The status hint follows the kind. Known difference: a Line layer's box follows the dragged point, so it can sit between pixels, and at 1:1 and in exports QPainter draws it on whole pixels, where CoreGraphics interpolates. Equivalent mutants: Qt's `addRoundedRect` adds a plain rectangle at radius 0, so `radius > 0` and `>= 0` agree; an undragged draft's box is its anchor, so `finishShape`'s `value_or` may take either; the draft drawing's empty-box test for a rectangle or ellipse only spares an empty surface, since a path of no area paints nothing.
|
||||
- The Type tool (8.9a): `Document/TypeTool.h/.cpp` twin Swift's file, `TypeTool+Session.h` its session half. `TextAlignment`'s raw values and `textAlignment(text)` serve the manifest; `LayerTextStyle`, `LayerText` and `TextDraft` keep Swift's fields. A `LayerText` holds its image's `ImageIdentity`, as a `LayerShape` does; `ImageLayer.text` joins equality and `liveText()` compares identities. TextKit has no Linux twin, so `Rendering/TextLayout` is Linux-only: `TextLines` lays a style out with `QTextLayout` as `textBoxSize` and `textImage` lay it out with TextKit. Lines have a fixed `lineHeight` with the baseline at their bottom, so extra leading sits above the letters (Swift's equal minimum and maximum line heights); they wrap at word boundaries, a word longer than the line anywhere (`byWordWrapping`); a line that would pass the container's bottom is not laid, nor anything after it (`overflows`); a final newline adds no line to a measure (its line holds the caret alone, 8.9b), and an empty paragraph lays one, the caret's room. Paragraphs part at Cocoa's separators, LF, CR, CRLF and U+2029, and Qt breaks a line at U+2028 by itself. Tabs stop every 28 points, as `NSParagraphStyle`'s twelve default stops do up to 336. Past the twelfth Cocoa has no stop and moves a tab down a line (a line of tabs wraps after 336 points in an `NSTextView`); here the stops go on every 28 points (known difference): moving such a tab down would lay the rest of its paragraph again for each one, quadratic in a run of tabs, and measure bidi lines by hand, while `QTextLine::setNumColumns` leaves the tab hanging on the line above. Faces go by PostScript name, as macOS names them: fontconfig lists each installed face's `FC_POSTSCRIPT_NAME` with its family and style (`availableFonts`, the bar's list). A name it does not list is Qt's match for that family, where Swift falls back to the system font; fontconfig's aliases put Helvetica, the default, on a metric twin where one is installed. The size is the style's in pixels, as a CoreGraphics context counts points, rounded: Qt draws a face in whole pixels (`QFontPrivate::engineForScript` rounds a point size's pixels too), so a fractional size draws at the nearest whole pixel where CoreText draws it exactly (known difference; the default is whole, and the bar's steps keep a whole size whole). Kerning is off, since Swift always sets `.kern`; tracking is absolute letter spacing after every letter, the last included, as CoreText kerns; hinting is off, so Qt lays out design advances by itself (`QFontEngineFT::shouldUseDesignMetrics`) and the layout sets no flag for them. Qt 6.4 scales absolute letter spacing by the device's dpi over its own in whole numbers, so on a 72-dpi image it vanishes (probed): lines are laid out without a device, where that scale is one. Letters differ from macOS's where the faces differ; the rules are the same. `Dockerfile.dev` installs `libfontconfig-dev`; CMake links `Fontconfig::Fontconfig`. In the session `textDraft` has a public setter, as Swift writes it freely, and `textDefaults` a getter. `applyText` takes its draft by value, as Swift's struct is passed: `finishText` hands it the session's own draft, which it clears. It clears the draft first (the gates read it) and puts it back on refusal, silently, and before an error is announced, so the error's signal sees the draft; blank new text and an unchanged edit close the draft, announced, with no step and no canvas focus request. `layerName` keeps 40 graphemes (`QTextBoundaryFinder`), Swift's `prefix(40)` of Characters; `simplified()` makes whitespace runs one space. `recolorText` logs a failed redraw where Swift's `try?` is silent. Left out as dead: the `isFinite` terms of `isValid` and `boxIsValid` (the ranges refuse NaN and infinities); the draft and document guards of `beginText`, `editActiveText` and `recolorText` (`canEditLayers` reads both); `textBoxSize`'s `max(16, …)` on both sides (the padding alone makes 24) and its caret line (an empty paragraph lays a line); `textImage`'s `width >= 1` terms; `recolorText`'s validity guard (`textImage` refuses an invalid style, and the refusal is logged); `setPaletteColor`'s Type-tool test (a draft lives only under the Type tool: every way in sets the tool, and a switch finishes text first); `fillSelection`'s `liveText` test (`recolorText` asks it); the store rule's folder term (text needs pixels, and `LayerHierarchy::validate` refuses a folder that has them). Hooks: the gates and `canStartProjectOperation` read `textDraft`; `selectLayer`, `selectLayers` and `selectTool` finish text first when the choice changes, and stop when that is refused; the foreground swatch colours an open draft; Fill recolours live text when neither a selection nor a mask is in play; `addPixelLayer` takes the style, as it takes a shape's; `installProject` loads text and `applyDocumentSize` does not; the record carries live text; every rebuild that drops a shape drops text; the canvas resizer passeLine truncated
|
||||
@@ -175,7 +175,7 @@ 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 opacLine truncated
|
||||
- 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 Line truncated
|
||||
- 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 Line 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
|
||||
- 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
|
||||
|
||||
@@ -475,7 +475,11 @@ Reviews for this section ran on codex `gpt-6.1-sol` at `max` (agent `codex-sol`)
|
||||
- [x] 13.19c `63e42ac` (13.8's `upright` pin) built, run, its mutant caught.
|
||||
- 13.19c work: `SharpCanvasTests` passes; `transform.radians() == 0` set to `true` in `LayerRenderer::draw` and in `TiledLayerRenderer`, each caught by the turned rows (dropping `upright` inside `interpolation` leaves the parameter unused, which `-Werror` refuses).
|
||||
- 13.19c review: round 1 PASS.
|
||||
- [ ] 13.19d `2190a1b` (13.10–13.12 tests) built and run; its open items closed: the 12.5b, 13.11 and 8.6 bullets, `sizeLabel`'s tie claim, 13.12's pending pixel gradient.
|
||||
- [x] 13.19d `2190a1b` (13.10–13.12 tests) built and run; its open items closed: the 12.5b, 13.11 and 8.6 bullets, `sizeLabel`'s tie claim, 13.12's pending pixel gradient.
|
||||
- 13.19d work: `MaskAloneTests`, `PaintRefusalTests` and `SelectionCanvasTests` pass. Mutants: the three reachable `paintRefusal` seams swapped, caught (mask off and adjustment exclude each other, an equivalent swap); the Shift axis test without either `abs`, without both, and `>`, caught; `drawMaskAlone` without its `isMask` or its layer term, caught; `setIsMaskSelected` keeping the view, caught. The 12.5b, 8.6 and 13.11 lines match the code. `sizeLabel`'s tie claim was false (1062.5 over 1000 pixels is exactly 106.25%): ties to even are now tested both ways (ICU's default rounding, which Swift's `.formatted` uses), `round` and `floor(x + 0.5)` caught.
|
||||
- 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.
|
||||
- [ ] 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.
|
||||
|
||||
@@ -191,14 +191,14 @@ void BlurNoiseAdjustmentTests::theStoreTakesTheKindsFromVersion9()
|
||||
QCOMPARE(stored(snapshot), std::optional(ProjectError::Kind::invalid));
|
||||
// Invalid settings refuse it at any version.
|
||||
snapshot.manifest.version = 9;
|
||||
snapshot.manifest.layers[1].adjustment->setGaussianRadius(300);
|
||||
snapshot.manifest.layers[1].adjustment.value().setGaussianRadius(300);
|
||||
QCOMPARE(stored(snapshot), std::optional(ProjectError::Kind::invalid));
|
||||
}
|
||||
// The rule reads the kind: older kinds may carry keys.
|
||||
ProjectSnapshot older = twoLayers();
|
||||
older.manifest.version = 7;
|
||||
older.manifest.layers[1].adjustment = LayerAdjustment{AdjustmentKind::levels};
|
||||
older.manifest.layers[1].adjustment->setGaussianRadius(5);
|
||||
older.manifest.layers[1].adjustment.value().setGaussianRadius(5);
|
||||
QCOMPARE(stored(older), std::nullopt);
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ void BlurNoiseAdjustmentTests::eachNoiseLayerRollsItsOwnSeed()
|
||||
session.setAdjustmentEditingID(std::nullopt);
|
||||
session.addAdjustment(AdjustmentKind::addNoise);
|
||||
const LayerAdjustment second = session.activeLayer().value().adjustment.value();
|
||||
QVERIFY(first.noiseSeed && second.noiseSeed && *first.noiseSeed != *second.noiseSeed);
|
||||
QVERIFY(first.noiseSeed && second.noiseSeed && first.noiseSeed.value() != second.noiseSeed.value());
|
||||
// Only noise rolls one; the rest keep their defaults.
|
||||
session.setAdjustmentEditingID(std::nullopt);
|
||||
session.addAdjustment(AdjustmentKind::gaussianBlur);
|
||||
|
||||
@@ -28,11 +28,11 @@ inline QImage preview(const BrushStroke &stroke, QSizeF canvas)
|
||||
QPainter painter(&context);
|
||||
const ImageLayer &layer = stroke.layer;
|
||||
const std::optional<ImportedImage> &asset = layer.asset;
|
||||
const QImage image = asset && !asset->raster ? asset->image() : QImage();
|
||||
const QImage image = asset && !asset.value().raster ? asset.value().image() : QImage();
|
||||
LayerRenderer::drawBrushPreview(image, stroke.paintTransform, stroke.paintTransform.center(), painter,
|
||||
{.opacity = layer.opacity, .blendMode = layer.blendMode, .mask = layer.mask ? layer.mask->enabledImage().value_or(QImage()) : QImage()},
|
||||
{.opacity = layer.opacity, .blendMode = layer.blendMode, .mask = layer.mask ? layer.mask.value().enabledImage().value_or(QImage()) : QImage()},
|
||||
{.patches = stroke.patches(), .pixelWidth = stroke.width, .pixelHeight = stroke.height, .paintingMask = stroke.isMask,
|
||||
.sourceRect = stroke.sourceRect, .raster = asset ? asset->raster : nullptr});
|
||||
.sourceRect = stroke.sourceRect, .raster = asset ? asset.value().raster : nullptr});
|
||||
painter.end();
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ void CanvasEditContractTests::aGradientThatCannotFillCancels()
|
||||
scene.session.selectTool(NavigationTool::gradient);
|
||||
scene.session.beginGradient(QPointF(10, 30));
|
||||
QVERIFY(scene.session.gradientEdit());
|
||||
scene.session.gradientEdit()->raster->pixelLimit = 100;
|
||||
scene.session.gradientEdit().value().raster->pixelLimit = 100;
|
||||
scene.session.moveGradient(std::nullopt, QPointF(70, 30));
|
||||
QVERIFY(!scene.session.gradientEdit() && scene.session.brushError().has_value());
|
||||
QCOMPARE(scene.session.history.undoCount(), count);
|
||||
|
||||
@@ -19,7 +19,7 @@ struct Shown {
|
||||
explicit Shown(std::optional<QSize> document = QSize(100, 100), QSize size = QSize(400, 300)) : canvas(new CanvasView(session, &window))
|
||||
{
|
||||
if (document)
|
||||
session.createDocument(document->width(), document->height());
|
||||
session.createDocument(document.value().width(), document.value().height());
|
||||
window.resize(size);
|
||||
canvas->setGeometry(QRect(QPoint(0, 0), size));
|
||||
window.show();
|
||||
|
||||
@@ -253,8 +253,8 @@ void EffectsCanvasTests::aSurfaceIsMadeAnewForOtherSettings()
|
||||
QVERIFY(session.finishBrushImmediately());
|
||||
// A canvas alone follows nothing: the old surface stays.
|
||||
LayerEffects cyan = outline(2);
|
||||
cyan.stroke->red = 0;
|
||||
cyan.stroke->blue = 1;
|
||||
cyan.stroke.value().red = 0;
|
||||
cyan.stroke.value().blue = 1;
|
||||
session.setEffects(cyan);
|
||||
session.beginBrush(QPointF(45, 32));
|
||||
session.continueBrush(QPointF(55, 32));
|
||||
|
||||
+23
-23
@@ -146,56 +146,56 @@ void EffectsPanelTests::theSheetEditsItsEffect()
|
||||
auto &slider = find<QSlider>(sheet, "sizeSlider");
|
||||
QCOMPARE(size.text(), QString("4"));
|
||||
type(size, "12");
|
||||
QCOMPARE(editor.effects().stroke->size, 12.0);
|
||||
QCOMPARE(editor.effects().stroke.value().size, 12.0);
|
||||
// The slider spans 0 to 20; typing reaches 500.
|
||||
QCOMPARE(slider.value(), 600);
|
||||
// No finite number changes nothing; the field goes back.
|
||||
type(size, "abc");
|
||||
QVERIFY(editor.effects().stroke->size == 12 && size.text() == "12");
|
||||
QVERIFY(editor.effects().stroke.value().size == 12 && size.text() == "12");
|
||||
type(size, "inf");
|
||||
QVERIFY(editor.effects().stroke->size == 12 && size.text() == "12");
|
||||
QVERIFY(editor.effects().stroke.value().size == 12 && size.text() == "12");
|
||||
// Up steps one, Shift ten; past the range, clamped.
|
||||
QTest::keyClick(&size, Qt::Key_Up);
|
||||
QCOMPARE(editor.effects().stroke->size, 13.0);
|
||||
QCOMPARE(editor.effects().stroke.value().size, 13.0);
|
||||
QTest::keyClick(&size, Qt::Key_Up, Qt::ShiftModifier);
|
||||
QCOMPARE(editor.effects().stroke->size, 23.0);
|
||||
QCOMPARE(editor.effects().stroke.value().size, 23.0);
|
||||
// Past its end the thumb rests there, writing nothing back.
|
||||
QCOMPARE(slider.value(), 1000);
|
||||
// A step replaces pending typing, even where it clamps.
|
||||
typing(size, "12");
|
||||
QTest::keyClick(&size, Qt::Key_Up);
|
||||
QVERIFY(editor.effects().stroke->size == 24 && size.text() == "24" && !size.isModified());
|
||||
QVERIFY(editor.effects().stroke.value().size == 24 && size.text() == "24" && !size.isModified());
|
||||
size.clearFocus();
|
||||
QCOMPARE(editor.effects().stroke->size, 24.0);
|
||||
QCOMPARE(editor.effects().stroke.value().size, 24.0);
|
||||
type(size, "900");
|
||||
QVERIFY(editor.effects().stroke->size == 500 && size.text() == "500");
|
||||
QVERIFY(editor.effects().stroke.value().size == 500 && size.text() == "500");
|
||||
typing(size, "12");
|
||||
QTest::keyClick(&size, Qt::Key_Up);
|
||||
QVERIFY(editor.effects().stroke->size == 500 && size.text() == "500" && !size.isModified());
|
||||
QVERIFY(editor.effects().stroke.value().size == 500 && size.text() == "500" && !size.isModified());
|
||||
size.clearFocus();
|
||||
QCOMPARE(editor.effects().stroke->size, 500.0);
|
||||
QCOMPARE(editor.effects().stroke.value().size, 500.0);
|
||||
// The slider sets any value; the field rounds, ties even.
|
||||
slider.setValue(525);
|
||||
QVERIFY(editor.effects().stroke->size == 10.5 && size.text() == "10");
|
||||
QVERIFY(editor.effects().stroke.value().size == 10.5 && size.text() == "10");
|
||||
// Leaving an untouched field keeps the exact value.
|
||||
size.setFocus();
|
||||
size.clearFocus();
|
||||
QCOMPARE(editor.effects().stroke->size, 10.5);
|
||||
QCOMPARE(editor.effects().stroke.value().size, 10.5);
|
||||
// Typing waits while other changes arrive.
|
||||
auto &opacity = find<QLineEdit>(sheet, "opacityField");
|
||||
QCOMPARE(opacity.text(), QString("100"));
|
||||
typing(size, "3");
|
||||
find<QSlider>(sheet, "opacitySlider").setValue(500);
|
||||
QVERIFY(editor.effects().stroke->opacity == 0.5 && opacity.text() == "50" && size.text() == "3");
|
||||
QVERIFY(editor.effects().stroke.value().opacity == 0.5 && opacity.text() == "50" && size.text() == "3");
|
||||
size.clearFocus();
|
||||
QCOMPARE(editor.effects().stroke->size, 3.0);
|
||||
QCOMPARE(editor.effects().stroke.value().size, 3.0);
|
||||
// Inside or Outside, as Swift's segments, following an undo.
|
||||
const QList<QToolButton *> position = find<QWidget>(sheet, "strokePosition").findChildren<QToolButton *>();
|
||||
QVERIFY(position.size() == 2 && position[0]->text() == "Outside" && position[1]->text() == "Inside" && position[0]->isChecked());
|
||||
position[1]->click();
|
||||
QVERIFY(editor.effects().stroke->inside && position[1]->isChecked());
|
||||
QVERIFY(editor.effects().stroke.value().inside && position[1]->isChecked());
|
||||
editor.session.undo();
|
||||
QVERIFY(!editor.effects().stroke->inside && position[0]->isChecked());
|
||||
QVERIFY(!editor.effects().stroke.value().inside && position[0]->isChecked());
|
||||
position[1]->click();
|
||||
// The swatch opens the picker, whose colour shows at once.
|
||||
auto &swatch = find<QAbstractButton>(sheet, "effectColor");
|
||||
@@ -207,7 +207,7 @@ void EffectsPanelTests::theSheetEditsItsEffect()
|
||||
Painted counter(paints);
|
||||
swatch.installEventFilter(&counter);
|
||||
editor.session.setColorPickerHSB(PickerHSB(PaletteColor{1, 0, 0}));
|
||||
QCOMPARE(editor.effects().stroke->color(), (PaletteColor{1, 0, 0}));
|
||||
QCOMPARE(editor.effects().stroke.value().color(), (PaletteColor{1, 0, 0}));
|
||||
QTRY_VERIFY(paints > 0);
|
||||
// A black rim, a white ring inside, then the colour.
|
||||
const QImage shot = swatch.grab().toImage();
|
||||
@@ -218,7 +218,7 @@ void EffectsPanelTests::theSheetEditsItsEffect()
|
||||
// Return commits, then goes on to OK, keeping everything.
|
||||
typing(size, "7");
|
||||
QTest::keyClick(&size, Qt::Key_Return);
|
||||
QVERIFY(!editor.session.effectsEditing() && editor.effects().stroke->size == 7 && editor.effects().stroke->inside);
|
||||
QVERIFY(!editor.session.effectsEditing() && editor.effects().stroke.value().size == 7 && editor.effects().stroke.value().inside);
|
||||
QTRY_VERIFY(!editor.sheet());
|
||||
}
|
||||
|
||||
@@ -236,11 +236,11 @@ void EffectsPanelTests::aWheelOnTheSliderReplacesTheTyping()
|
||||
Qt::NoScrollPhase, false);
|
||||
QApplication::sendEvent(&slider, &wheel);
|
||||
const double wheeled = 20.0 * (200 - QApplication::wheelScrollLines()) / 1000;
|
||||
QCOMPARE(editor.effects().stroke->size, wheeled);
|
||||
QCOMPARE(editor.effects().stroke.value().size, wheeled);
|
||||
QVERIFY(size.hasFocus() && size.text() == QString::number(std::lround(wheeled)) && !size.isModified());
|
||||
// Return then keeps the slider's value, going on to OK.
|
||||
QTest::keyClick(&size, Qt::Key_Return);
|
||||
QVERIFY(!editor.session.effectsEditing() && editor.effects().stroke->size == wheeled);
|
||||
QVERIFY(!editor.session.effectsEditing() && editor.effects().stroke.value().size == wheeled);
|
||||
}
|
||||
|
||||
void EffectsPanelTests::theSheetKeepsSwiftsMeasures()
|
||||
@@ -355,14 +355,14 @@ void EffectsPanelTests::cancelAndEscapePutTheEffectBack()
|
||||
auto &cancel = find<QPushButton>(*editor.sheet(), "effectsCancel");
|
||||
cancel.setFocus();
|
||||
QTest::keyClick(&cancel, Qt::Key_Return);
|
||||
QVERIFY(editor.effects().shadow->angle == -45 && !editor.session.effectsEditing());
|
||||
QVERIFY(editor.effects().shadow.value().angle == -45 && !editor.session.effectsEditing());
|
||||
// An existing shadow: Cancel puts it back as it was.
|
||||
editor.session.selectEffect(LayerEffectKind::shadow, editor.layer, true);
|
||||
QCOMPARE(find<QLineEdit>(*editor.sheet(), "distanceField").text(), QString("20"));
|
||||
find<QSlider>(*editor.sheet(), "distanceSlider").setValue(400);
|
||||
QCOMPARE(editor.effects().shadow->distance, 40.0);
|
||||
QCOMPARE(editor.effects().shadow.value().distance, 40.0);
|
||||
find<QPushButton>(*editor.sheet(), "effectsCancel").click();
|
||||
QVERIFY(editor.effects().shadow->distance == 20 && !editor.session.effectsEditing());
|
||||
QVERIFY(editor.effects().shadow.value().distance == 20 && !editor.session.effectsEditing());
|
||||
// A new overlay's panel: Escape takes the overlay away again.
|
||||
editor.session.addEffect(LayerEffectKind::colorOverlay);
|
||||
QVERIFY(editor.effects().colorOverlay);
|
||||
|
||||
@@ -92,7 +92,7 @@ void EffectsPreviewCacheTests::aPreviewLandsAfterItsDelayAsTheRendererDrawsIt()
|
||||
QCOMPARE(*landings.count, 1);
|
||||
// No effects, or none shown: nothing, and the entry goes.
|
||||
ImageLayer hidden = layer;
|
||||
hidden.effects->stroke->enabled = false;
|
||||
hidden.effects.value().stroke.value().enabled = false;
|
||||
QVERIFY(!ask(cache, hidden, landings) && !cache.rendered(layer.id));
|
||||
QTest::qWait(150);
|
||||
QVERIFY(!cache.rendered(layer.id));
|
||||
@@ -110,7 +110,7 @@ void EffectsPreviewCacheTests::movingTheLayerKeepsThePreviewUnlessAMaskIsPlaced(
|
||||
EffectsPreviewCache cache;
|
||||
ImageLayer layer = layerOf(filled(20, 10, QColor(0, 0, 255)), outline(3));
|
||||
layer.mask = LayerMask(LayerMask::assetFrom(gray(20, 10, 255)));
|
||||
const QImage mask = layer.mask->enabledImage().value();
|
||||
const QImage mask = layer.mask.value().enabledImage().value();
|
||||
const Landings landings;
|
||||
ask(cache, layer, landings, mask);
|
||||
const qint64 first = landed(cache, layer, landings, 1).image.cacheKey();
|
||||
@@ -149,7 +149,7 @@ void EffectsPreviewCacheTests::settingsKeepTheLastPreviewButNewPixelsDropIt()
|
||||
const EffectsPreviewCache::Result second = landed(cache, layer, landings, 2);
|
||||
QCOMPARE(second.inset, 7.0);
|
||||
// Another kind shown or hidden: the last preview stands.
|
||||
layer.effects->colorOverlay = ColorOverlayEffect();
|
||||
layer.effects.value().colorOverlay = ColorOverlayEffect();
|
||||
QCOMPARE(ask(cache, layer, landings).value().image.cacheKey(), second.image.cacheKey());
|
||||
landed(cache, layer, landings, 3);
|
||||
// New pixels drop it.
|
||||
@@ -158,10 +158,10 @@ void EffectsPreviewCacheTests::settingsKeepTheLastPreviewButNewPixelsDropIt()
|
||||
const qint64 red = landed(cache, layer, landings, 4).image.cacheKey();
|
||||
// A mask added or removed on those pixels keeps it.
|
||||
layer.mask = LayerMask(LayerMask::assetFrom(gray(20, 10, 255)));
|
||||
QCOMPARE(ask(cache, layer, landings, layer.mask->enabledImage()).value().image.cacheKey(), red);
|
||||
QCOMPARE(ask(cache, layer, landings, layer.mask.value().enabledImage()).value().image.cacheKey(), red);
|
||||
QVERIFY(landed(cache, layer, landings, 5).image.cacheKey() != red);
|
||||
// Switched off again, the unmasked effects are known: no render.
|
||||
layer.mask->isEnabled = false;
|
||||
layer.mask.value().isEnabled = false;
|
||||
QCOMPARE(ask(cache, layer, landings).value().image.cacheKey(), red);
|
||||
QTest::qWait(200);
|
||||
QCOMPARE(*landings.count, 5);
|
||||
@@ -325,7 +325,7 @@ void EffectsPreviewCacheTests::preparingDropsLayersWithoutEffectsAndSharesTheBud
|
||||
ask(cache, small, landings);
|
||||
landed(cache, small, landings, 5);
|
||||
ImageLayer off = small;
|
||||
off.effects->stroke->enabled = false;
|
||||
off.effects.value().stroke.value().enabled = false;
|
||||
cache.seed(wide.id, filled(4, 4, Qt::green), placedAt(QPointF(0, 0), QSizeF(4, 4)));
|
||||
cache.prepare({off});
|
||||
QVERIFY(!cache.rendered(small.id) && !cache.rendered(wide.id));
|
||||
@@ -374,7 +374,7 @@ void EffectsPreviewCacheTests::aMaskHidesPixelsInTheReducedPreview()
|
||||
}
|
||||
QVERIFY(low == 125 && high == 129);
|
||||
layer.mask = LayerMask(LayerMask::assetFrom(stripes.scaled(6000, 100)));
|
||||
ask(cache, layer, landings, layer.mask->enabledImage());
|
||||
ask(cache, layer, landings, layer.mask.value().enabledImage());
|
||||
QVERIFY(landed(cache, layer, landings, 4).image == row);
|
||||
}
|
||||
|
||||
@@ -397,13 +397,13 @@ void EffectsPreviewCacheTests::aReducedPreviewScalesEveryLength()
|
||||
// The renderer on the reduced pixels, every length reduced alike.
|
||||
const double factor = 1528.0 / (6000 + 2 * LayerEffectsRenderer::margin(effects));
|
||||
const QImage reduced = image.scaled(1518, 25, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
effects.stroke->size *= factor;
|
||||
effects.shadow->distance *= factor;
|
||||
effects.shadow->blur *= factor;
|
||||
effects.innerShadow->distance *= factor;
|
||||
effects.innerShadow->blur *= factor;
|
||||
effects.outerGlow->size *= factor;
|
||||
effects.innerGlow->size *= factor;
|
||||
effects.stroke.value().size *= factor;
|
||||
effects.shadow.value().distance *= factor;
|
||||
effects.shadow.value().blur *= factor;
|
||||
effects.innerShadow.value().distance *= factor;
|
||||
effects.innerShadow.value().blur *= factor;
|
||||
effects.outerGlow.value().size *= factor;
|
||||
effects.innerGlow.value().size *= factor;
|
||||
QVERIFY(result.image == LayerEffectsRenderer::render(reduced, std::nullopt, effects).image);
|
||||
}
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ void FilterSessionTests::aCoveringMaskStaysPutUnderABlur()
|
||||
session->insert(ImportedImage(image, image, QStringLiteral("Quarter")));
|
||||
const QUuid id = session->activeLayerID().value();
|
||||
session->applySelection(rectPath(QRectF(0, 0, 20, 20)), SelectionMode::replace, "Select");
|
||||
session->addMask(false);
|
||||
session->addMask(true);
|
||||
session->selectLayerTarget(id, false);
|
||||
session->beginFilter(FilterKind::gaussianBlur);
|
||||
QVERIFY(committed(*session));
|
||||
|
||||
@@ -276,11 +276,11 @@ void FinishingFilterTests::aPreviewForAnOlderGridShowsOnThatGrid()
|
||||
int older = 0;
|
||||
QObject::connect(&session, &EditorSession::changed, &session, [&] {
|
||||
const std::optional<FilterEdit> &edit = session.filterEdit();
|
||||
if (!edit || !edit->preparedPreview)
|
||||
if (!edit || !edit.value().preparedPreview)
|
||||
return;
|
||||
if (QSizeF(edit->preparedPreview->size()) != edit->preparedTransform.value().size)
|
||||
if (QSizeF(edit.value().preparedPreview.value().size()) != edit.value().preparedTransform.value().size)
|
||||
mismatched = true;
|
||||
older += edit->preparedPreview->size() != edit->previewSource.size();
|
||||
older += edit.value().preparedPreview.value().size() != edit.value().previewSource.size();
|
||||
});
|
||||
session.updateFilter(FilterSettings{.bloomRadius = 5}, true);
|
||||
const LayerTransform grid = session.filterEdit().value().grownTransform.value();
|
||||
@@ -289,7 +289,7 @@ void FinishingFilterTests::aPreviewForAnOlderGridShowsOnThatGrid()
|
||||
QVERIFY(session.filterEdit().value().grownTransform.value().size.width() > grid.size.width());
|
||||
QTRY_VERIFY(session.filterEdit().value().preparedPreview && !session.filterEdit().value().preparing);
|
||||
QVERIFY(!mismatched && older > 0);
|
||||
QCOMPARE(session.filterEdit().value().preparedPreview->size(), session.filterEdit().value().previewSource.size());
|
||||
QCOMPARE(session.filterEdit().value().preparedPreview.value().size(), session.filterEdit().value().previewSource.size());
|
||||
QCOMPARE(session.filterEdit().value().preparedTransform, session.filterEdit().value().grownTransform);
|
||||
// Preview off: nothing waits; the stale render ends it.
|
||||
session.updateFilter(FilterSettings{.bloomRadius = 5}, true);
|
||||
@@ -313,7 +313,7 @@ void FinishingFilterTests::aJobWaitingBehindAnotherEditShowsOnItsGrid()
|
||||
bool mismatched = false, armed = true;
|
||||
QObject::connect(&session, &EditorSession::changed, &session, [&] {
|
||||
const std::optional<FilterEdit> &edit = session.filterEdit();
|
||||
if (edit && edit->preparedPreview && QSizeF(edit->preparedPreview->size()) != edit->preparedTransform.value().size)
|
||||
if (edit && edit.value().preparedPreview && QSizeF(edit.value().preparedPreview.value().size()) != edit.value().preparedTransform.value().size)
|
||||
mismatched = true;
|
||||
// The waiting job starts: Preview comes back next turn.
|
||||
if (std::exchange(armed, false))
|
||||
@@ -322,7 +322,7 @@ void FinishingFilterTests::aJobWaitingBehindAnotherEditShowsOnItsGrid()
|
||||
QTRY_VERIFY(!armed);
|
||||
QTRY_VERIFY(session.filterEdit().value().preparedPreview && !session.filterEdit().value().preparing);
|
||||
QVERIFY(!mismatched);
|
||||
QCOMPARE(session.filterEdit().value().preparedPreview->size(), session.filterEdit().value().previewSource.size());
|
||||
QCOMPARE(session.filterEdit().value().preparedPreview.value().size(), session.filterEdit().value().previewSource.size());
|
||||
}
|
||||
|
||||
void FinishingFilterTests::theVignettePickerPreviewsAndPutsBack()
|
||||
|
||||
+23
-23
@@ -48,8 +48,8 @@ void HistoryTests::undoAndRedoReturnTheRecordedSnapshots()
|
||||
DocumentHistory history;
|
||||
const std::optional<CanvasDocument> before = documentNamed("Before");
|
||||
std::optional<CanvasDocument> after = before;
|
||||
after->layers[0].name = "After";
|
||||
const QUuid selection = before->layers[0].id;
|
||||
after.value().layers[0].name = "After";
|
||||
const QUuid selection = before.value().layers[0].id;
|
||||
history.begin("Rename Layer", before, std::nullopt);
|
||||
history.end(after, selection);
|
||||
QCOMPARE(history.undoCount(), 1);
|
||||
@@ -96,9 +96,9 @@ void HistoryTests::nestedTransactionsRecordOneEntry()
|
||||
history.redo();
|
||||
history.begin("Layer Setup", start, std::nullopt);
|
||||
QVERIFY(!history.canUndo());
|
||||
edited->layers[0].name = "B";
|
||||
edited.value().layers[0].name = "B";
|
||||
history.begin("Inner", edited, std::nullopt);
|
||||
edited->layers[0].name = "C";
|
||||
edited.value().layers[0].name = "C";
|
||||
history.end(edited, std::nullopt);
|
||||
QCOMPARE(history.undoCount(), 1);
|
||||
QVERIFY(!history.canUndo());
|
||||
@@ -114,12 +114,12 @@ void HistoryTests::noOpEditsPreserveRedo()
|
||||
DocumentHistory history;
|
||||
const std::optional<CanvasDocument> start = documentNamed("A");
|
||||
std::optional<CanvasDocument> renamed = start;
|
||||
renamed->layers[0].name = "B";
|
||||
renamed.value().layers[0].name = "B";
|
||||
history.begin("Rename Layer", start, std::nullopt);
|
||||
history.end(renamed, std::nullopt);
|
||||
history.undo();
|
||||
history.begin("Rename Layer", start, std::nullopt);
|
||||
history.end(start, start->layers[0].id);
|
||||
history.end(start, start.value().layers[0].id);
|
||||
QCOMPARE(history.undoCount(), 0);
|
||||
QVERIFY(history.canRedo());
|
||||
QVERIFY(!history.isModified());
|
||||
@@ -203,16 +203,16 @@ void HistoryTests::historyBoundsEntriesAndUniqueRetainedPixels()
|
||||
{
|
||||
DocumentHistory history(2, 0);
|
||||
std::optional<CanvasDocument> doc = CanvasDocument(64, 32);
|
||||
doc->layers.push_back(ImageLayer(asset(64, 32, "fixture"), QPointF(0, 0)));
|
||||
doc.value().layers.push_back(ImageLayer(asset(64, 32, "fixture"), QPointF(0, 0)));
|
||||
for (const QString name : {"A", "B", "C"}) {
|
||||
history.begin("Rename", doc, doc->layers[0].id);
|
||||
doc->layers[0].name = name;
|
||||
history.end(doc, doc->layers[0].id);
|
||||
history.begin("Rename", doc, doc.value().layers[0].id);
|
||||
doc.value().layers[0].name = name;
|
||||
history.end(doc, doc.value().layers[0].id);
|
||||
}
|
||||
QCOMPARE(history.undoCount(), 2);
|
||||
QCOMPARE(history.retainedBytes(doc), 0);
|
||||
history.begin("Delete", doc, doc->layers[0].id);
|
||||
doc->layers.clear();
|
||||
history.begin("Delete", doc, doc.value().layers[0].id);
|
||||
doc.value().layers.clear();
|
||||
history.end(doc, std::nullopt);
|
||||
QCOMPARE(history.undoCount(), 0);
|
||||
QCOMPARE(history.retainedBytes(doc), 0);
|
||||
@@ -222,15 +222,15 @@ void HistoryTests::retainedBytesCountEachHistoryOnlyImageOnce()
|
||||
{
|
||||
DocumentHistory history;
|
||||
std::optional<CanvasDocument> doc = CanvasDocument(64, 32);
|
||||
doc->layers.push_back(ImageLayer(asset(10, 10, "old"), QPointF(0, 0)));
|
||||
doc.value().layers.push_back(ImageLayer(asset(10, 10, "old"), QPointF(0, 0)));
|
||||
for (const QString name : {"A", "B"}) {
|
||||
history.begin("Rename", doc, std::nullopt);
|
||||
doc->layers[0].name = name;
|
||||
doc.value().layers[0].name = name;
|
||||
history.end(doc, std::nullopt);
|
||||
}
|
||||
QCOMPARE(history.retainedBytes(doc), 0);
|
||||
history.begin("Replace", doc, std::nullopt);
|
||||
doc->layers[0].asset = asset(20, 10, "new");
|
||||
doc.value().layers[0].asset = asset(20, 10, "new");
|
||||
history.end(doc, std::nullopt);
|
||||
const qint64 oldBytes = 10 * 10 * 4 + 8 * 8 * 4;
|
||||
QCOMPARE(history.retainedBytes(doc), oldBytes);
|
||||
@@ -242,13 +242,13 @@ void HistoryTests::byteLimitDropsTheOldestPastEntryFirst()
|
||||
const qint64 small = 10 * 10 * 4 + 8 * 8 * 4;
|
||||
DocumentHistory history(100, small);
|
||||
std::optional<CanvasDocument> doc = CanvasDocument(64, 32);
|
||||
doc->layers.push_back(ImageLayer(asset(10, 10, "x"), QPointF(0, 0)));
|
||||
doc.value().layers.push_back(ImageLayer(asset(10, 10, "x"), QPointF(0, 0)));
|
||||
history.begin("First", doc, std::nullopt);
|
||||
doc->layers[0].asset = asset(10, 10, "y");
|
||||
doc.value().layers[0].asset = asset(10, 10, "y");
|
||||
history.end(doc, std::nullopt);
|
||||
QCOMPARE(history.undoCount(), 1);
|
||||
history.begin("Second", doc, std::nullopt);
|
||||
doc->layers[0].asset = asset(10, 10, "z");
|
||||
doc.value().layers[0].asset = asset(10, 10, "z");
|
||||
history.end(doc, std::nullopt);
|
||||
QCOMPARE(history.undoCount(), 1);
|
||||
QCOMPARE(history.undoName(), QString("Second"));
|
||||
@@ -260,10 +260,10 @@ void HistoryTests::undoTrimsRedoWhenItsPixelsExceedTheLimit()
|
||||
const qint64 small = 10 * 10 * 4 + 8 * 8 * 4;
|
||||
DocumentHistory history(100, small);
|
||||
std::optional<CanvasDocument> doc = CanvasDocument(64, 32);
|
||||
doc->layers.push_back(ImageLayer(asset(10, 10, "small"), QPointF(0, 0)));
|
||||
doc.value().layers.push_back(ImageLayer(asset(10, 10, "small"), QPointF(0, 0)));
|
||||
const std::optional<CanvasDocument> before = doc;
|
||||
history.begin("Enlarge", doc, std::nullopt);
|
||||
doc->layers[0].asset = asset(40, 40, "large");
|
||||
doc.value().layers[0].asset = asset(40, 40, "large");
|
||||
history.end(doc, std::nullopt);
|
||||
QCOMPARE(history.undoCount(), 1);
|
||||
const DocumentHistory::Snapshot undone = history.undo().value();
|
||||
@@ -277,12 +277,12 @@ void HistoryTests::undoTrimsThePastBeforeTheRedoStack()
|
||||
const qint64 small = 10 * 10 * 4 + 8 * 8 * 4;
|
||||
DocumentHistory history(100, 2 * small);
|
||||
std::optional<CanvasDocument> doc = CanvasDocument(64, 32);
|
||||
doc->layers.push_back(ImageLayer(asset(10, 10, "a"), QPointF(0, 0)));
|
||||
doc.value().layers.push_back(ImageLayer(asset(10, 10, "a"), QPointF(0, 0)));
|
||||
history.begin("First", doc, std::nullopt);
|
||||
doc->layers[0].asset = asset(10, 10, "b");
|
||||
doc.value().layers[0].asset = asset(10, 10, "b");
|
||||
history.end(doc, std::nullopt);
|
||||
history.begin("Second", doc, std::nullopt);
|
||||
doc->layers[0].asset = asset(15, 15, "c");
|
||||
doc.value().layers[0].asset = asset(15, 15, "c");
|
||||
history.end(doc, std::nullopt);
|
||||
QCOMPARE(history.undoCount(), 2);
|
||||
history.undo();
|
||||
|
||||
@@ -244,7 +244,7 @@ void ImageAdjustmentTests::newAdjustmentLayersStartFromThePaletteRenderAndEditIn
|
||||
session.setAdjustmentEditingID(std::nullopt);
|
||||
session.addAdjustment(AdjustmentKind::grain);
|
||||
const std::vector<ImageLayer> &layers = session.document().value().layers;
|
||||
QCOMPARE(std::count_if(layers.begin(), layers.end(), [](const ImageLayer &layer) { return layer.adjustment && layer.adjustment->kind == AdjustmentKind::grain; }), 2);
|
||||
QCOMPARE(std::count_if(layers.begin(), layers.end(), [](const ImageLayer &layer) { return layer.adjustment && layer.adjustment.value().kind == AdjustmentKind::grain; }), 2);
|
||||
QVERIFY2(session.activeLayer().value().adjustment.value().grain().seed != first, "each Grain layer gets its own pattern");
|
||||
QVERIFY(std::find(allAdjustmentKinds.begin(), allAdjustmentKinds.end(), AdjustmentKind::exposure) != allAdjustmentKinds.end()
|
||||
&& filterKind(AdjustmentKind::exposure) == FilterKind::exposure);
|
||||
|
||||
@@ -135,7 +135,7 @@ void ImageFileDropTests::aPictureIsCopiedToAFileFirst()
|
||||
sky.image = PSDFixture::colorImage(4, 2, 0, 0, 1);
|
||||
sky.bounds = QRectF(0, 0, 4, 2);
|
||||
QMimeData photoshop;
|
||||
photoshop.setData("image/vnd.adobe.photoshop", PSDFixture::data(PSDDocument{4, 2, 72, {sky}}, *sky.image));
|
||||
photoshop.setData("image/vnd.adobe.photoshop", PSDFixture::data(PSDDocument{4, 2, 72, {sky}}, sky.image.value()));
|
||||
photoshop.setData("application/x-other", "ignored");
|
||||
QCOMPARE(ImageFileDrop::providers(photoshop).front()->formats(), QStringList{"image/vnd.adobe.photoshop"});
|
||||
photoshop.setData("image/tiff", "tiff first");
|
||||
@@ -165,7 +165,7 @@ void ImageFileDropTests::aPictureIsCopiedToAFileFirst()
|
||||
done = false;
|
||||
ImageFileDrop::importProviders(raw, developed, std::nullopt, nullptr, std::nullopt, [&done] { done = true; });
|
||||
QTRY_VERIFY(developed.rawDevelop());
|
||||
developed.finishRawDevelop(developed.rawDevelop()->settings);
|
||||
developed.finishRawDevelop(developed.rawDevelop().value().settings);
|
||||
QTRY_VERIFY(done);
|
||||
QCOMPARE(names(developed), QStringList{"Dropped"});
|
||||
QCOMPARE(takeCopy(before), QString("Dropped.dng"));
|
||||
|
||||
@@ -222,7 +222,7 @@ void ImageTrimTests::theSheetSaysSwiftsWordsAndReturnsItsChoice()
|
||||
ok.click();
|
||||
QVERIFY(answer.value() == edges(TrimBasedOn::bottomRightPixelColor, false, true, false, false));
|
||||
sheet.findChild<QPushButton *>("trimCancel")->click();
|
||||
QVERIFY(answer && !answer->has_value());
|
||||
QVERIFY(answer && !answer.value().has_value());
|
||||
}
|
||||
|
||||
void ImageTrimTests::theControllerTrimsThroughTheSheetAsOneStep()
|
||||
|
||||
@@ -77,7 +77,7 @@ void InnerGlowTests::innerGlowCodableRoundTrip()
|
||||
QCOMPARE(ManifestJson::effects(encoded), original);
|
||||
// Absent `enabled` stays absent, read as shown.
|
||||
LayerEffects shown = original;
|
||||
shown.innerGlow->enabled = std::nullopt;
|
||||
shown.innerGlow.value().enabled = std::nullopt;
|
||||
QVERIFY(!ManifestJson::encoded(shown).value("innerGlow").toObject().contains("enabled"));
|
||||
QCOMPARE(ManifestJson::effects(ManifestJson::encoded(shown)), shown);
|
||||
// A key of the wrong type, or missing, refuses it.
|
||||
@@ -165,10 +165,10 @@ void InnerGlowTests::itSitsOverTheOverlayAndUnderTheInnerShadow()
|
||||
const LayerEffectsRenderer::Rendered shadowed = LayerEffectsRenderer::render(square(40, Qt::black), std::nullopt, effects);
|
||||
QCOMPARE(at(shadowed, 0, 20), at(covered, 0, 20));
|
||||
// A sideways shadow covers the glow on one edge.
|
||||
effects.innerShadow->distance = 4;
|
||||
const int x = effects.innerShadow->offset().width() > 0 ? 0 : 39;
|
||||
effects.innerShadow.value().distance = 4;
|
||||
const int x = effects.innerShadow.value().offset().width() > 0 ? 0 : 39;
|
||||
const QColor edge = at(LayerEffectsRenderer::render(square(40, Qt::black), std::nullopt, effects), x, 20);
|
||||
QVERIFY(std::abs(effects.innerShadow->offset().width()) > 3.9);
|
||||
QVERIFY(std::abs(effects.innerShadow.value().offset().width()) > 3.9);
|
||||
QVERIFY(edge.redF() > 0.9 && edge.greenF() < 0.1);
|
||||
}
|
||||
|
||||
@@ -195,11 +195,11 @@ void InnerGlowTests::theSessionAddsCopiesAndCancelsIt()
|
||||
session.addEffect(LayerEffectKind::innerGlow);
|
||||
QCOMPARE(session.history.undoName(), QString("Add Inner Glow"));
|
||||
QVERIFY(session.activeEffects().innerGlow == InnerGlowEffect());
|
||||
session.changeEffects([](LayerEffects &effects) { effects.innerGlow->size = 44; });
|
||||
session.changeEffects([](LayerEffects &effects) { effects.innerGlow.value().size = 44; });
|
||||
session.finishEffectsEditing(false);
|
||||
QVERIFY(!session.activeEffects().innerGlow);
|
||||
session.addEffect(LayerEffectKind::innerGlow);
|
||||
session.changeEffects([](LayerEffects &effects) { effects.innerGlow->size = 44; });
|
||||
session.changeEffects([](LayerEffects &effects) { effects.innerGlow.value().size = 44; });
|
||||
session.finishEffectsEditing(true);
|
||||
// Adding it again keeps the glow it has.
|
||||
session.addEffect(LayerEffectKind::innerGlow);
|
||||
|
||||
@@ -132,14 +132,14 @@ void LayerAdjustmentTests::validityFollowsSwiftsBounds()
|
||||
QCOMPARE((LayerAdjustment{.kind = AdjustmentKind::hsv, .hue = hue, .saturation = saturation, .lightness = lightness}.isValid()), valid);
|
||||
// Each range's values too, and every band's handles finite.
|
||||
LayerAdjustment ranged = full();
|
||||
ranged.hsvSettings->adjustments[ColorRange::blues] = {0, 0, 101};
|
||||
ranged.hsvSettings.value().adjustments[ColorRange::blues] = {0, 0, 101};
|
||||
QVERIFY(!ranged.isValid());
|
||||
ranged = full();
|
||||
ranged.hsvSettings->bands[ColorRange::cyans].falloffEnd = std::numeric_limits<double>::infinity();
|
||||
ranged.hsvSettings.value().bands[ColorRange::cyans].falloffEnd = std::numeric_limits<double>::infinity();
|
||||
QVERIFY(!ranged.isValid());
|
||||
// The legacy fields count only when no range settings exist.
|
||||
ranged = full();
|
||||
ranged.hsvSettings->adjustments[ColorRange::reds] = {-360, -100, -100};
|
||||
ranged.hsvSettings.value().adjustments[ColorRange::reds] = {-360, -100, -100};
|
||||
QVERIFY(ranged.isValid());
|
||||
// Levels as normalized, then every kind's own rules.
|
||||
LayerAdjustment levels{AdjustmentKind::hsv};
|
||||
@@ -308,7 +308,7 @@ void LayerAdjustmentTests::theStoreKeepsAdjustmentsToVersion7WithoutPixels()
|
||||
blank.isGroup = true;
|
||||
QCOMPARE(stored(snapshot), ProjectError::Kind::invalid);
|
||||
blank.isGroup = std::nullopt;
|
||||
blank.adjustment->hue = 400;
|
||||
blank.adjustment.value().hue = 400;
|
||||
QCOMPARE(stored(snapshot), ProjectError::Kind::invalid);
|
||||
}
|
||||
|
||||
@@ -440,7 +440,7 @@ void LayerAdjustmentTests::adjustmentsTravelWithTheirLayers()
|
||||
session->duplicateActiveLayer();
|
||||
QVERIFY(session->activeLayer().value().adjustment == full());
|
||||
ImageLayer changed = session->activeLayer().value();
|
||||
changed.adjustment->hue = 11;
|
||||
changed.adjustment.value().hue = 11;
|
||||
QVERIFY(!(changed == session->activeLayer().value()));
|
||||
}
|
||||
|
||||
|
||||
@@ -237,15 +237,15 @@ void LayerEffectRowTests::theEyeShowsOrHidesItsEffect()
|
||||
LayerEffectRow &stroke = *listed.effects(0)[0];
|
||||
// One step each way; the choice stays as it was.
|
||||
listed.click(listed.eye(stroke), QPoint(10, 11));
|
||||
QVERIFY(!layerWith(listed.session, top).effects->stroke->isEnabled() && listed.session.history.undoName() == "Hide Stroke");
|
||||
QVERIFY(!layerWith(listed.session, top).effects.value().stroke.value().isEnabled() && listed.session.history.undoName() == "Hide Stroke");
|
||||
QVERIFY(!listed.chosen() && listed.eye(stroke).accessibleName() == "Show Stroke");
|
||||
listed.click(listed.eye(stroke), QPoint(10, 11));
|
||||
QVERIFY(layerWith(listed.session, top).effects->stroke->isEnabled() && listed.session.history.undoName() == "Show Stroke");
|
||||
QVERIFY(layerWith(listed.session, top).effects.value().stroke.value().isEnabled() && listed.session.history.undoName() == "Show Stroke");
|
||||
// It rests while layers cannot be edited.
|
||||
listed.session.setIsProjectBusy(true);
|
||||
QVERIFY(!listed.eye(stroke).isEnabled());
|
||||
listed.click(listed.eye(stroke), QPoint(10, 11));
|
||||
QVERIFY(layerWith(listed.session, top).effects->stroke->isEnabled() && !listed.chosen());
|
||||
QVERIFY(layerWith(listed.session, top).effects.value().stroke.value().isEnabled() && !listed.chosen());
|
||||
listed.session.setIsProjectBusy(false);
|
||||
QVERIFY(listed.eye(stroke).isEnabled());
|
||||
}
|
||||
@@ -270,14 +270,14 @@ void LayerEffectRowTests::pressesFollowSwiftsOrder()
|
||||
listed.session.dropEffectSelection();
|
||||
listed.press(listed.eye(shadow), QPoint(10, 5), Qt::AltModifier);
|
||||
listed.release(listed.eye(shadow), QPoint(10, 5), Qt::AltModifier);
|
||||
QVERIFY(layerWith(listed.session, top).effects->shadow->isEnabled() && listed.chosen() == (LayerEffectSelection{top, LayerEffectKind::shadow}));
|
||||
QVERIFY(layerWith(listed.session, top).effects.value().shadow.value().isEnabled() && listed.chosen() == (LayerEffectSelection{top, LayerEffectKind::shadow}));
|
||||
// An Alt double click on the eye hides nothing either.
|
||||
listed.session.dropEffectSelection();
|
||||
QTest::mouseDClick(listed.list.windowHandle(), Qt::LeftButton, Qt::AltModifier, listed.at(listed.eye(shadow), QPoint(12, 5)));
|
||||
QVERIFY(layerWith(listed.session, top).effects->shadow->isEnabled() && listed.chosen() == (LayerEffectSelection{top, LayerEffectKind::shadow}));
|
||||
QVERIFY(layerWith(listed.session, top).effects.value().shadow.value().isEnabled() && listed.chosen() == (LayerEffectSelection{top, LayerEffectKind::shadow}));
|
||||
// On the strip, the eye's Alt press clips too.
|
||||
listed.click(listed.eye(shadow), QPoint(10, 20), Qt::AltModifier);
|
||||
QVERIFY(!layerWith(listed.session, top).maskSourceID && !listed.chosen() && layerWith(listed.session, top).effects->shadow->isEnabled());
|
||||
QVERIFY(!layerWith(listed.session, top).maskSourceID && !listed.chosen() && layerWith(listed.session, top).effects.value().shadow.value().isEnabled());
|
||||
// Its own row's press lets go, keeping the layer.
|
||||
listed.session.selectEffect(LayerEffectKind::stroke, top);
|
||||
listed.click(listed.cell(0), QPoint(listed.cell(0).width() - 20, 20));
|
||||
@@ -295,7 +295,7 @@ void LayerEffectRowTests::pressesFollowSwiftsOrder()
|
||||
QVERIFY(listed.chosen() == (LayerEffectSelection{top, LayerEffectKind::shadow}));
|
||||
listed.release(shadow, QPoint(150, 5), Qt::AltModifier | Qt::ControlModifier);
|
||||
listed.click(listed.eye(shadow), QPoint(10, 5), Qt::AltModifier | Qt::ControlModifier);
|
||||
QVERIFY(!layerWith(listed.session, top).effects->shadow->isEnabled());
|
||||
QVERIFY(!layerWith(listed.session, top).effects.value().shadow.value().isEnabled());
|
||||
// A plain press on another effect chooses it at once.
|
||||
listed.press(stroke, QPoint(150, 12));
|
||||
QVERIFY(listed.chosen() == (LayerEffectSelection{top, LayerEffectKind::stroke}));
|
||||
@@ -412,7 +412,7 @@ void LayerEffectRowTests::effectDropsLandOnTheRowUnderThePointer()
|
||||
const std::unique_ptr<QMimeData> probe(effectDrag(listed.list, payload));
|
||||
QCOMPARE(listed.list.dropTarget(*probe, Qt::CopyAction, onMiddle).value(), (LayerDropTarget{.row = 1, .onRow = true, .atBottom = false, .copying = true}));
|
||||
QVERIFY(listed.drop(effectDrag(listed.list, payload), onMiddle));
|
||||
QVERIFY(listed.lastAction == Qt::CopyAction && layerWith(listed.session, middle).effects->stroke->size == 12);
|
||||
QVERIFY(listed.lastAction == Qt::CopyAction && layerWith(listed.session, middle).effects.value().stroke.value().size == 12);
|
||||
QVERIFY(listed.session.history.undoName() == "Copy Stroke" && listed.chosen() == (LayerEffectSelection{middle, LayerEffectKind::stroke}));
|
||||
// Refused: its own row, a missing kind, strangers, bad words.
|
||||
QVERIFY(!listed.drop(effectDrag(listed.list, payload), onTop));
|
||||
@@ -448,7 +448,7 @@ void LayerEffectRowTests::aRowRebuiltUnderItsOwnPressLivesOn()
|
||||
QTest::mouseDClick(shadow.data(), Qt::LeftButton, Qt::NoModifier, QPoint(150, 12));
|
||||
QVERIFY(shadow && !shadow->isVisible());
|
||||
QVERIFY(listed.session.effectsEditing() == (LayerEffectSelection{top, LayerEffectKind::shadow}) && listed.list.hasFocus());
|
||||
QVERIFY(!layerWith(listed.session, top).effects->stroke && listed.effects(0).size() == 1);
|
||||
QVERIFY(!layerWith(listed.session, top).effects.value().stroke && listed.effects(0).size() == 1);
|
||||
QTRY_VERIFY(!shadow);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,32 +86,32 @@ QImage replayed(const QImage &pixels, const LayerEffects &effects)
|
||||
for (int x = 0; x < pixels.width(); ++x)
|
||||
shader.shape.push_back(float(pixels.constScanLine(y)[x * 4 + 3]) / 255.f);
|
||||
}
|
||||
const auto on = [](const auto &effect) { return effect && effect->isEnabled() && effect->opacity > 0; };
|
||||
const bool stroke = on(effects.stroke) && effects.stroke->size > 0;
|
||||
const auto on = [](const auto &effect) { return effect && effect.value().isEnabled() && effect.value().opacity > 0; };
|
||||
const bool stroke = on(effects.stroke) && effects.stroke.value().size > 0;
|
||||
std::vector<float> ring(shader.shape.size()), cast(shader.shape.size()), inner(shader.shape.size()), glow(shader.shape.size()),
|
||||
innerGlow(shader.shape.size());
|
||||
if (stroke) {
|
||||
const int reach = std::max(1, int(std::lround(effects.stroke->size)));
|
||||
const std::vector<float> moved = shader.spread(shader.spread(shader.shape, reach, effects.stroke->inside, true), reach, effects.stroke->inside, false);
|
||||
const int reach = std::max(1, int(std::lround(effects.stroke.value().size)));
|
||||
const std::vector<float> moved = shader.spread(shader.spread(shader.shape, reach, effects.stroke.value().inside, true), reach, effects.stroke.value().inside, false);
|
||||
for (size_t index = 0; index < ring.size(); ++index)
|
||||
ring[index] = std::clamp(effects.stroke->inside ? shader.shape[index] - moved[index] : moved[index] - shader.shape[index], 0.f, 1.f);
|
||||
ring[index] = std::clamp(effects.stroke.value().inside ? shader.shape[index] - moved[index] : moved[index] - shader.shape[index], 0.f, 1.f);
|
||||
}
|
||||
if (on(effects.shadow))
|
||||
cast = shader.softened(shader.shift(shader.shape, float(effects.shadow->offset().width()), float(effects.shadow->offset().height())),
|
||||
float(effects.shadow->blur / 2));
|
||||
const bool glowing = on(effects.outerGlow) && effects.outerGlow->size > 0;
|
||||
cast = shader.softened(shader.shift(shader.shape, float(effects.shadow.value().offset().width()), float(effects.shadow.value().offset().height())),
|
||||
float(effects.shadow.value().blur / 2));
|
||||
const bool glowing = on(effects.outerGlow) && effects.outerGlow.value().size > 0;
|
||||
if (glowing)
|
||||
glow = shader.softened(shader.shape, float(effects.outerGlow->size / 2));
|
||||
const bool glowingInside = on(effects.innerGlow) && effects.innerGlow->size > 0;
|
||||
glow = shader.softened(shader.shape, float(effects.outerGlow.value().size / 2));
|
||||
const bool glowingInside = on(effects.innerGlow) && effects.innerGlow.value().size > 0;
|
||||
if (glowingInside) {
|
||||
const std::vector<float> softened = shader.softened(shader.shape, float(effects.innerGlow->size / 2));
|
||||
const std::vector<float> softened = shader.softened(shader.shape, float(effects.innerGlow.value().size / 2));
|
||||
for (size_t index = 0; index < innerGlow.size(); ++index)
|
||||
innerGlow[index] = std::clamp(shader.shape[index] * (1.f - softened[index]), 0.f, 1.f);
|
||||
}
|
||||
if (on(effects.innerShadow)) {
|
||||
const std::vector<float> moved = shader.softened(shader.shift(shader.shape, float(effects.innerShadow->offset().width()),
|
||||
float(effects.innerShadow->offset().height())),
|
||||
float(effects.innerShadow->blur / 2));
|
||||
const std::vector<float> moved = shader.softened(shader.shift(shader.shape, float(effects.innerShadow.value().offset().width()),
|
||||
float(effects.innerShadow.value().offset().height())),
|
||||
float(effects.innerShadow.value().blur / 2));
|
||||
for (size_t index = 0; index < inner.size(); ++index)
|
||||
inner[index] = std::clamp(shader.shape[index] * (1.f - moved[index]), 0.f, 1.f);
|
||||
}
|
||||
@@ -127,25 +127,25 @@ QImage replayed(const QImage &pixels, const LayerEffects &effects)
|
||||
alpha = coverage + alpha * (1.f - coverage);
|
||||
};
|
||||
if (on(effects.shadow))
|
||||
blend(effects.shadow->color(), std::clamp(cast[index] * float(effects.shadow->opacity), 0.f, 1.f));
|
||||
blend(effects.shadow.value().color(), std::clamp(cast[index] * float(effects.shadow.value().opacity), 0.f, 1.f));
|
||||
if (glowing)
|
||||
blend(effects.outerGlow->color(), std::clamp(glow[index] * (1.f - shader.shape[index]) * float(effects.outerGlow->opacity), 0.f, 1.f));
|
||||
const float strokeCoverage = stroke ? std::clamp(ring[index] * float(effects.stroke->opacity), 0.f, 1.f) : 0.f;
|
||||
if (stroke && !effects.stroke->inside)
|
||||
blend(effects.stroke->color(), strokeCoverage);
|
||||
blend(effects.outerGlow.value().color(), std::clamp(glow[index] * (1.f - shader.shape[index]) * float(effects.outerGlow.value().opacity), 0.f, 1.f));
|
||||
const float strokeCoverage = stroke ? std::clamp(ring[index] * float(effects.stroke.value().opacity), 0.f, 1.f) : 0.f;
|
||||
if (stroke && !effects.stroke.value().inside)
|
||||
blend(effects.stroke.value().color(), strokeCoverage);
|
||||
const uchar *source = pixels.constScanLine(y) + x * 4;
|
||||
const float sourceAlpha = float(source[3]) / 255.f;
|
||||
for (int channel = 0; channel < 3; ++channel)
|
||||
colour[channel] = float(source[channel]) / 255.f + colour[channel] * (1.f - sourceAlpha);
|
||||
alpha = sourceAlpha + alpha * (1.f - sourceAlpha);
|
||||
if (on(effects.colorOverlay))
|
||||
blend(effects.colorOverlay->color(), std::clamp(shader.shape[index] * float(effects.colorOverlay->opacity), 0.f, 1.f));
|
||||
blend(effects.colorOverlay.value().color(), std::clamp(shader.shape[index] * float(effects.colorOverlay.value().opacity), 0.f, 1.f));
|
||||
if (glowingInside)
|
||||
blend(effects.innerGlow->color(), std::clamp(innerGlow[index] * float(effects.innerGlow->opacity), 0.f, 1.f));
|
||||
blend(effects.innerGlow.value().color(), std::clamp(innerGlow[index] * float(effects.innerGlow.value().opacity), 0.f, 1.f));
|
||||
if (on(effects.innerShadow))
|
||||
blend(effects.innerShadow->color(), std::clamp(inner[index] * float(effects.innerShadow->opacity), 0.f, 1.f));
|
||||
if (stroke && effects.stroke->inside)
|
||||
blend(effects.stroke->color(), strokeCoverage);
|
||||
blend(effects.innerShadow.value().color(), std::clamp(inner[index] * float(effects.innerShadow.value().opacity), 0.f, 1.f));
|
||||
if (stroke && effects.stroke.value().inside)
|
||||
blend(effects.stroke.value().color(), strokeCoverage);
|
||||
uchar *out = result.scanLine(y) + x * 4;
|
||||
for (int channel = 0; channel < 3; ++channel)
|
||||
out[channel] = uchar(std::clamp(colour[channel], 0.f, 1.f) * 255.f + 0.5f);
|
||||
@@ -219,7 +219,7 @@ void LayerEffectsKernelTests::everyEffectMatchesTheShader_data()
|
||||
all.innerShadow = inner.innerShadow;
|
||||
all.outerGlow = glow.outerGlow;
|
||||
all.innerGlow = innerGlow.innerGlow;
|
||||
all.shadow->blur = 0;
|
||||
all.shadow.value().blur = 0;
|
||||
wide.stroke = StrokeEffect{.size = 9, .red = 1, .opacity = 1};
|
||||
wide.shadow = ShadowEffect{.angle = 200, .distance = 0.5, .blur = 0.01, .opacity = 1};
|
||||
QTest::newRow("outside stroke") << outside;
|
||||
|
||||
@@ -60,11 +60,11 @@ void LayerEffectsRendererTests::theMarginHoldsWhatReachesOut()
|
||||
LayerEffects cast = stroke(10);
|
||||
cast.shadow = ShadowEffect{.distance = 5, .blur = 2.5};
|
||||
QCOMPARE(LayerEffectsRenderer::margin(cast), 15.0);
|
||||
cast.shadow->blur = 4;
|
||||
cast.shadow.value().blur = 4;
|
||||
QCOMPARE(LayerEffectsRenderer::margin(cast), 19.0);
|
||||
// Switched off, it takes no room.
|
||||
cast.shadow->enabled = false;
|
||||
cast.stroke->enabled = false;
|
||||
cast.shadow.value().enabled = false;
|
||||
cast.stroke.value().enabled = false;
|
||||
QCOMPARE(LayerEffectsRenderer::margin(cast), 2.0);
|
||||
}
|
||||
|
||||
@@ -113,11 +113,11 @@ void LayerEffectsRendererTests::renderingPadsMasksThenRunsTheKernel()
|
||||
QVERIFY(pixel(soft, 2, 10)[3] == 0 && pixel(soft, 65, 10)[3] == 255);
|
||||
// Switched-off effects are none; invalid or vast ones refuse.
|
||||
LayerEffects hidden = stroke(3);
|
||||
hidden.stroke->enabled = false;
|
||||
hidden.stroke.value().enabled = false;
|
||||
QCOMPARE(LayerEffectsRenderer::render(image, std::nullopt, hidden).image.size(), QSize(24, 14));
|
||||
// A switched-off effect is not judged.
|
||||
LayerEffects wild = stroke(600);
|
||||
wild.stroke->enabled = false;
|
||||
wild.stroke.value().enabled = false;
|
||||
QVERIFY(LayerEffectsRenderer::render(image, std::nullopt, wild).image == LayerEffectsRenderer::render(image, std::nullopt, hidden).image);
|
||||
try {
|
||||
LayerEffectsRenderer::render(image, std::nullopt, stroke(501));
|
||||
@@ -142,7 +142,7 @@ void LayerEffectsRendererTests::theCacheKeepsEightWithinItsBudget()
|
||||
QVERIFY(!LayerEffectsRenderer::cached(image, std::nullopt, std::nullopt));
|
||||
QVERIFY(!LayerEffectsRenderer::cached(image, std::nullopt, LayerEffects()));
|
||||
LayerEffects hidden = stroke(2);
|
||||
hidden.stroke->enabled = false;
|
||||
hidden.stroke.value().enabled = false;
|
||||
QVERIFY(!LayerEffectsRenderer::cached(image, std::nullopt, hidden));
|
||||
QVERIFY(!LayerEffectsRenderer::cached(image, std::nullopt, stroke(600)));
|
||||
// The same pixels, mask and effects: the same result.
|
||||
|
||||
@@ -83,7 +83,7 @@ void LayerEffectsSessionTests::addingAnEffectTakesItsDefaultsAndOpensItsPanel()
|
||||
present.shadow = ShadowEffect{.distance = 33};
|
||||
session.setEffects(present);
|
||||
session.addEffect(LayerEffectKind::shadow);
|
||||
QVERIFY(two.effects(two.top).value().shadow->distance == 33);
|
||||
QVERIFY(two.effects(two.top).value().shadow.value().distance == 33);
|
||||
session.finishEffectsEditing(true);
|
||||
const int kept = session.history.undoCount();
|
||||
session.addEffect(LayerEffectKind::colorOverlay);
|
||||
@@ -128,15 +128,15 @@ void LayerEffectsSessionTests::cancelPutsBackThisPanelsEffectAlone()
|
||||
session.setEffects(start);
|
||||
session.selectEffect(LayerEffectKind::stroke, two.top, true);
|
||||
QVERIFY(session.effectsEditingOriginal() == start);
|
||||
session.changeEffects([](LayerEffects &effects) { effects.stroke->size = 9; });
|
||||
session.changeEffects([](LayerEffects &effects) { effects.stroke.value().size = 9; });
|
||||
QCOMPARE(session.history.undoName(), QString("Edit Stroke"));
|
||||
// Another effect changed meanwhile keeps its change.
|
||||
LayerEffects other = two.effects(two.top).value();
|
||||
other.shadow->distance = 30;
|
||||
other.shadow.value().distance = 30;
|
||||
session.setEffects(other);
|
||||
session.finishEffectsEditing(false);
|
||||
const LayerEffects after = two.effects(two.top).value();
|
||||
QVERIFY(after.stroke->size == 3 && after.shadow->distance == 30);
|
||||
QVERIFY(after.stroke.value().size == 3 && after.shadow.value().distance == 30);
|
||||
QCOMPARE(session.history.undoName(), QString("Cancel Stroke"));
|
||||
QVERIFY(!session.effectsEditing() && !session.effectsEditingOriginal());
|
||||
// The chosen effect stays chosen while its layer shows it.
|
||||
@@ -147,26 +147,26 @@ void LayerEffectsSessionTests::cancelPutsBackThisPanelsEffectAlone()
|
||||
QVERIFY(!two.effects(two.top).value().innerShadow);
|
||||
QVERIFY(!session.effectSelection());
|
||||
session.selectEffect(LayerEffectKind::shadow, two.top, true);
|
||||
session.changeEffects([](LayerEffects &effects) { effects.shadow->blur = 2; });
|
||||
session.changeEffects([](LayerEffects &effects) { effects.shadow.value().blur = 2; });
|
||||
const int steps = session.history.undoCount();
|
||||
session.finishEffectsEditing(true);
|
||||
QVERIFY(session.history.undoCount() == steps && two.effects(two.top).value().shadow->blur == 2);
|
||||
QVERIFY(session.history.undoCount() == steps && two.effects(two.top).value().shadow.value().blur == 2);
|
||||
// Nothing open, nothing to finish.
|
||||
session.finishEffectsEditing(false);
|
||||
QCOMPARE(session.history.undoCount(), steps);
|
||||
// Opening another panel cancels the open one's changes.
|
||||
session.selectEffect(LayerEffectKind::stroke, two.top, true);
|
||||
session.changeEffects([](LayerEffects &effects) { effects.stroke->size = 40; });
|
||||
session.changeEffects([](LayerEffects &effects) { effects.stroke.value().size = 40; });
|
||||
session.selectEffect(LayerEffectKind::shadow, two.top, true);
|
||||
QVERIFY(two.effects(two.top).value().stroke->size == 3 && session.history.undoName() == QString("Cancel Stroke"));
|
||||
QVERIFY(two.effects(two.top).value().stroke.value().size == 3 && session.history.undoName() == QString("Cancel Stroke"));
|
||||
session.finishEffectsEditing(true);
|
||||
// An overlay's Cancel puts back the overlay alone.
|
||||
session.addEffect(LayerEffectKind::colorOverlay);
|
||||
session.finishEffectsEditing(true);
|
||||
session.selectEffect(LayerEffectKind::colorOverlay, two.top, true);
|
||||
session.changeEffects([](LayerEffects &effects) { effects.colorOverlay->opacity = 0.25; });
|
||||
session.changeEffects([](LayerEffects &effects) { effects.colorOverlay.value().opacity = 0.25; });
|
||||
session.finishEffectsEditing(false);
|
||||
QCOMPARE(two.effects(two.top).value().colorOverlay->opacity, 1.0);
|
||||
QCOMPARE(two.effects(two.top).value().colorOverlay.value().opacity, 1.0);
|
||||
// Its layer's effects all gone, Cancel has nothing to restore.
|
||||
session.selectEffect(LayerEffectKind::shadow, two.top, true);
|
||||
session.setEffects(LayerEffects(), two.top);
|
||||
@@ -195,7 +195,7 @@ void LayerEffectsSessionTests::setEffectsIsOneStepOnAValidChange()
|
||||
// The same again, an invalid one, a missing layer: nothing.
|
||||
session.setEffects(effects, two.bottom);
|
||||
LayerEffects wild = effects;
|
||||
wild.stroke->size = 501;
|
||||
wild.stroke.value().size = 501;
|
||||
session.setEffects(wild, two.bottom);
|
||||
session.setEffects(effects, QUuid::createUuid());
|
||||
QCOMPARE(session.history.undoCount(), steps + 1);
|
||||
@@ -220,18 +220,18 @@ void LayerEffectsSessionTests::panelEditsStayWithTheirLayer()
|
||||
EditorSession &session = two.session;
|
||||
session.addEffect(LayerEffectKind::stroke);
|
||||
session.selectLayer(two.bottom);
|
||||
session.changeEffects([](LayerEffects &effects) { effects.stroke->size = 12; });
|
||||
QVERIFY(two.effects(two.top).value().stroke->size == 12 && !two.effects(two.bottom));
|
||||
session.changeEffects([](LayerEffects &effects) { effects.stroke.value().size = 12; });
|
||||
QVERIFY(two.effects(two.top).value().stroke.value().size == 12 && !two.effects(two.bottom));
|
||||
// Once its effect is gone, edits have nowhere to go.
|
||||
LayerEffects without = two.effects(two.top).value();
|
||||
without.stroke.reset();
|
||||
without.shadow = ShadowEffect();
|
||||
session.setEffects(without, two.top);
|
||||
const int steps = session.history.undoCount();
|
||||
session.changeEffects([](LayerEffects &effects) { effects.shadow->blur = 1; });
|
||||
session.changeEffects([](LayerEffects &effects) { effects.shadow.value().blur = 1; });
|
||||
QCOMPARE(session.history.undoCount(), steps);
|
||||
session.finishEffectsEditing(true);
|
||||
session.changeEffects([](LayerEffects &effects) { effects.shadow->blur = 1; });
|
||||
session.changeEffects([](LayerEffects &effects) { effects.shadow.value().blur = 1; });
|
||||
QCOMPARE(session.history.undoCount(), steps);
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ void LayerEffectsSessionTests::copyingAnEffectClosesTheTargetsPanelFirst()
|
||||
target.stroke = StrokeEffect();
|
||||
session.setEffects(target, two.bottom);
|
||||
session.selectEffect(LayerEffectKind::shadow, two.bottom, true);
|
||||
session.changeEffects([](LayerEffects &effects) { effects.shadow->blur = 7; });
|
||||
session.changeEffects([](LayerEffects &effects) { effects.shadow.value().blur = 7; });
|
||||
session.copyEffect(LayerEffectKind::shadow, two.top, two.bottom);
|
||||
QCOMPARE(session.history.undoName(), QString("Copy Drop Shadow"));
|
||||
QVERIFY(!session.effectsEditing() && two.effects(two.bottom).value().shadow == source.shadow);
|
||||
@@ -259,7 +259,7 @@ void LayerEffectsSessionTests::copyingAnEffectClosesTheTargetsPanelFirst()
|
||||
QVERIFY(session.effectSelection() == (LayerEffectSelection{two.bottom, LayerEffectKind::shadow}) && session.activeLayerID() == two.bottom);
|
||||
// The open edit was kept: one undo brings it back.
|
||||
session.undo();
|
||||
QVERIFY(two.effects(two.bottom).value().shadow->blur == 7);
|
||||
QVERIFY(two.effects(two.bottom).value().shadow.value().blur == 7);
|
||||
session.redo();
|
||||
// No source, a blank or folder target, busy: no copy.
|
||||
QVERIFY(!session.canCopyEffect(LayerEffectKind::shadow, QUuid::createUuid(), two.bottom));
|
||||
@@ -278,9 +278,9 @@ void LayerEffectsSessionTests::togglingAndRemovingAnEffect()
|
||||
session.addEffect(LayerEffectKind::stroke);
|
||||
session.finishEffectsEditing(true);
|
||||
session.toggleEffect(LayerEffectKind::stroke, two.top);
|
||||
QVERIFY(two.effects(two.top).value().stroke->enabled == false && session.history.undoName() == QString("Hide Stroke"));
|
||||
QVERIFY(two.effects(two.top).value().stroke.value().enabled == false && session.history.undoName() == QString("Hide Stroke"));
|
||||
session.toggleEffect(LayerEffectKind::stroke, two.top);
|
||||
QVERIFY(two.effects(two.top).value().stroke->enabled == true && session.history.undoName() == QString("Show Stroke"));
|
||||
QVERIFY(two.effects(two.top).value().stroke.value().enabled == true && session.history.undoName() == QString("Show Stroke"));
|
||||
// Toggling a missing effect, or none at all, does nothing.
|
||||
const int steps = session.history.undoCount();
|
||||
session.toggleEffect(LayerEffectKind::shadow, two.top);
|
||||
@@ -381,7 +381,7 @@ void LayerEffectsSessionTests::thePickerEditsTheOpenEffect()
|
||||
QVERIFY(session.colorPicker());
|
||||
const int steps = session.history.undoCount();
|
||||
session.previewEffectColor();
|
||||
QVERIFY(session.history.undoCount() == steps && two.effects(two.top).value().stroke->red == 1);
|
||||
QVERIFY(session.history.undoCount() == steps && two.effects(two.top).value().stroke.value().red == 1);
|
||||
session.openEffectColorPicker(LayerEffectKind::shadow);
|
||||
QVERIFY(session.colorPicker().value().target.kind == ColorPickerTarget::Kind::palette);
|
||||
session.closeColorPicker(false);
|
||||
@@ -408,7 +408,7 @@ void LayerEffectsSessionTests::thePickerEditsTheOpenEffect()
|
||||
session.setColorPickerHSB(PickerHSB(PaletteColor{1, 1, 0}));
|
||||
session.previewEffectColor();
|
||||
session.addEffect(LayerEffectKind::stroke);
|
||||
QVERIFY(!session.colorPicker() && two.effects(two.top).value().shadow->color() == PaletteColor::black());
|
||||
QVERIFY(!session.colorPicker() && two.effects(two.top).value().shadow.value().color() == PaletteColor::black());
|
||||
}
|
||||
|
||||
void LayerEffectsSessionTests::everyChangeIsAnnouncedLast()
|
||||
@@ -424,7 +424,7 @@ void LayerEffectsSessionTests::everyChangeIsAnnouncedLast()
|
||||
return seen == left ? QString() : seen.join("; ") + " != " + left.join("; ");
|
||||
};
|
||||
QCOMPARE(stale([&] { session.addEffect(LayerEffectKind::stroke); }), QString());
|
||||
QCOMPARE(stale([&] { session.changeEffects([](LayerEffects &effects) { effects.stroke->size = 6; }); }), QString());
|
||||
QCOMPARE(stale([&] { session.changeEffects([](LayerEffects &effects) { effects.stroke.value().size = 6; }); }), QString());
|
||||
QCOMPARE(stale([&] { session.openEffectColorPicker(LayerEffectKind::stroke); }), QString());
|
||||
QCOMPARE(stale([&] { session.setColorPickerHSB(PickerHSB(PaletteColor{1, 0, 0})); }), QString());
|
||||
QCOMPARE(stale([&] { session.previewEffectColor(); }), QString());
|
||||
|
||||
@@ -118,16 +118,16 @@ void LayerEffectsTests::validityFollowsSwiftsBounds()
|
||||
QVERIFY(!InnerShadowEffect{.angle = -360.5}.isValid() && !InnerShadowEffect{.distance = -1}.isValid() && !InnerShadowEffect{.blur = notANumber}.isValid());
|
||||
// One bad effect spoils the set.
|
||||
LayerEffects effects = every();
|
||||
effects.innerShadow->blur = infinity;
|
||||
effects.innerShadow.value().blur = infinity;
|
||||
QVERIFY(!effects.isValid());
|
||||
effects = every();
|
||||
effects.stroke->size = 501;
|
||||
effects.stroke.value().size = 501;
|
||||
QVERIFY(!effects.isValid());
|
||||
effects = every();
|
||||
effects.shadow->distance = -1;
|
||||
effects.shadow.value().distance = -1;
|
||||
QVERIFY(!effects.isValid());
|
||||
effects = every();
|
||||
effects.colorOverlay->opacity = 2;
|
||||
effects.colorOverlay.value().opacity = 2;
|
||||
QVERIFY(!effects.isValid());
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ void LayerEffectsTests::theHelpersReadAndWriteOneKind()
|
||||
effects.setEnabled(true, LayerEffectKind::stroke);
|
||||
effects.setEnabled(false, LayerEffectKind::innerShadow);
|
||||
effects.setEnabled(true, LayerEffectKind::shadow);
|
||||
QVERIFY(effects.stroke->enabled == true && effects.innerShadow->enabled == false && !effects.shadow);
|
||||
QVERIFY(effects.stroke.value().enabled == true && effects.innerShadow.value().enabled == false && !effects.shadow);
|
||||
// Visible keeps what is switched on.
|
||||
const LayerEffects shown = effects.visible();
|
||||
QVERIFY(shown.stroke == effects.stroke && !shown.innerShadow && !shown.shadow && !shown.colorOverlay);
|
||||
|
||||
+12
-12
@@ -351,8 +351,8 @@ void LayerMaskTests::maskTransformPrefersThePlacement()
|
||||
QCOMPARE(layer.maskTransform(), layer.transform);
|
||||
layer.mask = wideMask();
|
||||
QCOMPARE(layer.maskTransform(), layer.transform);
|
||||
layer.mask->placement = LayerTransform{.origin = {5, 6}, .size = {7, 8}};
|
||||
QCOMPARE(layer.maskTransform(), *layer.mask->placement);
|
||||
layer.mask.value().placement = LayerTransform{.origin = {5, 6}, .size = {7, 8}};
|
||||
QCOMPARE(layer.maskTransform(), layer.mask.value().placement.value());
|
||||
}
|
||||
|
||||
void LayerMaskTests::layersCompareAndRecordTheirMasks()
|
||||
@@ -370,7 +370,7 @@ void LayerMaskTests::layersCompareAndRecordTheirMasks()
|
||||
QVERIFY(!(masked == layer));
|
||||
QCOMPARE(masked, ImageLayer(masked));
|
||||
ImageLayer disabled = masked;
|
||||
disabled.mask->isEnabled = false;
|
||||
disabled.mask.value().isEnabled = false;
|
||||
QVERIFY(!(disabled == masked));
|
||||
const ProjectLayerRecord hidden = disabled.hierarchyRecord();
|
||||
QCOMPARE(hidden.maskEnabled, std::optional<bool>(false));
|
||||
@@ -382,12 +382,12 @@ void LayerMaskTests::layersCompareAndRecordTheirMasks()
|
||||
QCOMPARE(record.maskPlacement, std::nullopt);
|
||||
QCOMPARE(record.maskLinked, std::optional<bool>(true));
|
||||
ImageLayer unlinked = masked;
|
||||
unlinked.mask->isLinked = false;
|
||||
unlinked.mask->placement = LayerTransform{.origin = {5, 6}, .size = {7, 8}};
|
||||
unlinked.mask.value().isLinked = false;
|
||||
unlinked.mask.value().placement = LayerTransform{.origin = {5, 6}, .size = {7, 8}};
|
||||
const ProjectLayerRecord moved = unlinked.hierarchyRecord();
|
||||
QCOMPARE(moved.maskEnabled, std::optional<bool>(true));
|
||||
QCOMPARE(moved.maskLinked, std::optional<bool>(false));
|
||||
QCOMPARE(moved.maskPlacement, unlinked.mask->placement);
|
||||
QCOMPARE(moved.maskPlacement, unlinked.mask.value().placement);
|
||||
}
|
||||
|
||||
void LayerMaskTests::historyCountsMaskPixels()
|
||||
@@ -395,18 +395,18 @@ void LayerMaskTests::historyCountsMaskPixels()
|
||||
DocumentHistory history;
|
||||
std::optional<CanvasDocument> document = CanvasDocument(40, 20);
|
||||
const ImportedImage pixels(BrushRaster::context(40, 20, false), QImage(), "Photo");
|
||||
document->layers.push_back(ImageLayer(pixels, QPointF(0, 0)));
|
||||
document->layers[0].mask = LayerMask(LayerMask::assetFrom(gray(200, 100, 255)));
|
||||
const qint64 maskBytes = document->layers[0].mask->asset.byteCount()
|
||||
+ document->layers[0].mask->asset.thumbnail.sizeInBytes();
|
||||
document.value().layers.push_back(ImageLayer(pixels, QPointF(0, 0)));
|
||||
document.value().layers[0].mask = LayerMask(LayerMask::assetFrom(gray(200, 100, 255)));
|
||||
const qint64 maskBytes = document.value().layers[0].mask.value().asset.byteCount()
|
||||
+ document.value().layers[0].mask.value().asset.thumbnail.sizeInBytes();
|
||||
QCOMPARE(maskBytes, qint64(200 * 100 + 96 * 48));
|
||||
history.begin("Disable Layer Mask", document, std::nullopt);
|
||||
document->layers[0].mask->isEnabled = false;
|
||||
document.value().layers[0].mask.value().isEnabled = false;
|
||||
history.end(document, std::nullopt);
|
||||
QCOMPARE(history.undoCount(), 1);
|
||||
QCOMPARE(history.retainedBytes(document), 0);
|
||||
history.begin("Delete Layer Mask", document, std::nullopt);
|
||||
document->layers[0].mask.reset();
|
||||
document.value().layers[0].mask.reset();
|
||||
history.end(document, std::nullopt);
|
||||
QCOMPARE(history.retainedBytes(document), maskBytes);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ void LayerTests::importedLayerTakesItsSizeAndNameFromTheAsset()
|
||||
QCOMPARE(layer.name, QString("Photo"));
|
||||
QCOMPARE(layer.origin(), QPointF(40, -10));
|
||||
QCOMPARE(layer.size(), QSizeF(320, 200));
|
||||
QCOMPARE(layer.asset->image().size(), QSize(320, 200));
|
||||
QCOMPARE(layer.asset.value().image().size(), QSize(320, 200));
|
||||
QVERIFY(layer.isVisible);
|
||||
QCOMPARE(layer.opacity, 1.0);
|
||||
QCOMPARE(layer.blendMode, LayerBlendMode::normal);
|
||||
|
||||
@@ -326,7 +326,7 @@ void MaskAloneTests::aPendingGradientElsewhereStaysOutOfTheView()
|
||||
session.selectLayerTarget(shown.id, false);
|
||||
session.beginGradient(QPointF(0, 80));
|
||||
session.moveGradient(std::nullopt, QPointF(200, 80));
|
||||
QVERIFY(session.gradientEdit().has_value() && !session.gradientEdit()->raster->isMask);
|
||||
QVERIFY(session.gradientEdit().has_value() && !session.gradientEdit().value().raster->isMask);
|
||||
session.toggleMaskAlone(shown.id);
|
||||
// The commit is posted: the pixels' raster lingers, unshown.
|
||||
QVERIFY(session.maskAloneLayer().has_value() && session.gradientEdit().has_value());
|
||||
@@ -338,7 +338,7 @@ void MaskAloneTests::aPendingGradientElsewhereStaysOutOfTheView()
|
||||
session.addLayerMask();
|
||||
session.beginGradient(QPointF(0, 80));
|
||||
session.moveGradient(std::nullopt, QPointF(200, 80));
|
||||
QVERIFY(session.gradientEdit().has_value() && session.gradientEdit()->raster->isMask && session.gradientEdit()->raster->layer.id == other);
|
||||
QVERIFY(session.gradientEdit().has_value() && session.gradientEdit().value().raster->isMask && session.gradientEdit().value().raster->layer.id == other);
|
||||
session.toggleMaskAlone(shown.id);
|
||||
QCOMPARE(session.maskAloneLayer().value().id, shown.id);
|
||||
QVERIFY(session.gradientEdit().has_value());
|
||||
|
||||
@@ -284,6 +284,11 @@ void NativeResolutionPaintTests::layersPanelSaysHowFarALayerIsScaled()
|
||||
QCOMPARE(sizeLabel(layer), QString("500 × 300 px · 50%"));
|
||||
layer.transform.size = QSizeF(2333, 2000);
|
||||
QCOMPARE(sizeLabel(layer), QString("2333 × 2000 px · 233.3%"));
|
||||
// Exact ties go to the even tenth.
|
||||
layer.transform.size = QSizeF(1062.5, 300);
|
||||
QCOMPARE(sizeLabel(layer), QString("1063 × 300 px · 106.2%"));
|
||||
layer.transform.size = QSizeF(1187.5, 300);
|
||||
QCOMPARE(sizeLabel(layer), QString("1188 × 300 px · 118.8%"));
|
||||
// A blank layer has no pixels to measure against.
|
||||
QCOMPARE(sizeLabel(ImageLayer(QStringLiteral("Blank"), QSizeF(30, 20))), QString("30 × 20 px"));
|
||||
// The row shows it.
|
||||
|
||||
@@ -80,7 +80,7 @@ void OuterGlowTests::outerGlowCodableRoundTrip()
|
||||
QCOMPARE(ManifestJson::effects(encoded), original);
|
||||
// Absent `enabled` stays absent, read as shown.
|
||||
LayerEffects shown = original;
|
||||
shown.outerGlow->enabled = std::nullopt;
|
||||
shown.outerGlow.value().enabled = std::nullopt;
|
||||
QVERIFY(!ManifestJson::encoded(shown).value("outerGlow").toObject().contains("enabled"));
|
||||
QCOMPARE(ManifestJson::effects(ManifestJson::encoded(shown)), shown);
|
||||
// A key of the wrong type, or missing, refuses it.
|
||||
@@ -104,7 +104,7 @@ void OuterGlowTests::layerEffectsIntegration()
|
||||
QVERIFY(effects.color(LayerEffectKind::outerGlow) == (PaletteColor{1, 0, 0}));
|
||||
QVERIFY((effects.kinds() == std::vector{LayerEffectKind::outerGlow}));
|
||||
effects.setColor(PaletteColor{0, 1, 0}, LayerEffectKind::outerGlow);
|
||||
QVERIFY(effects.outerGlow->green == 1 && effects.outerGlow->red == 0);
|
||||
QVERIFY(effects.outerGlow.value().green == 1 && effects.outerGlow.value().red == 0);
|
||||
effects.setEnabled(false, LayerEffectKind::outerGlow);
|
||||
QVERIFY(!effects.isEnabled(LayerEffectKind::outerGlow) && !effects.visible().outerGlow);
|
||||
effects.remove(LayerEffectKind::outerGlow);
|
||||
@@ -202,7 +202,7 @@ void OuterGlowTests::theGlowWidensTheMargin()
|
||||
{
|
||||
LayerEffects effects = glowing(7.2, 1, 1, 1, 1);
|
||||
QCOMPARE(LayerEffectsRenderer::margin(effects), 24.0);
|
||||
effects.outerGlow->enabled = false;
|
||||
effects.outerGlow.value().enabled = false;
|
||||
QCOMPARE(LayerEffectsRenderer::margin(effects), 2.0);
|
||||
}
|
||||
|
||||
@@ -215,11 +215,11 @@ void OuterGlowTests::theSessionAddsCopiesAndCancelsIt()
|
||||
session.addEffect(LayerEffectKind::outerGlow);
|
||||
QCOMPARE(session.history.undoName(), QString("Add Outer Glow"));
|
||||
QVERIFY(session.activeEffects().outerGlow == OuterGlowEffect());
|
||||
session.changeEffects([](LayerEffects &effects) { effects.outerGlow->size = 44; });
|
||||
session.changeEffects([](LayerEffects &effects) { effects.outerGlow.value().size = 44; });
|
||||
session.finishEffectsEditing(false);
|
||||
QVERIFY(!session.activeEffects().outerGlow);
|
||||
session.addEffect(LayerEffectKind::outerGlow);
|
||||
session.changeEffects([](LayerEffects &effects) { effects.outerGlow->size = 44; });
|
||||
session.changeEffects([](LayerEffects &effects) { effects.outerGlow.value().size = 44; });
|
||||
session.finishEffectsEditing(true);
|
||||
session.insert(ImportedImage(square(), QImage(), QStringLiteral("Two")));
|
||||
const QUuid second = session.activeLayerID().value();
|
||||
|
||||
@@ -149,7 +149,7 @@ inline QByteArray engine(const TypeBlock &block)
|
||||
return "<<\n/EngineDict\n<<\n/Editor\n<<\n/Text " + parenthesized(block.engineText.value_or(block.text)) + "\n>>\n/ParagraphRun\n<<\n/RunArray\n[\n<<\n"
|
||||
+ "/ParagraphSheet\n<<\n/Properties\n<<\n/Justification " + block.justification + "\n>>\n>>\n>>\n]\n>>\n"
|
||||
+ "/StyleRun\n<<\n/RunArray\n[\n" + runs + "\n]\n>>\n>>\n/ResourceDict\n<<\n/FontSet\n[\n<<\n/Name " + parenthesized(block.font)
|
||||
+ "\n>>\n" + (block.secondFont ? "<<\n/Name " + parenthesized(*block.secondFont) + "\n>>\n" : QByteArray()) + "]\n>>\n>>";
|
||||
+ "\n>>\n" + (block.secondFont ? "<<\n/Name " + parenthesized(block.secondFont.value()) + "\n>>\n" : QByteArray()) + "]\n>>\n>>";
|
||||
}
|
||||
|
||||
inline QByteArray tySh(const TypeBlock &block)
|
||||
@@ -164,9 +164,9 @@ inline QByteArray tySh(const TypeBlock &block)
|
||||
items.emplace_back(block.wordsKey, textItem(block.text));
|
||||
items.emplace_back("Ornt", enumItem("Ornt", block.vertical ? "Vrtc" : "Hrzn"));
|
||||
if (block.bounds)
|
||||
items.emplace_back("bounds", rectItem(*block.bounds));
|
||||
items.emplace_back("bounds", rectItem(block.bounds.value()));
|
||||
if (block.glyphBounds)
|
||||
items.emplace_back("boundingBox", rectItem(*block.glyphBounds));
|
||||
items.emplace_back("boundingBox", rectItem(block.glyphBounds.value()));
|
||||
if (block.withEngine)
|
||||
items.emplace_back("EngineData", rawItem(engine(block)));
|
||||
descriptor(data, "TxLr", items);
|
||||
|
||||
+8
-8
@@ -128,27 +128,27 @@ inline std::vector<Channel> emptyChannels()
|
||||
|
||||
inline Prepared layer(const PSDRecord &record, bool large = false)
|
||||
{
|
||||
const int width = record.image ? record.image->width() : 0, height = record.image ? record.image->height() : 0;
|
||||
const int width = record.image ? record.image.value().width() : 0, height = record.image ? record.image.value().height() : 0;
|
||||
const int left = int(std::round(record.bounds.left())), top = int(std::round(record.bounds.top()));
|
||||
Prepared prepared;
|
||||
prepared.record = record;
|
||||
if (record.image && width > 0 && height > 0) {
|
||||
const Planes split = planes(*record.image);
|
||||
const Planes split = planes(record.image.value());
|
||||
for (const auto &[id, plane] : {std::pair<qint16, const std::vector<uchar> &>(-1, split.alpha), {0, split.red}, {1, split.green}, {2, split.blue}})
|
||||
prepared.channels.push_back({id, channelPayload(plane, width, height, large)});
|
||||
} else {
|
||||
prepared.channels = emptyChannels();
|
||||
}
|
||||
if (record.mask)
|
||||
prepared.channels.push_back({-2, channelPayload(grayPlane(*record.mask), record.mask->width(), record.mask->height(), large)});
|
||||
prepared.channels.push_back({-2, channelPayload(grayPlane(record.mask.value()), record.mask.value().width(), record.mask.value().height(), large)});
|
||||
prepared.top = top;
|
||||
prepared.left = left;
|
||||
prepared.bottom = top + height;
|
||||
prepared.right = left + width;
|
||||
prepared.maskTop = top;
|
||||
prepared.maskLeft = left;
|
||||
prepared.maskBottom = top + (record.mask ? record.mask->height() : 0);
|
||||
prepared.maskRight = left + (record.mask ? record.mask->width() : 0);
|
||||
prepared.maskBottom = top + (record.mask ? record.mask.value().height() : 0);
|
||||
prepared.maskRight = left + (record.mask ? record.mask.value().width() : 0);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
@@ -170,9 +170,9 @@ inline Prepared emptyLayer(const QString &name, const QString &blendKey, int sec
|
||||
prepared.isDivider = section == 3;
|
||||
prepared.channels = emptyChannels();
|
||||
if (record.mask) {
|
||||
prepared.channels.push_back({-2, channelPayload(grayPlane(*record.mask), record.mask->width(), record.mask->height(), large)});
|
||||
prepared.maskBottom = record.mask->height();
|
||||
prepared.maskRight = record.mask->width();
|
||||
prepared.channels.push_back({-2, channelPayload(grayPlane(record.mask.value()), record.mask.value().width(), record.mask.value().height(), large)});
|
||||
prepared.maskBottom = record.mask.value().height();
|
||||
prepared.maskRight = record.mask.value().width();
|
||||
}
|
||||
return prepared;
|
||||
}
|
||||
|
||||
@@ -78,10 +78,10 @@ inline QByteArray file(const std::vector<Record> &records, const QByteArray &res
|
||||
PSDFixture::Buffer extra;
|
||||
if (record.mask) {
|
||||
extra.u32(20);
|
||||
extra.i32(record.mask->top());
|
||||
extra.i32(record.mask->left());
|
||||
extra.i32(record.mask->top() + record.mask->height());
|
||||
extra.i32(record.mask->left() + record.mask->width());
|
||||
extra.i32(record.mask.value().top());
|
||||
extra.i32(record.mask.value().left());
|
||||
extra.i32(record.mask.value().top() + record.mask.value().height());
|
||||
extra.i32(record.mask.value().left() + record.mask.value().width());
|
||||
extra.u8(record.maskDefault);
|
||||
extra.u8(record.maskFlags);
|
||||
extra.u16(0);
|
||||
|
||||
@@ -437,15 +437,15 @@ void RasterSnapshotTests::historyBudgetNeverFlattensPaintedTiles()
|
||||
const auto raster = std::make_shared<const RasterSnapshot>(40, 30, QImage(), QRectF(), std::vector<BrushPatch>());
|
||||
DocumentHistory history;
|
||||
std::optional<CanvasDocument> document = CanvasDocument(40, 30);
|
||||
document->layers.push_back(ImageLayer(painted(raster), QPointF(0, 0)));
|
||||
QVERIFY(document->layers[0] == ImageLayer(document->layers[0]));
|
||||
document.value().layers.push_back(ImageLayer(painted(raster), QPointF(0, 0)));
|
||||
QVERIFY(document.value().layers[0] == ImageLayer(document.value().layers[0]));
|
||||
history.begin("Select Layer", document, std::nullopt);
|
||||
history.end(document, document->layers[0].id);
|
||||
history.end(document, document.value().layers[0].id);
|
||||
QCOMPARE(history.undoCount(), 0);
|
||||
QVERIFY(!raster->hasMaterializedPixels());
|
||||
history.begin("Delete Layer", document, std::nullopt);
|
||||
const qint64 thumbnailBytes = document->layers[0].asset->thumbnail.sizeInBytes();
|
||||
document->layers.clear();
|
||||
const qint64 thumbnailBytes = document.value().layers[0].asset.value().thumbnail.sizeInBytes();
|
||||
document.value().layers.clear();
|
||||
history.end(document, std::nullopt);
|
||||
QCOMPARE(history.retainedBytes(document), qint64(40 * 30 * 4) + thumbnailBytes);
|
||||
QVERIFY(!raster->hasMaterializedPixels());
|
||||
|
||||
@@ -72,10 +72,10 @@ void RawDevelopTests::theSheetsSettingsDevelopTheLayer()
|
||||
bool done = false;
|
||||
session.importImages({raw, png(folder)}, std::nullopt, [&done] { done = true; });
|
||||
QTRY_VERIFY(session.rawDevelop());
|
||||
QCOMPARE(session.rawDevelop()->path, raw.toLocalFile());
|
||||
QVERIFY(session.rawDevelop()->settings == RawImporter::asShot(raw.toLocalFile()).value());
|
||||
QCOMPARE(session.rawDevelop().value().path, raw.toLocalFile());
|
||||
QVERIFY(session.rawDevelop().value().settings == RawImporter::asShot(raw.toLocalFile()).value());
|
||||
QVERIFY(session.isImporting() && !session.document());
|
||||
RawDevelopSettings settings = session.rawDevelop()->settings;
|
||||
RawDevelopSettings settings = session.rawDevelop().value().settings;
|
||||
settings.boost = 0;
|
||||
settings.exposure = 1;
|
||||
// The sheet hears at once that it has gone.
|
||||
@@ -139,7 +139,7 @@ void RawDevelopTests::whatCannotBeDevelopedIsReported()
|
||||
QFile broken(gray.toLocalFile());
|
||||
QVERIFY(broken.open(QIODevice::WriteOnly) && broken.write("gone") == 4);
|
||||
broken.close();
|
||||
session.finishRawDevelop(session.rawDevelop()->settings);
|
||||
session.finishRawDevelop(session.rawDevelop().value().settings);
|
||||
QTRY_VERIFY(done);
|
||||
QCOMPARE(session.importError().value(), "Gray.dng: " + unreadable);
|
||||
QCOMPARE(names(session), QStringList{"Plain"});
|
||||
@@ -149,7 +149,7 @@ void RawDevelopTests::whatCannotBeDevelopedIsReported()
|
||||
const QUrl warm = dng(folder, "Warm.dng");
|
||||
session.importImages({warm}, std::nullopt, [&done] { done = true; });
|
||||
QTRY_VERIFY(session.rawDevelop());
|
||||
RawDevelopSettings candle = session.rawDevelop()->settings;
|
||||
RawDevelopSettings candle = session.rawDevelop().value().settings;
|
||||
candle.temperature = 2000;
|
||||
session.finishRawDevelop(candle);
|
||||
QTRY_VERIFY(done);
|
||||
@@ -240,7 +240,7 @@ void RawDevelopTests::theSheetPreviewsAndAnswers()
|
||||
QTest::qWait(50);
|
||||
QCOMPARE(preview->grab().toImage().pixelColor(200, 170), QColor(139, 139, 139));
|
||||
QVERIFY(!spinner->isHidden());
|
||||
RawDevelopSettings dimmer = session.rawDevelop()->settings;
|
||||
RawDevelopSettings dimmer = session.rawDevelop().value().settings;
|
||||
dimmer.exposure = -1;
|
||||
wait->start();
|
||||
QTRY_COMPARE_WITH_TIMEOUT(preview->grab().toImage().pixelColor(200, 170), centre(RawImporter::develop(path, dimmer)), 1000);
|
||||
@@ -249,7 +249,7 @@ void RawDevelopTests::theSheetPreviewsAndAnswers()
|
||||
exposure->setValue(100);
|
||||
QCOMPARE(label(sheet, "rawExposureValue"), QString("1.00 EV"));
|
||||
QVERIFY(reset->isEnabled());
|
||||
RawDevelopSettings brighter = session.rawDevelop()->settings;
|
||||
RawDevelopSettings brighter = session.rawDevelop().value().settings;
|
||||
brighter.exposure = 1;
|
||||
const QColor expected = centre(RawImporter::develop(path, brighter));
|
||||
QTRY_COMPARE(preview->grab().toImage().pixelColor(200, 170), expected);
|
||||
|
||||
@@ -75,12 +75,12 @@ void RemoveBackgroundTests::thePreviewClearsTheBackgroundAndTheCommitMasksIt()
|
||||
const FilterEdit &edit = session->filterEdit().value();
|
||||
QVERIFY(edit.previewSource.size() == QSize(400, 300) && !edit.previewError);
|
||||
const QImage preview = edit.previewImage(id).value();
|
||||
QVERIFY(preview.pixelColor(0, 0).alpha() == 0 && preview.pixelColor(200, 150) == before.asset->image().pixelColor(200, 150));
|
||||
QVERIFY(preview.pixelColor(0, 0).alpha() == 0 && preview.pixelColor(200, 150) == before.asset.value().image().pixelColor(200, 150));
|
||||
const int steps = session->history.undoCount();
|
||||
QVERIFY(committed(*session));
|
||||
// The pixels stay; a mask hides the background instead.
|
||||
const ImageLayer after = layerWith(*session, id);
|
||||
QVERIFY(after.asset->identity() == before.asset->identity() && after.transform == before.transform);
|
||||
QVERIFY(after.asset.value().identity() == before.asset.value().identity() && after.transform == before.transform);
|
||||
const LayerMask &mask = after.mask.value();
|
||||
QVERIFY(mask.isEnabled && !mask.placement && mask.isLinked);
|
||||
QVERIFY(mask.asset.image().format() == QImage::Format_Grayscale8 && mask.asset.size() == QSize(400, 300));
|
||||
|
||||
@@ -176,7 +176,7 @@ void SelectionFeatherTests::aMaskFromAFeatheredSelectionFades()
|
||||
EditorSession session;
|
||||
session.createDocument(60, 20, true);
|
||||
session.setSelection(DocumentSelection{rectPath(QRectF(20, 0, 20, 20)), true, 6}, "probe");
|
||||
session.addMask(false);
|
||||
session.addMask(true);
|
||||
const QImage mask = session.activeLayer().value().mask.value().asset.image();
|
||||
QCOMPARE(mask, session.document().value().layers.back().mask.value().asset.image());
|
||||
QVERIFY2(fading(mask, 10, 0, 1) >= 4, "the mask has a hard edge");
|
||||
|
||||
@@ -53,7 +53,11 @@ void SelectionSignalTests::selectionCallsAnnounceAndRefuseInSilence()
|
||||
QVERIFY(session->selection().has_value());
|
||||
QVERIFY(announced([&] { session->selectAll(); }));
|
||||
QVERIFY(!announced([&] { session->selectAll(); }));
|
||||
// Inverse of everything leaves nothing to deselect.
|
||||
QVERIFY(announced([&] { session->invertSelection(); }));
|
||||
QVERIFY(!session->selection().has_value());
|
||||
QVERIFY(!announced([&] { session->deselect(); }));
|
||||
QVERIFY(announced([&] { session->selectAll(); }));
|
||||
QVERIFY(announced([&] { session->deselect(); }));
|
||||
QVERIFY(announced([&] { session->toggleMarqueeKind(); }));
|
||||
QVERIFY(announced([&] { session->toggleLassoKind(); }));
|
||||
|
||||
@@ -27,10 +27,10 @@ inline QStringList described(const EditorSession &session)
|
||||
"tool error " + session.brushError().value_or(QStringLiteral("none")),
|
||||
"renaming " + text(session.renamingLayerID()),
|
||||
"adjusting " + text(session.adjustmentEditingID()) + (session.adjustmentOriginal().has_value() ? " open" : ""),
|
||||
"effects " + (session.effectsEditing() ? text(session.effectsEditing()->layerID) + " " + rawValue(session.effectsEditing()->kind) : QStringLiteral("closed"))
|
||||
+ (session.effectsEditingOriginal() ? " " + QJsonDocument(ManifestJson::encoded(*session.effectsEditingOriginal())).toJson(QJsonDocument::Compact) : QString())
|
||||
+ " chosen " + (session.effectSelection() ? text(session.effectSelection()->layerID) + " " + rawValue(session.effectSelection()->kind) : QStringLiteral("none")),
|
||||
"picker " + (session.colorPicker() ? session.colorPicker()->target.title() + " " + session.colorPicker()->color().hex() : QStringLiteral("closed")),
|
||||
"effects " + (session.effectsEditing() ? text(session.effectsEditing().value().layerID) + " " + rawValue(session.effectsEditing().value().kind) : QStringLiteral("closed"))
|
||||
+ (session.effectsEditingOriginal() ? " " + QJsonDocument(ManifestJson::encoded(session.effectsEditingOriginal().value())).toJson(QJsonDocument::Compact) : QString())
|
||||
+ " chosen " + (session.effectSelection() ? text(session.effectSelection().value().layerID) + " " + rawValue(session.effectSelection().value().kind) : QStringLiteral("none")),
|
||||
"picker " + (session.colorPicker() ? session.colorPicker().value().target.title() + " " + session.colorPicker().value().color().hex() : QStringLiteral("closed")),
|
||||
QStringLiteral("mask target %1").arg(int(session.isMaskSelected())),
|
||||
QStringLiteral("history %1%2").arg(int(session.canUndo())).arg(int(session.canRedo())),
|
||||
QStringLiteral("names %1/%2").arg(session.history.undoName(), session.history.redoName()),
|
||||
|
||||
@@ -320,7 +320,7 @@ void ShapeLayerTests::aProjectKeepsLiveShapesOnly()
|
||||
// Swift's keys; a rectangle writes no line fields.
|
||||
QCOMPARE(firstShape(snapshot, 1).keys(), (QStringList{"blue", "cornerRadius", "green", "kind", "red"}));
|
||||
QCOMPARE(firstShape(snapshot, 1).value("kind").toString(), QString("Rectangle"));
|
||||
QCOMPARE(firstShape(snapshot, 2).value("start").toArray(), (QJsonArray{line.start->x(), line.start->y()}));
|
||||
QCOMPARE(firstShape(snapshot, 2).value("start").toArray(), (QJsonArray{line.start.value().x(), line.start.value().y()}));
|
||||
QCOMPARE(firstShape(snapshot, 2).value("lineWidth").toDouble(), 3.0);
|
||||
const ProjectManifest decoded = ProjectManifest::decoded(snapshot.manifest.encoded());
|
||||
QCOMPARE(decoded.layers[1].shape, snapshot.manifest.layers[1].shape);
|
||||
|
||||
@@ -126,7 +126,7 @@ struct Oracle {
|
||||
pickUp(point);
|
||||
return;
|
||||
}
|
||||
const QPointF from = *last;
|
||||
const QPointF from = last.value();
|
||||
const double distance = std::hypot(point.x() - from.x(), point.y() - from.y());
|
||||
const double spacing = std::max(1.0, diameter * (smudging ? 0.005 : 0.025));
|
||||
if (distance < spacing)
|
||||
|
||||
@@ -125,7 +125,7 @@ void TextColorModelTests::everyChannelOfARunIsChecked()
|
||||
session->setPaletteColor(red, false);
|
||||
QVERIFY(session->finishText());
|
||||
ProjectSnapshot snapshot = session->projectSnapshot().value();
|
||||
snapshot.manifest.layers.back().text->colorRuns->front().blue = 2;
|
||||
snapshot.manifest.layers.back().text.value().colorRuns.value().front().blue = 2;
|
||||
QTemporaryDir root;
|
||||
try {
|
||||
ProjectStore::save(snapshot, root.filePath(QStringLiteral("Bad.comp")));
|
||||
|
||||
@@ -220,7 +220,7 @@ void WritingProjectsGuideTests::hsvSettingsWinOverTheLegacyFields()
|
||||
QJsonObject adjustment = layer.value(QStringLiteral("adjustment")).toObject();
|
||||
adjustment.insert(QStringLiteral("hue"), legacyHue);
|
||||
if (masterHue) {
|
||||
const QJsonObject values{{"hue", *masterHue}, {"saturation", 0}, {"lightness", 0}};
|
||||
const QJsonObject values{{"hue", masterHue.value()}, {"saturation", 0}, {"lightness", 0}};
|
||||
adjustment.insert(QStringLiteral("hsvSettings"), QJsonObject{{"range", "Master"}, {"colorize", false}, {"invertRange", false},
|
||||
{"adjustments", QJsonArray{"Master", values}}, {"bands", QJsonArray{}}});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user