AGENTS and TASKS for 13.8, 13.9a, 13.9b
This commit is contained in:
1 parent
90a8e2860e
commit
d571a090d3
2 files changed
+11
-5
No files matched your search
@@ -101,7 +101,7 @@ Platform adaptations, each forced by a missing Qt analog:
|
||||
- Filters in the session (9.4a): `Document/Filters.h/.cpp` hold Swift's model (Remove Background's kind, its four settings, `SubjectRemoval` and `commitBackgroundMask` since 9.7); `isImageAdjustment` and the list of kinds arrive with the Filter menu (9.4b), their only reader. `FilterSettings::normalized` clamps through `ImageAdjustmentPixels::clamp`, a value that is not finite taking the default. `PixelFilter::run` hands Curves, Exposure, the Gradient Map and Grain to their settings' `apply` (Grain in layer pixels, `1 / scale` units, the job's seed), the fill to `ContentFill::run`, and the rest a premultiplied copy, as Swift's contexts: Gaussian Blur is `PixelAdjust::gaussianBlur` unclamped, clear past the edge as Core Image blurs a finite image; Motion Blur is `PixelAdjust::motionBlur`, `CIMotionBlur` on the CPU: Gaussian taps (exp(−i²/2σ²) over three sigmas, normalised) along the angle at σ = distance × scale / √12, each tap read bilinearly with clear past the edge, rows in parallel, the angle counterclockwise on screen since Core Image's y points up; Add Noise and Lens Correction run Swift's kernels, `NoisePixels.c` and `LensPixels.c`, compiled in place (the lens's k is distortion / 100 × 0.35). Known difference: Swift measured Core Image's taper on a dot; the twin's is the exact Gaussian. `FilterEdit::prepare` is Swift's `prepared` and `prepare` in one: the mapping and a preview copy at most 2048 on its long side (sides truncated, one at least; whole for noise, grain and the fill; a painted layer's copy drawn from its tiles, a full-size one flattened as Levels' is). `growForBlur` pads by the blur's reach (3σ + 2, or half the streak + 2) through `grow`, 30,000 a side and one surface at most (`DocumentLimits`, 200 megapixels since 12.11a; `ProjectError(tooLarge)`), and never shrinks the grid; its rounding up is left out, since `grow` takes the integral box. The session keeps `filterSettings`, which each commit writes and each filter starts from, the Gradient Map from the palette's foreground and background instead. `updateFilter(settings, preview)` normalises, grows the grid when a blur needs more room (a failure is `brushError`; the old preview goes), renders an automatic kind once per settings (`preparedSettings`, which the worker hands back with its image), and with Preview off drops the waiting job and the preview. Known difference: one watcher serves every edit, so a job may wait behind a cancelled edit's run; that job is Swift's running task, and Preview off leaves it (dropped, it would leave `preparing` set for good). `commitFilter` closes a Gradient Map picker with OK first (`cancelFilter` with Cancel), closes as Cancel does with no step when nothing would change (no distortion, Exposure's defaults, no grain), forgets the running preview and drops the waiting job (Swift cancels the task), and hands the worker the job's parts, so a painted layer flattens there; a blur's result is trimmed to the pixels it left, any other keeps the grown grid, and an automatic kind reuses its preview when the settings match. Known difference (a fix): Swift carries a covering mask over the grown box while a trimmed blur's pixels sit on the trimmed box, so the mask slid and squeezed; the twin carries it over the layer's new box. The picker's `gradientMap` target (`ColorPickerTarget::highlights` names the end; the title follows it) opens on the open Gradient Map through `openGradientMapColorPicker`, `previewGradientMapColor` writes the working colour into the map through `updateFilter` (silent when nothing changes), and `closeColorPicker` writes OK's colour or puts the original back, the palette untouched. Left out as dead: the opener's `committing` term (a commit keeps the project busy, which `canEditPalette` reads) and `setGradientMapColor`'s (a commit closes the picker first); that function's other guards came back with 9.5c. `PixelFilter::trimmed` throws `ExportError(render)` when its crop finds no memory, since Swift's `cropping` shares the pixels and cannot fail so; the blurs' row lists (`rowNumbers`) turn `bad_alloc` into `ExportError(render)`, as Hue/Saturation's do, and their weight tables stay plain, the callers bounding them to 1,734 entries. Tests: `FilterTests` (Swift's four; the Gaussian one tests the code, which grows and trims, as Swift's has since 1.2.0, `7c4512a`), `FilterModelTests` (Add Noise and the lens checked against Swift's kernels, whose headers only that test includes), `FilterSessionTests`, `FilterFailureTests` (a binary of its own, under `AddressSpaceLimit`), and in `ImageAdjustmentTests` Swift's two Image-menu tests and the command half of `curvesIdentityAndImageCommandPreserveAlpha` (Swift's `AdjustmentLayerTests` holds it). Equivalent mutant: `>=` in `growForBlur`'s guard (at equal reach it grows the same grid again).
|
||||
- The filter sheet (9.4b): `UI/FilterSheet.h/.cpp` twin Swift's file, `FilterSheet` and `GradientMapControls`. The sheet is the content of a `FloatingPanel` named `filterPanel`, which `ContentView::synchronizePanels` shows with the edit, titled by its kind, and closes with it, its close button and Escape cancelling (Swift's `onChange(of: filterEdit == nil)`). The kind's controls are made once, since an edit's kind never changes. Swift's `control` is a title (sixty points at least, the slider's buddy), a slider over a thousand steps of its range, or of the range's logarithm where Swift's is logarithmic, the value rounded to the field's decimals as Swift's binding rounds it, and a 56-point `PickerField` with its unit two points after (Swift's `unitSuffix`), which applies a readable finite number on Return or on leaving (normalizing clamps it), gives up its typing when its own slider changes the value (a wheel leaves the keys with the field; a notch that rounds to the same value changes nothing; 9.6f), and shows Swift's `.fractionLength(0...decimals)`: at most that many decimals, none trailing, in the field's locale, digits never grouped, as in every field of the twin. The sliders show the session's number under a `QSignalBlocker`, so a dragged thumb sits at the rounded value, as Swift's binding shows it; a step that rounds to the setting it already holds changes nothing, so the thumb keeps its place and arrow steps add up. Add Noise's distribution is two checkable buttons in a `QButtonGroup` beside its label (Swift's segmented picker), Monochromatic a `QCheckBox`; Lens Correction's note is callout-sized in `PlaceholderText`; Curves hosts `CurvesControls` through the settings. `GradientMapControls` draws Swift's gradient bar (20 high, rounded 4, a 35% black rim inside), the ends as the palette's `SwatchButton`s (24 points, radius 6, a white ring of 1.5 inside a black rim) with their titles, help and accessible names, and Reverse; a swatch opens the picker on its end, and on every change of the picker's colour the sheet calls `previewGradientMapColor` (Swift's `onChange`), which ignores a palette picker. The controls wait for the sheet's first synchronize, which runs once they are built. `settings` and `update` keep Swift's fallbacks for a sheet whose edit has gone: the defaults, Preview on. The Image menu gains Curves… (Ctrl+M) before Levels…, and Exposure…, Gradient Map… and Grain… after Hue/Saturation…; the Filter menu (`Fil&ter`, between Image and Layer, as Swift declares it) lists `allFilterKinds` but the fill and the image adjustments (`isImageAdjustment`), Remove Background among them since 9.7; every entry rests while colours cannot be adjusted or Hue/Saturation is open, as Swift's. Tests: `FilterSheetTests` (7.5's, moved to the panel), `FilterControlsTests` (each kind's controls, the sliders' travel, the fields, the Gradient Map's swatches, bar and repaint), the menus' in `SelectMenuTests` and `CompositorMenusTests`. Equivalent mutants: Swift's `?? .gaussianBlur` for the kind (the panel makes a sheet only for an open edit) and the memory of the picker's colour (`previewGradientMapColor` with an unchanged colour changes nothing).
|
||||
- Adjustment layers in the document and the store (9.5a): `Document/LayerAdjustment.h/.cpp` twin Swift's file, `LayerAdjustment+Session.h` its session half. `AdjustmentKind` keeps Swift's raw values (`rawValue`, and `adjustmentKind(text)` for the manifest) and case order (`allAdjustmentKinds`); `filterKind` came with 9.5c, its reader; `symbol` names an SF Symbol, whose glyph the panel draws (9.5d). `LayerAdjustment` keeps Swift's fields: the legacy Hue/Saturation four, the optional `hsvSettings`, Levels and Curves, and the optional Exposure, Gradient Map and Grain, whose computed properties are getter and setter pairs; `resolvedHSV` falls back on the legacy four. `isValid` keeps Swift's bounds (hue within 360 either way, the rest within 100, each range's values alike, every band's handles finite, Levels as normalized, then every kind's own rules whatever the kind) and leaves out the `isFinite` terms the bounds already refuse and Levels' count (the twin holds four ranges). `apply` arrives with 9.5b, its reader. `ImageLayer.adjustment` joins equality and travels wherever Swift lists it: the record (`hierarchyRecord`, so the snapshot), `ImageLayer(record, …)` (so an install and a resize's rebuild), both resizers and duplicates. A copy between projects needs no term of its own: the baker bakes nothing for a layer without pixels, and the placement unlinks it, where Swift skips the bake. The manifest's `adjustment` is Swift's synthesized `Codable`, written by hand in `IO/ProjectStore+Adjustment.cpp` over the readers the manifest shares (`ManifestJson` in `IO/ProjectStore+Json.h`): every key required but the four optionals, which stay out when absent; a `ColorRange`-keyed dictionary is an array of key and value in turn, since Swift's `ColorRange` declares no `CodingKeyRepresentable` (a range Swift does not name refuses it, and so does an odd count, whose last key has no value to read, so Swift's count check has no twin; a later pair wins, as a dictionary assigns); the seed is a `UInt32`. The store refuses an adjustment before version 7, on a folder, with pixels or invalid; Swift's text rule's adjustment term is dead, since text needs pixels. `LiveMaskGraph::validate` refuses clipping to an adjustment layer, so `canLinkMask` refuses one as a source; `canPaint` refuses an adjustment layer's pixels, not its mask. `addAdjustment` puts a blank layer named by its kind just above the active one (into an active folder, uncollapsed; Swift's `addBlankLayer` alone lifts a layer above the folder's contents), as one `New … Adjustment` step, a Gradient Map running from the foreground to the background and each Grain with a seed of its own, 10,000 layers at most, then asks for its editor (Swift's `adjustmentEditingID`, 9.5c). `updateAdjustment` writes valid settings alone, with no step of its own, and moves `brushRevision`. Tests: `LayerAdjustmentTests` (kinds, validity, the optionals, the manifest's keys and refusals, the store's and the graph's rules, adding, updating, travel), `AdjustmentLayerTests` (Swift's `legacyHSVStillDecodesAndRenders`; the rest arrive with 9.5b and 9.5c), Swift's `settingsSaveAndOlderAdjustmentsStillOpen` in `ImageAdjustmentTests`, and an unbaked copy in `ProjectCopyTests`.
|
||||
- Adjustment layers drawn (9.5b): `LayerAdjustment::apply` runs the kind's own filter (Hue/Saturation and Levels through their filters, Curves, Exposure and the Gradient Map through their settings), Grain placed by the region it is given: its origin and its units a pixel, `region.width / image.width`, so the export and `drawLiveComposite` keep the pattern in the document however their drawing is cut or scaled. Known difference, Swift's too: the canvas places layers by scale and centre, not through the painter's transform, so its region is the view's and Grain's pattern follows the view, moving under a pan and differing from the export (`grainFollowsTheViewOnTheCanvas`). `LiveMaskRenderer` gains Swift's three hooks as public members: `adjustment`, `adjustmentOpacity` and `adjustmentClip`, which multiplies the layer's own mask into device coverage where Swift clips the context. `prepareStacks` keeps every layer's mode (Swift's `blendMode`); Swift's `adjustment(base) == nil` there is dead and left out, since nothing clips to an adjustment (`LiveMaskGraph`). `drawComposite` hands an unclipped adjustment to `adjust` and draws a clipped one only inside its base's stack, where `adjust` runs on the group after its unpremultiplied base, as Swift's. `adjust` reads the painter's device (a `QImage`, as `LayerRenderer` requires; else a `logic_error`), converted to the kernels' `RGBA8888_Premultiplied` at a pixel ratio of one, within the pixel budget (past it, a warning on `omaphoto.rendering`, where Swift's `try?` is silent), applies the settings over the region the device shows (its device transform inverted), blends a mode other than normal at full coverage and restores the original alpha (`layer_extract_alpha`, `layer_unpremultiply_opaque`, `LayerRenderer::composite` for the mode, `layer_restore_alpha`), then mixes the result into the original by opacity × mask × outer clip, rounded once, where Swift mixes by opacity (`CIBlendWithMask`) and then draws through the soft clip, and draws it back one to one in `Source` mode under the painter's own clip, as Swift's copy honours the context's clip, the painter's whole device transform cancelled as the stack group cancels it, so an image of any format or pixel ratio takes it (the app hands it only `RGBA8888_Premultiplied` at a ratio of one: the export's image, a stack's group, `AdjustmentSurface`'s surface; a caller of the renderer itself may hand it any image). A failure to allocate is `ExportError`, caught there as a warning; anything else is a bug and is left to crash. `ImageExporter::render`, `drawLiveComposite` and the canvas set the hooks as Swift's three places do; `drawLiveComposite` gains Swift's `onSurface` and, when a document holds an adjustment, draws through `AdjustmentSurface`, so a painter's opacity or clip applies to the composite once. The canvas clips an adjustment by its enabled mask, mid-stroke by the edit's tiles (`maskEdit`, shared with the folders' clip, Swift's `liveMaskEdit`), placed by the layer's transform at the view's scale; its `DisplayState` counts adjustment layers with their settings, since an undo restores settings without moving `brushRevision`. Tests: Swift's `AdjustmentLayerTests` (the curves command test moved there from `ImageAdjustmentTests`, its Swift home), `theLiveCompositeDrawsAsTheExport`, `opacityMixesAndGrainSitsInTheDocument`, `everyKindAdjustsThroughItsSettings` and `aFoldersMaskClipsTheAdjustmentsInside` beside them, `anyImageTargetTakesTheAdjustment` beside them, `anAdjustmentAdjustsWithinItsBudgetAndStack` and `anAdjustmentTakesAnyImageTarget` in `LiveMaskRendererTests`, `adjustmentsShowAsTheExportDoes`, `anAdjustmentsMaskClipsItOnTheCanvas` and `grainFollowsTheViewOnTheCanvas` in `CanvasDisplayTests`.
|
||||
- Adjustment layers drawn (9.5b): `LayerAdjustment::apply` runs the kind's own filter (Hue/Saturation and Levels through their filters, Curves, Exposure and the Gradient Map through their settings), Grain placed by the region it is given: its origin and its units a pixel, `region.width / image.width`, so the export and `drawLiveComposite` keep the pattern in the document however their drawing is cut or scaled. `LiveMaskRenderer` gains Swift's three hooks as public members: `adjustment`, `adjustmentOpacity` and `adjustmentClip`, which multiplies the layer's own mask into device coverage where Swift clips the context. `prepareStacks` keeps every layer's mode (Swift's `blendMode`); Swift's `adjustment(base) == nil` there is dead and left out, since nothing clips to an adjustment (`LiveMaskGraph`). `drawComposite` hands an unclipped adjustment to `adjust` and draws a clipped one only inside its base's stack, where `adjust` runs on the group after its unpremultiplied base, as Swift's. `adjust` reads the painter's device (a `QImage`, as `LayerRenderer` requires; else a `logic_error`), converted to the kernels' `RGBA8888_Premultiplied` at a pixel ratio of one, within the pixel budget (past it, a warning on `omaphoto.rendering`, where Swift's `try?` is silent), applies the settings over the region the device shows (its device transform inverted), blends a mode other than normal at full coverage and restores the original alpha (`layer_extract_alpha`, `layer_unpremultiply_opaque`, `LayerRenderer::composite` for the mode, `layer_restore_alpha`), then mixes the result into the original by opacity × mask × outer clip, rounded once, where Swift mixes by opacity (`CIBlendWithMask`) and then draws through the soft clip, and draws it back one to one in `Source` mode under the painter's own clip, as Swift's copy honours the context's clip, the painter's whole device transform cancelled as the stack group cancels it, so an image of any format or pixel ratio takes it (the app hands it only `RGBA8888_Premultiplied` at a ratio of one: the export's image, a stack's group, `AdjustmentSurface`'s surface; a caller of the renderer itself may hand it any image). A failure to allocate is `ExportError`, caught there as a warning; anything else is a bug and is left to crash. `ImageExporter::render`, `drawLiveComposite` and the canvas set the hooks as Swift's three places do; `drawLiveComposite` gains Swift's `onSurface` and, when a document holds an adjustment, draws through `AdjustmentSurface`, so a painter's opacity or clip applies to the composite once. The canvas clips an adjustment by its enabled mask, mid-stroke by the edit's tiles (`maskEdit`, shared with the folders' clip, Swift's `liveMaskEdit`), placed by the layer's transform at the view's scale; its `DisplayState` counts adjustment layers with their settings, since an undo restores settings without moving `brushRevision`. Tests: Swift's `AdjustmentLayerTests` (the curves command test moved there from `ImageAdjustmentTests`, its Swift home), `theLiveCompositeDrawsAsTheExport`, `opacityMixesAndGrainSitsInTheDocument`, `everyKindAdjustsThroughItsSettings` and `aFoldersMaskClipsTheAdjustmentsInside` beside them, `anyImageTargetTakesTheAdjustment` beside them, `anAdjustmentAdjustsWithinItsBudgetAndStack` and `anAdjustmentTakesAnyImageTarget` in `LiveMaskRendererTests`, `adjustmentsShowAsTheExportDoes` and `anAdjustmentsMaskClipsItOnTheCanvas` in `CanvasDisplayTests`.
|
||||
- Editing adjustment layers in the session (9.5c): `Document/AdjustmentEditing.cpp` twins Swift's file, `AdjustmentEditing+Session.h` its session half; `adjustmentEditingID` (a getter and a setter that resumes file requests and notifies, Swift's `didSet`) and `adjustmentOriginal` (a getter) sit in `EditorSession.h`. `addAdjustment` asks for the new layer's editor last, as Swift's. `beginAdjustmentEditing` takes a completion callback: it hides every layer not beneath the adjustment in hierarchy order (the adjustment itself and all above; the records stay for live masks and folders), renders that snapshot through `ImageExporter::render` in a worker (`QtConcurrent::run` with a watcher per call, as each Swift call is a task of its own; the worker catches `ExportError` alone, since a session's snapshot never fails the store's rules), and on the UI thread opens the kind's editor on a layer of that image at the origin: Levels with its histogram (`countLevelsHistogram`, which `beginLevels` shares), Hue/Saturation with `resolvedHSV`, or the filter panel of `filterKind` (declared in `LayerAdjustment.h`, as Swift's extension) with the four settings; then it keeps the original and opens one `Edit … Adjustment` transaction. The editors' layer has an id of its own, so the canvas never shows their previews, and none renders: `updateLevels`, `updateHueSaturation` and `updateFilter` hand the settings to `previewAdjustmentEditing` first (the edited settings, or the original with Preview off) and notify, since settings the layer refuses still change the editor; the three cancels and commits hand the edit to `finishAdjustmentEditing` first (a filter's after its Gradient Map picker closes, as in Swift). `finishAdjustmentEditing` writes the edited settings on OK and the original on Cancel, closes the three edits and the Hue tools (sample mode, targeting, a targeting drag), ends the transaction, clears the request, resumes file requests, asks for canvas focus and moves `brushRevision`. OK writes the settings out even unchanged, so an Exposure or Hue/Saturation layer made without them gains them in a step, as in Swift. A Gradient Map picker stays open when `finishAdjustmentEditing` is called directly (Swift's tests call it; the app's cancel and commit close the picker first), so `setGradientMapColor` keeps Swift's guards: with no filter open, or another kind, it writes nothing; its `committing` term is dead, since a commit closes the picker before it begins. Left out as dead: Swift's `editedAdjustment` returns nil when its kind's edit is gone, which never happens while `adjustmentOriginal` is set (the two are set and cleared together, and every path that closes an edit reaches `finishAdjustmentEditing` first), so the twin reads the edit through `value()`; the `?? .curves` behind `filterKind`, a checked read; `finishAdjustmentEditing`'s cancellation of previews (none runs for an adjustment's editor, and a histogram lands only on a Levels edit still open) and its `hueSaturationPending = nil` (only a preview sets it). Swift's `.task(id:)` cancels a render whose request changed; the landing's guard (the same request, no editor open) does that work here, and nothing in the app changes the request while a render runs (every writer sits behind `canEditLayers`). A fix beyond Swift: Swift's landing checks the request alone, so a Levels, Hue/Saturation or filter edit begun on other pixels while the render ran (choosing a layer and opening one stay possible, as in Swift) would be replaced or joined by the adjustment's editor; the twin's landing leaves that edit alone, clears the request, resumes file requests and logs a warning on `omaphoto.app`. A render or an editor that fails clears the request and sets `brushError`, as Swift's `catch`. `canEditLayers` and `canStartProjectOperation` read the request; `canUseHistory` does not, as in Swift, so undo stays open while the render runs, and an editor may open for a layer undone meanwhile, whose close writes nothing. Swift's `isGroup != true` there is left out as dead: a folder not beneath the adjustment holds nothing that is, and a live mask's source is never a folder. Equivalent mutants: the start guard's request term (the landing asks again; the start's only spares the render) and its `adjustmentOriginal` term (the original never outlives its kind's edit, which the next terms read). Tests: `AdjustmentEditorTests` (Swift's `sharedEditorsKeepPixelsDynamicAndSupportCancel` for every kind, then what lies beneath, the refusals, a failed render, an edit begun meanwhile, the gates and file requests, the Hue tools, a layer undone meanwhile, the signals), Swift's `newAdjustmentLayersStartFromThePaletteRenderAndEditInThePanel` in `ImageAdjustmentTests`, the request as a gate in `SessionGateTests`; tests that add an adjustment and edit on clear the request, as Swift's `add` does.
|
||||
- Editing adjustment layers in the window (9.5d): `LayersPanel` twins Swift's `.task(id: adjustmentEditingID)`: on every `changed()` (and when made, as a task runs on appear) it compares the request with the one it last began (`m_adjustmentTask`) and begins a new one; a render already under way for the same request lands nothing, so nothing needs cancelling. The footer's adjustment button is a `QToolButton` whose menu (Swift's `Menu`, `InstantPopup`) lists the six kinds by name and calls `addAdjustment`, resting while `canEditLayers` does not hold. Its glyph and the rows' are `LayerIcons` paths for Swift's SF Symbols; `symbol` names each kind's (circle.lefthalf.filled, slider.horizontal.3, the curve path turned a quarter clockwise as Swift turns it, plusminus.circle, paintpalette, circle.grid.3x3), and `adjustmentThumbnail` draws it 1.2 times the rail's size, centred in the row's 36-point square, as the text glyph is. An adjustment's row shows that glyph, "Adjustment · Double-click to edit", and no link for its mask (Swift's `linkable`); a double click on a control makes it active and asks for its editor (Swift's `renameClickedLayer`), one on the name renames. The Layer menu opens with Swift's New Adjustment Layer submenu (the kinds with an ellipsis, named `new…Adjustment`, resting while `canEditLayers` does not hold) and Edit Adjustment… (the active layer's request, for an adjustment layer alone), then a separator. The Levels sheet's caption says "Underlying pixels · alpha-weighted histogram" for an adjustment's editor, and the filter and Hue/Saturation sheets hide "Limited to the selection" there, as Swift's `adjustmentOriginal` terms. Tests: Swift's `adjustmentEditorsUseMovableNonmodalPanels` for every kind in `FloatingPanelTests`; `AdjustmentPanelTests` (the footer and its request, the rows and their glyphs, the Layer menu, the sheets' words); the Layer menu's count and names in `CompositorMenusTests`.
|
||||
- Layer effects in the document and the session (9.6a): `Document/LayerEffects.h/.cpp` twin Swift's model: the four effects with Swift's defaults, `LayerEffectKind` by raw value in Swift's order (`allLayerEffectKinds`), `LayerEffectSelection`, `LayerEffects` with its helpers. Swift's file runs to 544 lines, so its session extension is `LayerEffects+Session.cpp` with `LayerEffects+Session.h`, and its renderer arrives with 9.6b. `isValid` keeps Swift's bounds and leaves out the `isFinite` terms the bounds already refuse; `offset` falls away from the light, y down. The manifest's `effects` is Swift's synthesized `Codable`, written by hand in `IO/ProjectStore+Effects.cpp`: every key required but `enabled`, which stays out while absent; the store holds no rule for effects, as Swift's. Swift 1.1 gave the record an `effects` key but its `projectSnapshot` and `installProject` never carried it, so its saves and exports dropped the effects; Swift agrees with the twin since 1.2.0 (`5775dd1`, `6230e41`). The twin's record (`hierarchyRecord`) carries them and `ImageLayer(record, …)` loads them, so they save, load and, with 9.6b, export. Elsewhere they travel as Swift lists them: equality, a duplicate and the brush's two commits keep them; Invert's, Levels' and Hue/Saturation's commits, a floating merge and both resizers leave them out (the filter's keeps them since 12.9a); a copy between projects keeps them since 12.10b, as Swift 1.2.5's list does. The session's `effectsEditing`, `effectsEditingOriginal` and `effectSelection` are getters; 9.6d gave the first two setters, which notify on a change, for `ContentView`'s check, and made `dropEffectSelection` public for the canvas's press (the list's arrive with 9.6e). `selectLayer`, `selectLayers` and `selectLayerTarget` drop the chosen effect before any guard, as Swift's, so a refusal still drops it and says so (`dropEffectSelection`); Delete and the trash take a chosen effect first. `setEffects` finishes an opacity drag and makes one step of a valid change; `changeEffects` stays with the panel's layer, a step per change as Swift's; Cancel puts back the panel's effect alone. The colour picker gains Swift's effect target (`openEffectColorPicker`, `previewEffectColor`, `closeColorPicker`'s effect case, titles such as "Color Picker (Stroke Color)"); `ColorPalette.h` declares `LayerEffectKind` opaquely, since `LayerEffects.h` includes it. Left out as dead: the `isGroup` terms of `canEditEffects`, `setEffects` and `canCopyEffect` (a folder never holds pixels, `LayerHierarchy::validate` sees to it, so `asset` says the same), `selectEffect`'s `selectedLayerIDs = [id]` (`setActiveLayerID` sets it, and `selectLayer` cannot refuse behind `canEditLayers`) and its effect picker close (every path that clears `effectsEditing` closes that picker first, `ContentView`'s check since 9.6d included). Swift has no effects tests: `LayerEffectsTests` and `LayerEffectsSessionTests` pin its code, and `ProjectCopyTests` the copy.
|
||||
@@ -133,7 +133,7 @@ Platform adaptations, each forced by a missing Qt analog:
|
||||
- The Camera Raw panel (12.8b1, Swift 1.2.3): Swift docks the filter's `NSPanel` to the document window's right edge, full height, following it (`dockedToMainWindowRight`). Wayland lets no client place a window, and a tiling compositor would tile a second one, so the twin docks inside the window: `FloatingPanel::show` takes a dock slot, `ContentView`'s `panelDock` at the right of the middle row, `FloatingPanel::dockedWidth` (Swift's 440) wide, and there the panel is its own `PanelWindow` made a plain child (`Qt::Widget`), so `QDialog` still takes Escape as Cancel and Return as the default button; a header holds the title and a close button, which cancels. A docked frame is never remembered (`moveEvent` asks `isWindow`), so Gaussian Blur and the rest reopen where they were left, Swift's `dockedPlacementLeavesTheSavedFilterPosition`; `refocus` hands a docked panel the keys through `setFocus`, looking past the hidden floating panel of the same name, which is the window's child too. The dock takes room beside the layers panel and spans the middle row alone, so the tool bar and status bar keep their width, where Swift's panel lies over the window's whole height (known difference). Refocus hands the keys to the docked panel itself, not the field last typed in (known difference: `makeKey` keeps its first responder). `UI/CameraRawSlider` twins Swift's file: `CameraRawSliderTrack` with Swift's colours, the gradient drawn 4 points high across the groove under the style's knob, a double click on the knob (its rect two points out) resetting; a press on the track is `SliderSnap`'s (10.5): Swift's 0.18 s glide has no twin, and a drag goes on from the snapped knob, as every slider of the twin does (known differences); `display` keeps a knob under a drag, Swift's `isTrackingValue`; Camera Raw's own rows keep the track they are made with, since the rows whose tracks change (12.8b2's mixer) are made anew; since 12.14c `reshape` is Swift's track and bounds update in `updateNSView`, for the filter and Hue/Saturation sheets. `UI/CameraRawControls.h/.cpp` and `CameraRawControls+Basic.cpp` (Light, Color, Effects) twin Swift's view: the scope 110 high on black at 35%, the three ribbons at 55% in SwiftUI's dark red, green and blue (the Levels histogram's values) or the vectorscope's cells, square by the width and counted up from the bottom, so a 110-point graph shows its lowest 18 rows, as Swift's does, its context menu choosing the mode, the two clipping triangles lit in that blue and red, the readout; the words say Alt for Option and a right click for Control-click; the ten groups, Light and Color open, each eye shown while its group adjusts; a row's title (96 points at least, a double click resetting), `CameraRawSlider` and a 56-point `PickerField` showing Swift's `.number` to the row's decimals. A row's object name is its settings field (`vignetteAmountSlider`), since titles repeat. Alt is read from `QGuiApplication::keyboardModifiers`, Swift's `NSEvent.modifierFlags`, and its release (auto-repeat aside), from any window, ends the clipping and sharpen-mask views (Swift's local monitor). Curve to Calibration fill their groups in 12.8b2, and the Filter menu's entry waits for them. On the canvas (`EditorCanvas+CameraRaw.cpp`) a hover reads the readout (Swift's `mouseMoved`, never a drag), leaving clears it, the three eyedroppers sample and hand the panel the keys, the guide and the targeted drag begin, go on and end on release, and a move without the left button ends either, since Qt can lose a release; the four tools are `picking`, so the eyedropper shows. `EyedropperIcon::icon` came from `LevelsSheet`, which shares it. Left out as dead: the canvas's `kind == cameraRaw` and edit terms before the readout (the session asks), a hide of the other placement's panel in `show` (every edit closes its panel first), Swift's clipping reset before a row's reset (`assign` clears it), and the docked panel's own hide on close (its slot hides it). Equivalent mutants: the titles' minimum width, 96 points (a layout sizes labels offscreen alike), and the unlit triangle's 55% white (cosmetic, over whatever the scope shows). Two consequences of the named differences: a double click on a slider's track resets it, since the first press puts the knob under the second; and a canvas press takes the keys, so a field still being typed in commits at the click, where Swift's panel keeps its field editor. Tests: `CameraRawSliderTests` (Swift's three), `CameraRawPanelTests`, `CameraRawCanvasTests` (the dock in a real window among them), `FloatingPanelTests::dockedPlacementLeavesTheSavedFilterPosition`.
|
||||
- The Camera Raw panel's other groups (12.8b2, Swift 1.2.3): `UI/CameraRawColorControls.h/.cpp` (Curve), `+Mixer.cpp` and `+Grading.cpp`, `UI/CameraRawDetailOpticsControls` and `UI/CameraRawGeometryCalibrationControls` twin Swift's three files, each view a `QWidget` over the session, synchronized on `changed()`. `UI/CameraRawRow` is Linux-only: the row each Swift view builds for itself (`sharpenSlider`, `opticsSlider`, `geometrySlider`, `calibrationSlider`, the curve's and mixer's sliders, 12.8b1's `slider`): a title of at least or exactly a width, `CameraRawSlider`, a field or none, a double click on the title resetting where Swift's main groups do; each view rounds the slider's value as its Swift twin does (Detail to whole numbers, Optics when its range starts at zero, Geometry and Calibration always, the rest not). Swift's segmented pickers are `cameraRawSegments`, touching checkable buttons in an exclusive `QButtonGroup`. The curve graph: a still press and release adds a point, a drag past two points moves the nearest divider or inner point, a double click removes the nearest inner point within 0.04 and its trailing release adds nothing, so the net is Swift's: SwiftUI adds on each click, `repair` drops the second as a duplicate and the double tap removes one. The graph is `CameraRawCurveGraph`: a class defined in a `.cpp` outside an anonymous namespace is global, and the Curves sheet's `CurveGraph` would share its vtable across the link. The mixer's rows whose help or track follow its tab, swatch or chosen point are made anew when those change, as SwiftUI renders them anew; a picked colour's swatch is shown at once, since a layout shows late children only later. The grading wheel fills a `QConicalGradient`, which runs counterclockwise from the right as the hue does (1.2.9's `96edd5f`), takes presses and double clicks inside its circle alone (Swift's `contentShape`), and Swift's `theDockedPanelFitsTheGradingWheels` holds; its stops are 96edd5f's own, pure HSV hues every 30 degrees (v1.2.3's six SwiftUI system colours predate that fix). Detail's idle noise rows are disabled under a 45% `QGraphicsOpacityEffect`, Swift's `.opacity(0.45).disabled`. Targeted Adjustment and Draw Guides are checkable push buttons, checked while armed, where Swift tints them (known difference); their SF Symbols, `scope` and `line.diagonal`, are `LayerIcons` paths. The Filter menu lists Camera Raw Filter… after Lens Correction, as Swift's. Known differences: a mixer family's field still being typed in loses its typing when the tab changes (the row is made anew), where SwiftUI's field would commit into the new tab; Equivalent mutants: the wheel's saturation cap (`normalized` clamps it), the picked swatches made anew only when their count changes (it spares rebuilds; Qt guards a swatch deleted in its own click) and the page menu's size policy (layout). The point-colour eyedropper is flat like the other two, where Swift leaves it bordered (cosmetic). Cosmetic, Swift's values and unpinned: the swatches' 18 points, Detail's idle 45%, the hue ranges' inset (title width and 10), the rows' spacing of 10 and the 11-pixel subheadlines. Tests: `CameraRawRowTable.h` walked by `CameraRawGroupRowsTests` (every row against Swift's values), `CameraRawGroupDrawTests` (drawing, pages, gestures, the panel through `FilterSheet`), `CameraRawGroupWordsTests` (every text, captions, buddies, the pickers' states, the mixer's pages, the wheel's lower half and rim, the curve's line, repaint and selected point), `CameraRawCurveMixerTests`, `CameraRawGradingLensTests`, the menu's entries in `CompositorMenusTests`.
|
||||
- Hash containers on the render path (12.8e): Qt 6.4 declares `QHashPrivate::Data::findOrInsert` `noexcept` while it allocates, so a `QHash` or `QSet` insert that finds no memory calls `std::terminate`, past every catch (found by a backtrace of `ProjectExportFailureTests` under load: the JPEG worker's render aborted in `LiveMaskGraph::validate`). Code a worker runs under a catch that promises to survive memory running out uses `std::map` and `std::set`, whose inserts throw: `LiveMaskGraph::validate`, `FolderMaskClip::draw` and `LiveMaskRenderer`'s caches (and `LiveMaskBaker::bake` beside `validate`, though its workers catch `ExportError` alone, so a `bad_alloc` there still ends the app, by design), as `ImageExporter::render` already keyed its records. The UI thread's own `QHash`es stay: a failure there ends in Qt's event loop either way. `RenderStarvationTests` replaces `operator new` on its thread and fails each allocation of `validate`, `prepareStacks` and `FolderMaskClip::draw` in turn: each must throw `std::bad_alloc`; the three aborted with the old containers. Painting is left out of it: Qt's painter is not exception-safe, and a failure injected inside it can leave a lock held.
|
||||
- Blur and noise adjustment layers, Image Trim and Inner Glow (12.8c, Swift 1.2.3: `98ad817`, `24052b5`, `feb39f3`, `cd0ca3c`). `AdjustmentKind` gains Add Noise, Gaussian Blur and Motion Blur after Grain, v1.2.3's order (it moved Add Noise ahead of the blurs), with `circle.dotted`, `drop.fill` and `wind` as `LayerIcons` paths. `LayerAdjustment` keeps Swift's seven optionals (`blurRadius`, `motionAngle`, `motionDistance`, `noiseAmount`, `noiseGaussian`, `noiseMonochromatic`, `noiseSeed`), their resolved getters and setters with Swift's defaults (10, 0, 10, 10, off, off, 0), `isValid`'s bounds on every kind, as Swift's, and `samplingMargin`; the manifest writes each only when set, the seed a `UInt32` read as Grain's; the store takes the three kinds from version 9, which it now writes (versions 1–9). `apply` runs the three through `PixelFilter::run` with the layer's own seed; `FilterJob::noiseOrigin` sits last, so the positional initialisers keep their order, and `kernels/NoisePixels.c` is v1.2.3's (`noise_add_at`, whose position hash reshuffles the Add Noise filter's grain too). A new Add Noise layer rolls its seed, as Grain does; the editor opens the kind's filter panel on the layer's settings and writes back that kind's alone. Adaptation: Swift's canvas surface holds a pixel a point, so its region's origin and `scale` are already the surface's pixels; the twin's surfaces hold device pixels, so `LiveMaskRenderer::adjust` passes `adjustmentScale` (the canvas's zoom, Swift's, 1 elsewhere) times the device's pixels a region unit, and `apply` counts the noise origin in the image's pixels, so a HiDPI canvas blurs at its own density and the noise stays one field across dirty rects. `AdjustmentSurface::draw` takes Swift's `padding` in the painter's units: the halo is allocated past the shown area, not cut to the device (the canvas's outer surface is dirty-sized), and drawn back under the painter's clip. The canvas pads by the widest visible `samplingMargin` times the zoom, and its crisp path (from 200%) now draws the layers through a padded surface as well, as Swift's `drawLayers` wraps its own there. Known difference, Swift's too: on the smooth path the canvas's noise follows the view, as Grain's does; on the crisp path the surface's region starts at the repaint region, so each partial repaint there draws a noise field of its own. The crisp path wraps its surface even without an adjustment, where Swift's `drawLayers` wraps only for one: pixel-identical (source-over onto a cleared raster), one allocation a repaint region, as the smooth path already does. The HiDPI product is pinned by its two factors (the zoom on the canvas, the device ratio on a scaled composite), not at a device ratio of two itself. Image Trim: `Document/ImageTrim` twins Swift's file: `TrimBasedOn` by raw value, `TrimOptions` (a tolerance per premultiplied channel, alpha included, which the sheet leaves at 0), `calculateTrimRect` (the alpha bounds, or each row scanned in from both ends against the corner's pixel) and `trim` (the export's render, then `CanvasResizer` by the content offset, or the snapshot itself when nothing is cut). `UI/TrimSheet` is Swift's sheet: 320 wide, the title, Based On as radio buttons, Trim Away's four checkboxes, OK resting while none is checked, Return and Escape through `NativeShortcut`. `ProjectController::trim` runs as Canvas Size does, through `resizeProject`, whose resize may now return nothing: Swift's trim with nothing left returns with no alert and no step. The Image menu's Trim… follows Image Size…, with no shortcut. Left out as dead: `TrimError` (never thrown), `trimImage` and the session's `trim` (called by Swift's tests alone; the twin tests the rect and the controller's two steps), the kept rect's `maxX > minX` guard (the early returns leave content, so left < right and top < bottom) and the colour scan's `last > first` (the row's first unmatched pixel stops it). A fix beyond Swift: a raster that cannot be converted is `ExportError(render)`, explained, where Swift's `try?` reports nothing left; that failure path is untested, since only a failed allocation reaches it. Inner Glow joins as Outer Glow did (12.5b): `InnerGlowEffect` (10 pixels, white, 75%; 0–500, 0–1), `LayerEffectKind::innerGlow` last; `LayerEffectsKernel` blurs the shape at sigma size ÷ 2 and keeps shape × (1 − blur), composed over the colour overlay and under the inner shadow, Metal's order; no margin (Swift's `innerGlowRendersInsideSourceWithoutBoundsExpansion`), the stroke surface's reach three sizes and two. Swift's CPU `innerGlowCoverage` is dead here, as Outer Glow's is, so its parity test has no twin. A fix beyond Swift: a reduced preview scales the glow's size. The sheet shows its swatch, Size (0–100, 500 typed, px) and Opacity. Equivalent mutants: the canvas's margin counting hidden layers (it only widens the halo), the glow's `opacity > 0` (a clear glow composes nothing; the term spares the blur) and Trim's return Line truncated
|
||||
- Blur and noise adjustment layers, Image Trim and Inner Glow (12.8c, Swift 1.2.3: `98ad817`, `24052b5`, `feb39f3`, `cd0ca3c`). `AdjustmentKind` gains Add Noise, Gaussian Blur and Motion Blur after Grain, v1.2.3's order (it moved Add Noise ahead of the blurs), with `circle.dotted`, `drop.fill` and `wind` as `LayerIcons` paths. `LayerAdjustment` keeps Swift's seven optionals (`blurRadius`, `motionAngle`, `motionDistance`, `noiseAmount`, `noiseGaussian`, `noiseMonochromatic`, `noiseSeed`), their resolved getters and setters with Swift's defaults (10, 0, 10, 10, off, off, 0), `isValid`'s bounds on every kind, as Swift's, and `samplingMargin`; the manifest writes each only when set, the seed a `UInt32` read as Grain's; the store takes the three kinds from version 9, which it now writes (versions 1–9). `apply` runs the three through `PixelFilter::run` with the layer's own seed; `FilterJob::noiseOrigin` sits last, so the positional initialisers keep their order, and `kernels/NoisePixels.c` is v1.2.3's (`noise_add_at`, whose position hash reshuffles the Add Noise filter's grain too). A new Add Noise layer rolls its seed, as Grain does; the editor opens the kind's filter panel on the layer's settings and writes back that kind's alone. Adaptation: Swift's canvas surface holds a pixel a point, so its region's origin and `scale` are already the surface's pixels; the twin's surfaces hold device pixels, so `LiveMaskRenderer::adjust` passes `adjustmentScale` (the canvas's zoom, Swift's, 1 elsewhere) times the device's pixels a region unit, and `apply` counts the noise origin in the image's pixels, so a HiDPI canvas blurs at its own density and the noise stays one field across dirty rects. `AdjustmentSurface::draw` takes Swift's `padding` in the painter's units: the halo is allocated past the shown area, not cut to the device (the canvas's outer surface is dirty-sized), and drawn back under the painter's clip. The canvas pads by the widest visible `samplingMargin` times the zoom, and its crisp path (from 200%) now draws the layers through a padded surface as well, as Swift's `drawLayers` wraps its own there. The crisp path wraps its surface even without an adjustment, where Swift's `drawLayers` wraps only for one: pixel-identical (source-over onto a cleared raster), one allocation a repaint region, as the smooth path already does. The HiDPI product is pinned by its two factors (the zoom on the canvas, the device ratio on a scaled composite), not at a device ratio of two itself. Image Trim: `Document/ImageTrim` twins Swift's file: `TrimBasedOn` by raw value, `TrimOptions` (a tolerance per premultiplied channel, alpha included, which the sheet leaves at 0), `calculateTrimRect` (the alpha bounds, or each row scanned in from both ends against the corner's pixel) and `trim` (the export's render, then `CanvasResizer` by the content offset, or the snapshot itself when nothing is cut). `UI/TrimSheet` is Swift's sheet: 320 wide, the title, Based On as radio buttons, Trim Away's four checkboxes, OK resting while none is checked, Return and Escape through `NativeShortcut`. `ProjectController::trim` runs as Canvas Size does, through `resizeProject`, whose resize may now return nothing: Swift's trim with nothing left returns with no alert and no step. The Image menu's Trim… follows Image Size…, with no shortcut. Left out as dead: `TrimError` (never thrown), `trimImage` and the session's `trim` (called by Swift's tests alone; the twin tests the rect and the controller's two steps), the kept rect's `maxX > minX` guard (the early returns leave content, so left < right and top < bottom) and the colour scan's `last > first` (the row's first unmatched pixel stops it). A fix beyond Swift: a raster that cannot be converted is `ExportError(render)`, explained, where Swift's `try?` reports nothing left; that failure path is untested, since only a failed allocation reaches it. Inner Glow joins as Outer Glow did (12.5b): `InnerGlowEffect` (10 pixels, white, 75%; 0–500, 0–1), `LayerEffectKind::innerGlow` last; `LayerEffectsKernel` blurs the shape at sigma size ÷ 2 and keeps shape × (1 − blur), composed over the colour overlay and under the inner shadow, Metal's order; no margin (Swift's `innerGlowRendersInsideSourceWithoutBoundsExpansion`), the stroke surface's reach three sizes and two. Swift's CPU `innerGlowCoverage` is dead here, as Outer Glow's is, so its parity test has no twin. A fix beyond Swift: a reduced preview scales the glow's size. The sheet shows its swatch, Size (0–100, 500 typed, px) and Opacity. Equivalent mutants: the canvas's margin counting hidden layers (it only widens the halo), the glow's `opacity > 0` (a clear glow composes nothing; the term spares the blur) and Trim's return of an uncut snapshot (the canvas resizer returns one unchanged at the same size and place). The padding branch keeps an unpadded surface to the shown pixels: mapped there and back, a rect rounds a pixel wider. Tests: `BlurNoiseAdjustmeLine truncated
|
||||
- The finishing filters (12.9a, Swift 1.2.4, `e08dd8c`): `kernels/AdjustPixels.c/.h` are v1.2.4's, with `adjust_colored_vignette` (Camera Raw's falloff pulled out as `vignette_mask`, Camera Raw's own vignette unchanged) and `adjust_tonal_contrast`. `FilterKind` gains Vignette, Bloom / Glow and Tonal Contrast after Add Noise, Swift's order, so the Filter menu lists them; `FilterSettings` keeps Swift's thirteen fields, defaults and clamps (the colour through `AdjustmentColor::clamped`). Vignette and Tonal Contrast hand the kernels every setting; Tonal Contrast's base is `PixelAdjust::gaussianBlur` at radius × scale, clear past the edge, Swift's unclamped `applyingGaussianBlur`. Known difference: Core Image's `CIBloom` is private, so the twin's Bloom / Glow is the pixels plus their Gaussian glow (radius × scale, clear past the edge) at amount ÷ 50, Swift's intensity, added byte by byte on the premultiplied values and rounded once, alpha included, so the glow shows past a clear layer's light as Swift's test asks; Core Image's own curve may differ. Bloom grows the grid by three radii and two and is trimmed on commit, as the blurs are. A commit with nothing to change closes as Cancel does: Vignette or Bloom at amount 0, Tonal Contrast at amount 0 or with all three tones at 0. Every filter's commit now keeps the layer's effects (Swift's rebuild gained `effects: current.effects`); shape and text still drop. `FilterEdit::previewSourceVersion` is Swift's: `prepare` moves it with each new grid, the worker hands back the version it rendered from, and a result for an older grid lands nothing, its waiting job rendering next (a render begun before `growForBlur` would otherwise show the old grid's pixels in the new placement). The twin's one watcher can hold a job waiting behind another edit's worker, which Swift's per-edit task never does, and Preview off keeps that job: a growth therefore rebuilds a waiting job on the new grid, so the version it starts with names its image. The picker gains Swift's Vignette target (`openVignetteColorPicker`, `previewVignetteColor`, "Color Picker (Vignette Color)"), closed with its colour by OK and without by Cancel, as the Gradient Map's; Swift's `committing` terms are left out as the Gradient Map's are (a commit keeps the project busy and closes the picker first), and so is `setVignetteColor`'s edit guard, read through `value()`: the Vignette picker lives only while its edit is open (Cancel and OK close it first), where the Gradient Map's may outlive an adjustment's editor. `UI/FilterSheet+Finishing.cpp` holds the three kinds' rows, FilterSheet.cpp running to 442 lines: Vignette's Color row (a 95-point title, the Gradient Map's 24-point swatch), Amount and Highlights with Swift's help, Midpoint, Roundness (no unit), Feather; Bloom's Amount and a logarithmic Radius; Tonal Contrast's Amount, Shadows, Midtones, Highlights and a logarithmic Radius. The sheet hands the picker's working colour to the Vignette and repaints the swatch on every change. Tests: `FinishingFilterTests` (Swift's five, then the kernels at a scale, the settings' bounds, the idle commits, Bloom's grid, a preview for an older grid, the picker, the sheet), the kinds in `FilterModelTests`, a filter keeping effects in `LayerEffectsTests`, the Filter menu in `CompositorMenusTests`.
|
||||
- Keyboard zoom, the rail and the tab strip (12.9b, Swift 1.2.4: `383548c`, `8053fb6`, `75c4219`). `CanvasViewport::keyboardZoomLevels` holds Swift's seventeen stops (12.5% to 1600%) and `keyboardZoomTarget` the next stop past the zoom within a billionth, or the zoom itself at an end; `EditorSession::zoomKeyboard` steps about the view's centre, leaves fit mode and stays silent when nothing moves; Swift's `step != 0` there is dead and left out (the viewport answers the zoom itself for a step of 0). The View menu's Zoom In and Zoom Out and the toolbar's zoom buttons step through the stops, the menu entries doing nothing while a text field or open text holds the keys (Swift's `NSText` guard; the entries' `m_field` and `m_canvas` with a draft). Swift's `handleKeyboardZoom` catches its zoom keys on key-down and repeat because a Mac menu key equivalent does not repeat; Qt's shortcuts repeat, and a matched shortcut never reaches a key filter. The menu's Ctrl+= and Ctrl+- also take Ctrl with the keypad's minus (probed in the app), so the canvas's window-wide filter (`handleKeyboardZoom`, beside the blend keys, outside text) takes only what Zoom In's entry cannot hold: Ctrl with Shift and = (Qt's Ctrl++) and the keypad's plus, never with Alt or Meta, and only while that entry keeps its default key (a remapped entry leaves the plus alone, as Swift's check of `ShortcutSettings`). Swift's zoom-out chords are all the menu's here, so the filter has no zoom-out branch. Swift's `8053fb6` pads the zoom buttons by 4 points; Qt's toolbar spaces its actions itself, left as it is (cosmetic). The rail already scrolls without scroll bars at 56 points (4.3), what `IndicatorlessScrollView` gives macOS. The tab strip follows `ProjectTabScroller`: each title button is Swift's `projectTabLabelWidth` wide (the title's width in its 12-pixel medium or semibold font, 10 more when unsaved, 35 to 155) plus Swift's trailing 8, the title elided within it and painted from the left (`TabTitle`, Swift's leading frame; a `QToolButton` centres its text), so a title moves only the tabs after it; the unsaved dot is a character, wider than Swift's 10 points of dot and gap, and widens its button by the excess (known difference). Since 13.14 the strip no longer scrolls: tabs that do not fit gather under the overflow pill and the front tab is always shown (Swift's drop slot has no twin, 4.7). Tests: `KeyboardZoomTests` (Swift's five, the tolerance from each side, and the key test in the app's own window: each chord's route, repeat, Shift-minus, Alt, Meta, text fields, a remapped entry, no document), `ProjectTabStripTests`, the menu in `ViewMenuTests`, the toolbar and strip in `ProjectWorkspaceViewTests`.
|
||||
- The format version in one place and the layer context menu (12.10a, Swift 1.2.5: `4cc9951`; `6fa115e`, `2f1e796`, `5c5f05b`, `1366f68`, `5d7adba`). `ProjectManifest::current` (9) is what saves write and `supported` (1 to it) what loads accept; the manifest's default, both version checks (`supports`) and the version error's range read them, so a bump is one edit. Swift moved the layer menu from each cell to the table; the twin moves it from `LayerCell::actions()` to the list: `NativeLayerList::contextMenu` is Swift's `contextMenu(for:)` (a row pressed with nothing selected is selected first; Duplicate Layer, Rename… for one layer alone, Delete Layer, Delete Selected Layers or Delete Mask, Create or Release Clipping Mask, Group Selected Layers, Move Out of Folder, the merge entry under `mergeTitle`, Add Mask with Reveal All (White) and Hide All (Black), Enable or Disable Mask, Delete Mask, Link or Unlink Mask, Show or Hide Layer, with Swift's separators and each entry enabled as Swift's `validateMenuItem`, the mask entries targeting the pixels first; 1.2.5 left Layer Effects out; a selection refused (Levels open) leaves no active layer, and every entry then rests); `menuFor` is Swift's `menu(for:)`, routing the press first: on an effect's row that effect is chosen (Swift's `selectEffect(at:)`), elsewhere the chosen effect goes, a row counts as selected only while no effect is chosen (`selectedEffect`, which a stale choice an undo left fails, as the row's painting reads it; Swift's table then shows none), the pressed row's id is read first (Swift's `targetLayer`), since a selection may finish text and rebuild the rows, a selected row keeps the selection with the pressed layer primary unless a thumbnail targets its mask or pixels, and an unselected row becomes the selection, the list takes the keys and its mask or pixels are targeted. `LayerCell::contextMenuEvent` executes that menu; below the rows none shows. Left out as dead: Delete Mask's `mask != nil` (the session chooses a mask only while it exists) and the routing's `effectSelection = nil` (`selectLayers` and `selectLayerTarget`, which every path calls, drop a chosen effect themselves). The entries act on the layer active when chosen, as Swift's `@objc` actions read it, where the old cell menu acted on its own row; Rename asks `canEditLayers` again then, as Swift's `renameSelectedLayer`. Tests: `LayerContextMenuTests` (Swift's regression tests, the folder duplicate included; titles; a real right-click), `LayerContextMenuEdgeTests` (the gates at their edges, a refused selection, action-time targets and an entry with no active layer, a chosen or stale effect, a rebuild under the press), `LayerContextMenuFixtures.h`, `theCurrentFormatVersionIsOneTheReaderAccepts` in `ProjectTests`, the old cell-menu cases in `NativeLayerListTests` and `LayersPanelTests` moved to the list's menu.
|
||||
@@ -167,6 +167,9 @@ Platform adaptations, each forced by a missing Qt analog:
|
||||
- New Canvas presets and the Quick Look preview (13.7, Swift 1.3.6–1.3.7: `aaf3dc0`, `14aad18`, `4868465`): `NewCanvasSheet` twins `aaf3dc0`: the subtitle goes; beside "New canvas" a 28-point `QToolButton` (`presetSizes`, an instant popup) draws Swift's three 2.5-point dots in `WindowText`, flush with the fields' right edge, its own `paintEvent` drawing no menu arrow; Swift's tooltip and the accessible name "Preset sizes". `CanvasPreset` (in `NewCanvasSheet.h`, as Swift's file holds it) keeps Swift's `groups` and `all`; the menu is Custom, then each group after a separator, checkable `QAction`s in an exclusive `QActionGroup`, each holding its index in `all` (Custom −1). `checkPreset` checks the first preset whose sizes print as the fields' exact text, else Custom, on every edit of either field, as Swift's binding reads it; choosing a preset fills both fields; Custom fills nothing and checks the match again, since Qt checks a triggered action where Swift's picker sets nil and re-reads. Known difference: Swift's label is clickable ten points past the dots into the padding; the twin's button ends at the content's edge. The Quick Look preview twins `14aad18` and `4868465`: `ImageExporter::quickLookImages` returns nothing past 50 megapixels (logged as info), else renders the snapshot and `scaledJPEG`s it: the long side at most 1024, sides rounded, a pixel at least, `QImage::scaled` with `SmoothTransformation` (area averaging when shrinking) for CoreGraphics' high quality, drawn on white into `RGB888`, JPEG quality 80, 72 dpi as `CGImageDestination` writes; a render or scale that fails logs a warning on `compositor.io` and saves without one, Swift's `try?`. `QuickLookImages` (Swift's, in `ImageExporter.swift`) is declared in `ProjectStore.h`, since the exporter includes the store. `ProjectStore::save` takes it optionally: with one, the staged folder gains `QuickLook/Preview.jpg`, written and synced with its folder before the project's folder is synced and swapped in; without one the swapped-in project has none. Loading reads only the manifest and `images/`, so the folder is ignored, and `ProjectDigest` reads neither, so writing or deleting it is no external change. The controller's save worker makes the preview and hands it to the store. `docs/writing-comp-files.md` gains Swift's line, OmaPhoto's name. Equivalent mutant: the heading's stretch (the title label takes the room). Untested: the null checks after scaling and allocating the flattened image (an allocation failure); without them a null scale would save a white preview. Tests: `NewCanvasSheetTests` (the dots filled and never outlined, `presetsAreSwiftsGroups`, `aPresetFillsTheFieldsAndTheMatchIsChecked`, `theMoreButtonDrawsThreeDotsAndOpensTheMenu`), `QuickLookPreviewTests` (the store's folder and its absence, load ignoring it, the JPEG byte for byte against smooth scaling on white at quality 80, sizes and rounding, the 50-megapixel bound, a failed render, a controller save), and the sync order in `ProjectReplaceTests`.
|
||||
- Tabs reorder and overflow (13.14, Swift `1faf7a0`): `UI/ProjectTabLayout.h/.cpp` twin Swift's file: `projectTabSpacing`, `ProjectTabSlot`, `ProjectTabPillSlot`, `ProjectTabOverflow` (`contentWidth()` for Swift's computed property), `projectTabOverflowLabel` and `projectTabOverflow`, widths a `std::map` read through `at` (every tab has one), the pill's width a `std::function`. Left out as dead: `span`'s empty guard (never asked of no tabs) and `contentWidth`'s pill fallback (a pill always has a tab after it). `ProjectWorkspace::moveTab` is Swift's, clamped, and emits `changed()` only when a tab moved. `ProjectTabStrip` is now a plain `QWidget` that takes the toolbar's free width (the spacer after it went; its size hint is every tab in a row, its minimum none) and places its children by hand from `projectTabOverflow` over its own width: the overflow pill at x 0 and the visible tabs at y 3, 28 high, each as wide as its button's size hint (so the unsaved dot's extra width counts, as before), hidden tabs hidden; it lays out again on `resizeEvent`, on the workspace's signal and on each tab's session signal (a title or a dot changes widths; a hidden button posts no layout request). The QScrollArea, its offset and `showFront` are gone, as Swift's scroller and reveal are; the selected tab is always shown by the pinning. `OverflowTabsPill` (a `QToolButton`, `projectTabsOverflow`) paints an inactive tab's capsule, the label "N more tabs" at 12 pixels medium and a drawn `chevron.down`, the label and chevron at half opacity while the workspace cannot switch (the capsule stays, as Swift's `.opacity` sits before its background); tooltip and accessible name are the label; its width is Swift's `projectTabOverflowPillWidth` (the label's ceiling plus 11, 4, 10, 11). Its press opens a `QMenu` (`projectTabsOverflowMenu`, deleted on close) four points below it, as `popUp(positioning:)` does, listing the hidden tabs in order, "• " before an unsaved one, a choice selecting; while the workspace cannot switch no menu opens. The reorder twins Swift's: `ProjectTabButton` watches its title button's left press, move and release in global coordinates (Swift's `.global` space) and reports `TabDragPhase::changed` once the press has moved three points and `ended` on the release; the strip's `Reorder` is `TabReorderState` (others, widths, compacted x, start, origin, translation, target), `nearestSlot`, `renderX`, `makeReorder` and `commitReorder` as Swift's (a neighbour closed mid-drag has no place, as Swift's `if let`s). A drag begins only while the workspace can switch and across at least three points, selects its tab first and raises it over the others (Swift's `zIndex`); it commits on release only if the workspace can still switch; the others ease to their places through a 150 ms `QPropertyAnimation` of `pos` (`OutQuad` for SwiftUI's `easeOut`), the dragged tab following the pointer at once, and every tab eases home when it ends. Qt can lose a release: a drag whose tab closes is dropped on the workspace's signal (Swift's timer drops it too), and a drag on another tab drops a stale one first (Swift's timer would have cleared it within 0.1 s). Known difference: a lost release on the same tab leaves its stale state for that tab's next drag, which then measures from the old origin. Known difference: Swift's empty title bar beside the tabs drags the window (`TitleBarDragView`); Qt's toolbar is no title bar, so the strip's empty part does nothing. `NewTabDropSlot` has no twin, as before. Equivalent mutant: reporting `ended` after a click that never dragged (Swift's gesture never ends one); it differs only after a lost release, where it would drop another tab's stale drag a click sooner. Tests: `ProjectTabLayoutTests` (Swift's six, the bounds at exactly fitting, a lone wide pinned tab, the pill's width asked by count), `ProjectWorkspaceTests::moveTabReordersWithoutTouchingSelectionOrDocuments` (Swift's, plus its signal), `ProjectTabDragTests` (following the pointer, clamping, the gap, the slide's duration, curve and end, stacking, landing left and right and in place, a small or upward move as a click, a busy project at the press and at the drop, a tab closed mid-drag, a lost release giving way to the next drag, a three-point move and a move under another button, the overflow pill's label, place and width, the menu's entries, place and choice, pinning, the busy pill), and `ProjectWorkspaceViewTests` (overflow in the window; the strip ends before the zoom buttons). `ProjectTabStripTests::onlyANewFrontTabScrollsIntoView` went with the scrolling.
|
||||
- Ungroup Layers and the mask pointer (13.15, Swift `1318f1e`, `6c3b9a5`, `6955a6f`): `EditorSession::canUngroupLayers` and `ungroupLayers` sit in `LayerGroups.cpp` (declared in `LayerGroups+Session.h`), as Swift's extension: the folder's direct children, their parent now the folder's own, take its place in the list in their order; `releaseDetachedClipping` then frees clips that no longer sit on their base; `finishOpacityEdit` ends a drag first; one `Ungroup Layers` step selects the children with the lowest active (none for an empty folder) and forgets the folder's collapse. Swift's `LayerHierarchy.validate` guard is dead and left out: unwrapping a folder only shortens parent chains, so nothing it refuses can appear; its `document` guard is `canEditLayers`'. The Layer menu gains Ungroup Layers (Ctrl+Shift+G) after Group Selected Layers, enabled by `canUngroupLayers`, and `ShortcutDefinition::all` Swift's "Ungroup Layers" entry (g, Cmd+Shift), so it can be remapped; the list's context menu shows it after Group Selected Layers for a right-clicked folder alone (Swift reads the clicked row, not the active layer), enabled by `canUngroupLayers`. `NativeLayerList::showMaskCursor` twins Swift's `showMaskCursor`: the duplicate pointer drawn over an eye in a 7.5 by 5.5 box at the hot spot plus (15, 18), haloed by sixteen white copies one point out, the image widened to hold it (29 by 32 at 1×), the hot spot the pointer's; Swift's `eye.fill` is drawn as paths (an almond of two quadratic curves, a ring cut round a filled pupil). Over a mask thumbnail with Alt, `cursorFor` shows it whatever `canEditLayers` says, as Swift's now does (13.12 brings the Alt-click that shows the mask alone). Equivalent mutant: the context entry enabled by `canEditLayers` (the right-click makes the folder active, so `canUngroupLayers` says the same). Tests: `GroupSessionTests` (Swift's three ungroup tests; collapse forgotten, signals, a plain layer refused silently, a nested and an empty folder, an opacity drag ended, busy), `LayerContextMenuTests::aFoldersMenuOffersUngroup`, the menu entry and its gate in `CompositorMenusTests`, the shortcut count in `KeyboardShortcutsTests`, the eye pointer's pixels and hot spot in `LayerDragGestureTests`.
|
||||
- Compositor 1.4, part one (13.8: `8c0417b`, `fe7a83d`, `af3b568`, `3ccc125`, `d0135c8`, `dcf1c66`). A sharper canvas: `LayerRenderer::interpolation` takes Swift's `upright` (a layer whose `radians()` is 0, drawn within a thousandth of pixel for pixel, takes no filter), from `LayerRenderer::draw` and `TiledLayerRenderer`'s frames alike. The twin's adjustment surfaces already held device pixels (9.5b) and its adjustments already ran at the surface's scale (12.8c), so 1.4's surface and `adjustmentScale` changes were held; what was missing is ported: `AdjustmentSurface::draw` falls back to a pixel a painter unit when device pixels pass the budget (Swift's "falls back to points"), drawn back smoothly (Swift's `.high`), and nothing only when that passes too; `LayerAdjustment::apply` counts the noise origin per axis (`top × height / max(1, region height)`), where the twin used the width's scale for both. PSD (`IO/PSD`): Levels gamma in hundredths; `hue2` read as Swift's: 16 bytes at least, Colorize's own values or the Master's, then for each of the six ranges its band (degrees wrapped into 0–360) and values, 14 bytes each, until the data runs out; the reader keeps each mask's bounds and default byte (`PSDRecord::maskBounds`, `maskDefault`), and `PSDDocumentBuilder::maskOnLayerGrid` draws the stored patch, unfiltered, onto the layer's grid (its pixels, or the canvas for a layer without) at its place, the default round it; already the grid, or with no size, the patch is kept; a grid that cannot be allocated logs on `omaphoto.io` and takes the builder's existing "couldn't be converted" note, as Swift's nil does. `PSDAdjustments::levels` and `hue` are public, as Swift made them for its tests. Photoshop's Saturation: `HueSaturationFilter::adjustedSaturation`, Swift's to the letter. All cores: `kernels/LevelsPixels.c/.h` are Swift's file again (`cube_apply`); `BrushRaster::copy` (bytewise for the context's own format, drawn otherwise) and `BrushRaster::inBands` (one band under 250,000 pixels, else twice the ideal thread count, through `PoolMap`; a `bad_alloc` there is `ExportError(render)`, since the workers' callers catch that alone); `LevelsFilter::run` copies and runs `levels_apply` in bands; `HueSaturationFilter::run` copies and runs `cube_apply` in bands, where the twin had its own trilinear loop (the kernel's float order now: within a level of it, Swift's tests allow eight); `cube` keeps the last eight tables under a mutex, Swift's `cubes`. A blur's preview kept while the layer grows: `FilterEdit` gains `preparedTransform` and `pendingTransform`; a render carries the grown layer it was made from (`Filtered::placement`) and lands there, shown until the next replaces it; `displayedTransform` and `displayedMaskPlacement` read `preparedTransform`. Left out as dead in the twin: Swift's `current` terms (only a blur grows its grid, and only the automatic kinds, which never grow, refuse a stale preview or read `preparedSettings`), so the grid's version (`previewSourceVersion`, `Filtered::sourceVersion`) goes with nothing left to read it, and so does the twin's own remaking of a waiting job on growth (with Preview off nothing lands; with it on, the update makes a new job). The hierarchy kept: `LayerOrder` (`Document/LayerGroups.h`, its code in `LayerGroups+Order.cpp` with the document's hierarchy members, since `LayerGroups.cpp` would pass 500 lines): `Node`, `Result` (`order`, `visible`, `drawn`) and `resolve`, which keeps the last nodes and answer under a mutex; `CanvasDocument::hierarchy()` replaces `hierarchyEntries()`; `effectiveVisibleIDs`, `renderLayers` (a checked `std::map` index, after Swift's empty-order guard) and `effectiveOpacities` (a map of opacity and folder, no whole layers) read it; `groupSelectedLayers` reads `hierarchy().order`. `dcf1c66` was held: the canvas's `DisplayState` already works out the visible set once. Tests: `SharpCanvasTests` (the rule's edges; plain, masked and tiled layers crisp within the tolerance, filtered past it), `PSDAdjustmentTests` (Swift's three, plus the wrap, Colorize leaving the ranges, a cut record, the builder, a pixel layer's grid, a stretched layer, the patch kept, bounds or a layer of no height or width, the grid's bounds with a smaller patch, a patch the grid's size a pixel along), `PSDReaderTests` (the new layouts, the mask's bounds and default read and placed), `HueSaturationTests::positiveSaturationMatchesPhotoshop` (Swift's values, the clamps, a pixel), `AdjustmentBandsTests` (bands cover each pixel once, the copy's bytes, Levels and Hue/Saturation in bands against one pass of the kernels, cached tables), `HueSaturationFailureTests` (the filter now fails at its copy, the row list being gone), `FilterTests::growingABlurKeepsThePreviewUpUntilTheNextOne` (Swift's) and `aMaskKeepsItsPlaceUnderAPreview` (a covering mask under a preview that did not grow, and on a grown one the old bounds), `FilterSessionTests` and `FinishingFilterTests` (an older grid's preview shLine truncated
|
||||
- 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 backdrops of 0.1 and 0.25 under sources past half catch the PDF's cubic), `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). `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). Swift's laziness (`PixelMove.applyOffset`, `holed`, `liftedTarget`) only spares the GPU canvas a rebuild per move; the twin rebuilds eagerly in `movePixels`, which realises the tiles before display and commit by construction, and a failure there cancels the move with `brushError` (`PixelMoveTests::aPixelMoveHoldsTheSessionAndAFailedMoveCancels`). `5708ed3`, `91cb0b1`: live paint, erase and mask paint, paint past the old bounds under the old mask (`TiledStrokeTests`, `BrushPreviewTests`, `MaskPaintAnywhereTests`), Smudge and Liquify previews through the mask, opacity and placement (`SmearCanvasTests`, `FolderOpacityTests`): held. `GradientEdit.Fill` and `applyFill` are the GPU's laziness; the twin fills in `refreshGradient`, before display and commit by construction, a failure cancelling with `brushError` (now tested), and the commit publishes what was filled. `MetalWarpTests.matchesTheCPU`: the twin's `WarpStroke` is the CPU reference (`SmudgeLiquifyTests`); nothing to port. `93de9e7`: a one-pixel white mask revealing its layer: held (QPainter's smooth draw clamps an image's edge, as Core Graphics draws it; now tested); the live paint grid's extent and edge sampling: held (`TiledStrokeTests`, `DownsampleTests`, `TiledLayerTests`). `clampedToExtent` is GPU-only. `54dd707`: every adjustment kind with alpha, masks and opacity, blurs at the drawn scale and direction, adjustment results equal to the CPU reference, deterministic seeds: held (the canvas runs the CPU filters themselves; now Line truncated
|
||||
- Drops between projects: `ProjectWorkspace::draggedLayerSource` finds the tab whose document holds the dragged id (Swift reads the drag pasteboard; the twin reads the drag's `QMimeData`, which every drop target has), `canReceiveDrag(data, destination)` refuses only a layer's own tab, and `receiveProviders` copies several ids one after another, as Swift awaits each provider, so a two-row drop on the New button opens two tabs as Swift's does. `ContentView` takes such a drop over its whole area (`setWorkspace` hands it the workspace; `acceptsDrop` keeps Swift's guards: busy, the sheet, the importer, a rename; `levels` arrives with 9.x), shows the 3-point accent ring inside the canvas while targeted and hands the drop's document point on when it fell on the canvas. `ProjectTabButton` takes drops for its tab (accent fill and rim, "Add to <title>"), and `NewCanvasButton`, the toolbar's New canvas button with the action as its default, for a fresh tab ("Open in a new project tab"), both refusing an Alt drag, which duplicates in the panel; Swift's transient `NewTabDropSlot` in the strip has no twin, the New canvas button is `NewProjectDropTarget` already. Files and pictures arrived with 10.4.
|
||||
- `CanvasThumbnail` draws a layer or a mask inside the canvas's box at `backingScale` 2 (the panel's pixmaps carry that ratio); a mask's ground is `LayerMask::background`, white or black by its edge's majority, as the canvas treats it (Swift's `edgeTone` mean went with 12.12b). `LayerIcons` draws the panel's SF Symbols as paths, in one palette colour, like `ToolIcons`. `BlendModePicker` is a `QComboBox` whose popup previews the highlighted mode through `previewBlendMode` for the layer it opened on (Qt calls `hidePopup` before `activated`, so the preview, the target and the highlight are cleared from the event loop, after a choice had its chance) and refuses a choice once another layer is active. `LayerAppearanceControls` is Swift's blend and opacity row: the slider's press and release are one opacity drag (the release is watched application-wide while the slider is down, so a popup opened mid-drag cannot keep the step open), the percent field applies once through its `FocusOut`, which Return and Escape cause (Swift's `releasesFocusOnCommit`), and steps with Up and Down (Swift's `isFinite` test and the steps' clamp are left out: `setLayerOpacity` refuses a value that is no finite number and clamps the rest), and another active layer makes the controls anew, as Swift's `.id` does.
|
||||
- `ProjectWorkspaceView` is the `QMainWindow`: the front tab's `ContentView` as its central widget, made anew when another tab comes to the front, as SwiftUI's `.id` rebuilds it, and disabled while the workspace manages; the window holds the shown tab (`std::shared_ptr`) until the old editor is deleted, as Swift's views hold their session, so a replaced or closed tab's session never ends under its editor (`ProjectWorkspace::tab` is public for this); a `QToolBar` with the New canvas action, the `ProjectTabStrip` (which takes the toolbar's free width) and the four zoom actions, since SwiftUI's `.toolbar` modifier lives in `ContentView` but the toolbar belongs to the window. Its actions follow the front session's `changed()`, and on that signal the window syncs every tab's button as well, because a busy front tab dims the other tabs' buttons, which their own sessions' signals do not reach; the strip syncs its buttons on the workspace's signal, each button on its own session's. The window's title is the front tab's, with Qt's `[*]` placeholder, so `setWindowModified` can show the mark that `isDocumentEdited` shows; `setWindowFilePath` carries `representedURL`. The editor's own minimum size is cleared inside the window, which owns the 800 × 520 minimum for the whole, toolbar included. Closing the window ignores the event and asks the workspace (`closeWindow`), which confirms tab by tab and then closes the window again with the property `closeConfirmed` set, since the workspace may not include `UI/` headers; a confirmed close is accepted and the mark cleared, so a window shown again asks again. `ProjectTabStrip` is a plain `QWidget` whose buttons are kept for the tabs that stay, made for new tabs and deleted for tabs that went, and placed by hand from `projectTabOverflow` (13.14): every button at its own size hint, so no title is squeezed below Swift's 35 points, laid out again when the strip is resized, the workspace changes or any tab's session changes; it fills no background, so the toolbar shows through (a stylesheet would do the same but freezes the widgets' palette, so a theme switch would pass them by); the unsaved dot is a character before the title. Files named on the command line go to `ProjectWorkspace::receive`, as Finder's Open With reaches the delegate. Layer drops on tabs and the New canvas button are 4.7's; file and picture drops arrived with 10.4.
|
||||
|
||||
@@ -433,9 +433,12 @@ Reviews for this section ran on codex `gpt-6.1-sol` at `max` (agent `codex-sol`)
|
||||
- 13.6 review (Fable 5.1): round 1 FAIL (four survivors unrecorded; the code judged right against Swift); round 2 PASS.
|
||||
- [x] 13.7 Versions 1.3.6–1.3.7: New Canvas presets in a More menu (`aaf3dc0`); the project's Quick Look preview (`14aad18`, `4868465`) judged: the package's preview file is format, Finder's thumbnail extension is macOS alone.
|
||||
- 13.7 port (agent): tests in `NewCanvasSheetTests` (presets, the check, the dots), `QuickLookPreviewTests` (the store's folder, load ignoring it, the JPEG byte for byte, sizes, the 50-megapixel bound, a failed render, a controller save) and the sync order in `ProjectReplaceTests`. Survivors recorded: the heading's stretch (equivalent); the scale and allocation null checks untested. No review yet.
|
||||
- [ ] 13.8 Version 1.4, part one: the changes behind the GPU canvas that the CPU canvas and export share: a sharper canvas with adjustment layers (adjustment surfaces at device resolution, adjustment operations scaled with the surface, direct sampling for an upright layer at 1:1, tiled layers included) and the PSD adjustment and mask fixes (`8c0417b`); Photoshop's positive Saturation (`fe7a83d`); Levels and Hue/Saturation on all cores (`af3b568`); a blur's preview kept while the layer grows (`3ccc125`); the hierarchy kept until what shapes it changes (`d0135c8`, `dcf1c66`).
|
||||
- [ ] 13.9a Part two: blending. Clipping stacks and live-mask coverage at device resolution; 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 (`6dc2219`); a clipping stack composited in its base's mode in every mode, on the canvas, in export and in Copy Merged (`8ab32c9`; Qt keeps the stack's mode: a held judgment to prove); Soft Light by Photoshop's formula as Core Image computes it, on the canvas, in export and in Copy Merged (`5a8f6ce`: a 0.9 source over a 0.5 backdrop gives about 170; compare Qt's formula first); soft, partly clear painted layers blending alike in Soft Light, Overlay, Multiply and Color Dodge (`e6c964b`, `softEdgedLayersBlendTheSame`).
|
||||
- [ ] 13.9b Part three: the Metal canvas (`cdf8674`, `4b04323`, `5708ed3`, `93de9e7`, `54dd707`, `91cb0b1`, `f773f7b`, `4c5e1a6`, `d04f159`, `b1b7490`) stays CPU (the port's decision); each contract its tests hold for every canvas is judged and, where the twin lacks it, ported: Grain and Add Noise anchored to the document while panning and zooming, the canvas mapping its surface back to document coordinates (`54dd707`; the twin's adjustments take view-space regions today); every adjustment kind with alpha, own and folder masks and opacity, blurs at the drawn scale and direction (`54dd707`); a moved or duplicated selection's preview and commit (the hole cut only for a move, the rounded offset, opacity and coverage once) and the lazy tiles realised before display and commit, a failure cancelling the move with `brushError` (`4b04323`); live paint, erase and mask paint replacing patch interiors, revealing paint past the old bounds under the old mask, gradients' selection clip and opacity matching the commit, the lazy gradient fill realised before display and commit, Smudge and Liquify previews keeping mask, opacity and placement (`5708ed3`, `91cb0b1`); a new one-pixel white mask revealing its whole layer, rotated or scaled without a faded edge, and the live paint grid's extent kept (`93de9e7`); rotation, flips and 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 (`cdf8674`); non-adjacent live mask sources, distortion previews (perspective, folded, flipped, placed masks, effects), text previews keeping effects and handing them on without a flash, new drafts above the active layer inside a clipping stack, lines above the image, effects and placed masks kept while brushing, painting masks, dragging gradients and moving pixels (`f773f7b`); sharp reductions and fresh paint pixels (`4c5e1a6`, `d04f159`); `b1b7490` only words a test's diagnostics. Also named, each to judge: sampling modes and rounded-up reduction extents, premultiplied blending in sRGB, the surround, shadow and border, placed own masks and multiplied folder masks on ordinary pixel layers, and filter, Levels and Hue/Saturation previews replacing the layer's pixels with its alpha, mask, opacity and placement (`cdf8674`); image-edge sampling that carries edge values outward for filtering and keeps the image's own boundary, for ordinary images and live grids as for the white mask (`93de9e7`); adjustment results equal to the CPU reference, deterministic Noise and Grain seeds, Add Noise counted in frame pixels from the document's corner and Grain's size in document pixels (`54dd707`); a lazy gradient fill that fails cancelling the gradient with `brushError`, and a failed commit publishing it (`5708ed3`); Smudge and Liquify strokes and commits equal to the CPU algorithms (`91cb0b1`, `matchesTheCPU`: the twin's CPU path is the reference, a held judgment); new text shown when the active layer cannot draw, the pixel grid and the new text frame drawn above the image and passing input through, and edits keeping mask gray values and effects and placed masks as paint bounds grow (`f773f7b`). Texture retention, eviction and blits are GPU mechanisms with no CPU twin.
|
||||
- [x] 13.8 Version 1.4, part one: the changes behind the GPU canvas that the CPU canvas and export share: a sharper canvas with adjustment layers (adjustment surfaces at device resolution, adjustment operations scaled with the surface, direct sampling for an upright layer at 1:1, tiled layers included) and the PSD adjustment and mask fixes (`8c0417b`); Photoshop's positive Saturation (`fe7a83d`); Levels and Hue/Saturation on all cores (`af3b568`); a blur's preview kept while the layer grows (`3ccc125`); the hierarchy kept until what shapes it changes (`d0135c8`, `dcf1c66`).
|
||||
- 13.8 port (agent): tests in `SharpCanvasTests`, `PSDAdjustmentTests`, `AdjustmentBandsTests`, `LayerOrderTests`, `PSDReaderTests`, `HueSaturationTests`, `FilterTests`, `FilterSessionTests`, `FinishingFilterTests`, `BlurNoiseAdjustmentTests`, `AdjustmentSurfaceTests`, `HueSaturationFailureTests` (it now fails at the copy). Survivors recorded: Saturation's clamps, the cube cache, the copy's bytewise branch, the bands' guard (equivalent); the mask grid out of memory untested. No review yet.
|
||||
- [x] 13.9a Part two: blending. Clipping stacks and live-mask coverage at device resolution; 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 (`6dc2219`); a clipping stack composited in its base's mode in every mode, on the canvas, in export and in Copy Merged (`8ab32c9`; Qt keeps the stack's mode: a held judgment to prove); Soft Light by Photoshop's formula as Core Image computes it, on the canvas, in export and in Copy Merged (`5a8f6ce`: a 0.9 source over a 0.5 backdrop gives about 170; compare Qt's formula first); soft, partly clear painted layers blending alike in Soft Light, Overlay, Multiply and Color Dodge (`e6c964b`, `softEdgedLayersBlendTheSame`).
|
||||
- 13.9a port (agent): Soft Light by Photoshop's formula (Qt's is the PDF's, up to 17 levels darker on dark backdrops); stacks, coverage and adjustments in every mode held and proven. Tests in `BlendingTests`, `BlendModeTests`, `ClippingStackTests`. Survivor recorded: `<= 0.5` for `< 0.5` (equivalent). No review yet.
|
||||
- [x] 13.9b Part three: the Metal canvas (`cdf8674`, `4b04323`, `5708ed3`, `93de9e7`, `54dd707`, `91cb0b1`, `f773f7b`, `4c5e1a6`, `d04f159`, `b1b7490`) stays CPU (the port's decision); each contract its tests hold for every canvas is judged and, where the twin lacks it, ported: Grain and Add Noise anchored to the document while panning and zooming, the canvas mapping its surface back to document coordinates (`54dd707`; the twin's adjustments take view-space regions today); every adjustment kind with alpha, own and folder masks and opacity, blurs at the drawn scale and direction (`54dd707`); a moved or duplicated selection's preview and commit (the hole cut only for a move, the rounded offset, opacity and coverage once) and the lazy tiles realised before display and commit, a failure cancelling the move with `brushError` (`4b04323`); live paint, erase and mask paint replacing patch interiors, revealing paint past the old bounds under the old mask, gradients' selection clip and opacity matching the commit, the lazy gradient fill realised before display and commit, Smudge and Liquify previews keeping mask, opacity and placement (`5708ed3`, `91cb0b1`); a new one-pixel white mask revealing its whole layer, rotated or scaled without a faded edge, and the live paint grid's extent kept (`93de9e7`); rotation, flips and 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 (`cdf8674`); non-adjacent live mask sources, distortion previews (perspective, folded, flipped, placed masks, effects), text previews keeping effects and handing them on without a flash, new drafts above the active layer inside a clipping stack, lines above the image, effects and placed masks kept while brushing, painting masks, dragging gradients and moving pixels (`f773f7b`); sharp reductions and fresh paint pixels (`4c5e1a6`, `d04f159`); `b1b7490` only words a test's diagnostics. Also named, each to judge: sampling modes and rounded-up reduction extents, premultiplied blending in sRGB, the surround, shadow and border, placed own masks and multiplied folder masks on ordinary pixel layers, and filter, Levels and Hue/Saturation previews replacing the layer's pixels with its alpha, mask, opacity and placement (`cdf8674`); image-edge sampling that carries edge values outward for filtering and keeps the image's own boundary, for ordinary images and live grids as for the white mask (`93de9e7`); adjustment results equal to the CPU reference, deterministic Noise and Grain seeds, Add Noise counted in frame pixels from the document's corner and Grain's size in document pixels (`54dd707`); a lazy gradient fill that fails cancelling the gradient with `brushError`, and a failed commit publishing it (`5708ed3`); Smudge and Liquify strokes and commits equal to the CPU algorithms (`91cb0b1`, `matchesTheCPU`: the twin's CPU path is the reference, a held judgment); new text shown when the active layer cannot draw, the pixel grid and the new text frame drawn above the image and passing input through, and edits keeping mask gray values and effects and placed masks as paint bounds grow (`f773f7b`). Texture retention, eviction and blits are GPU mechanisms with no CPU twin.
|
||||
- 13.9b port (agent): Grain and Add Noise anchored to the document on the canvas (`adjustmentRegion`); every other GPU contract judged held, gaps pinned in `AdjustmentCanvasTests` and `CanvasEditContractTests`; the grid over the border hairline left as a known difference. No review yet.
|
||||
- [ ] 13.10 Part three: Shift keeps moved pixels on a line (`60bde4f`); Ctrl+A selects the canvas from the Layers panel (`451281e`).
|
||||
- [ ] 13.11 Version 1.4.1, part one: why a brush can't paint, Select All then Inverse deselects (`133c34a`); painting at the layer's own resolution (`d3170ab`): Clone Stamp and Blur sample there, the Brush and eraser on a scaled-down, rotated or unequally scaled layer that holds pixels lay a tip of the layer's own pixels and show a smooth edge scaled back up, while an empty layer, however small its transform, paints at document resolution (`paintingABlankLayerThatWasScaledDownPaintsAtDocumentResolution`); the twin's coverage in the stroke's grid (`BrushStroke::grid`, `BrushCoverage::render`) may already hold both, a held judgment to prove with `NativeResolutionPaintTests`' cases; Cancel and Apply only when an edit waits, scaled layers' scale (`67cc31e`).
|
||||
- [ ] 13.12 Part two: masks as Photoshop's: the button reveals the selection, Alt-click shows a mask alone (`b3419ab`).
|
||||
|
||||
Reference in new issue
Block a user