14.2: RAW import rounds to 8 bits with fine noise, in the kernel; the develop sheet shows Importing… until the layer lands
This commit is contained in:
14 files changed
+470
-65
No files matched your search
@@ -50,7 +50,7 @@ Platform adaptations, each forced by a missing Qt analog:
|
||||
- QPainter places an unscaled, unturned image on whole device pixels whatever the hints say; CoreGraphics would blur it by the fraction. Layers the app makes sit on whole pixels, Shape lines aside (8.8), and any scale or turn takes QPainter's filtered path, so tests of soft edges draw under a scale.
|
||||
- `ImageResizer` and `CanvasResizer` are namespaces. `Task.checkCancellation()` in the Swift resizer has no twin: no caller cancels a resize. Each turns a `std::bad_alloc` into `ExportError(render)` at its door (since 10.1 and 10.2), so the workers that run them catch it. A layer's new box is judged by `LayerTransform::isValid` before any number becomes an integer, where Swift converts first: `Int(Double)` traps on a wild value, C++ would be undefined. Records are rebuilt field by field as Swift rebuilds them, so a field Swift leaves out is dropped once that field exists (the image resizer passes on neither shape, effects nor text; the canvas resizer passes on shape and text, not effects). Options hold `qint64`, Swift's `Int`, like the manifest. `CanvasSizeDraft` keeps plain members where Swift has `let`, so the struct stays assignable.
|
||||
- `EditorSession` is a `QObject`. One signal, `changed()`, stands in for Swift's observation: every method that changes state emits it, a refused request stays silent, and widgets compare what they show with what the session holds, as `CanvasView.synchronizeDisplay` does. `beginEdit` emits as `endEdit` does, because an open transaction closes `canUndo` and `canRedo`. `fit` and `zoom` emit only when the view changed, so a zoom that is no number stays silent. The canvas edits `viewport` in place and then calls `notify()`. A Swift property with a `didSet` is a private member with a getter and a setter (`activeLayerID`, `isProjectBusy`, `showsNewDocument`, `showsImporter`, `isImporting`, `importError`, `renamingLayerID`); `document`, `tool`, `collapsedGroupIDs` and `selectedLayerIDs` are read through getters and change only through session methods; `history` and `viewport` are public members, as Swift's tests use them. `activeLayer()` returns a copy, as Swift's computed property does. `EditorSessionSignalTests` holds the signal's contract: every change is announced, a refusal is silent, and the last signal of a change sees the state the call leaves behind, read inside the slot.
|
||||
- `EditorSession.swift` runs to 741 lines, so the twin splits: `EditorSession+Model.h/.cpp` hold `ImageLayer`, `CanvasDocument` and `NavigationTool` (their own header also breaks the include cycle with `DocumentHistory`, which a member by value needs complete), `EditorSession.h/.cpp` the core, `EditorSession+Layers.cpp` the layer operations, `EditorSession+Transform.cpp` the transform edits, `EditorSession+Imports.cpp` the import queue and the busy gates; `EditorSession+Projects.cpp` twins the Swift file of that name. A session function that Swift defines in an extension lives in the twin of that Swift file, declared in its `Name+Session.h`: the `LayerGroups` extension (folders, the multi-selection, grouping, placing, `layerRows`) in `LayerGroups.cpp`, `finishDeletingLayer(s)` in `LiveLayerMask.cpp`.
|
||||
- `EditorSession.swift` runs to 741 lines, so the twin splits: `EditorSession+Model.h/.cpp` hold `ImageLayer`, `CanvasDocument` and `NavigationTool` (their own header also breaks the include cycle with `DocumentHistory`, which a member by value needs complete), `EditorSession.h/.cpp` the core, `EditorSession+Layers.cpp` the layer operations, `EditorSession+Transform.cpp` the transform edits, `EditorSession+Imports.cpp` the import queue and the busy gates (its queue's declarations in `EditorSession+Imports.h` since 14.2, keeping the header under 500 lines); `EditorSession+Projects.cpp` twins the Swift file of that name. A session function that Swift defines in an extension lives in the twin of that Swift file, declared in its `Name+Session.h`: the `LayerGroups` extension (folders, the multi-selection, grouping, placing, `layerRows`) in `LayerGroups.cpp`, `finishDeletingLayer(s)` in `LiveLayerMask.cpp`.
|
||||
- `groupSelectedLayers` follows Swift's steps (what stays keeps its place, the folder sits at the topmost selected branch, the wrapped layers go last in hierarchy order) with two terms dropped because they are dead in the twin: the `nil` that ends each ancestor chain (the twin's shared parent starts as the root) and the clamp of the insertion index (it counts layers that stay, so it cannot pass their number). `placeLayer` drops Swift's `target != id` for the same reason: the target is sought after the layer is taken out, so a layer is never found above itself. `canPlaceLayer` is tested by itself as well as through `placeLayer`, whose validation would hide its terms; the panel asks it before a drop. `selectLayers` and `extendSelection` fall back on any selected layer, as Swift's `Set.first` does: tests never depend on which. A session's document always passes `LayerHierarchy::validate` (every mutator checks before it commits), so every `parentID` names a folder of the document; `groupSelectedLayers` reads parents through `std::map::at`, which throws if that ever breaks.
|
||||
- Transform edits: `TransformEdit` holds what exists so far (`layerID`, `draft`, `group`); `persistent`, `corners`, `mask` and `floating` arrive with the items that read them (`TASKS.md`, "Session hooks owed"). A draft changes only through `previewTransform`, which refuses an invalid value, and begins as a valid transform or box, so it is valid by construction. `commitTransform` therefore gives each layer what `pendingTransform` gives it, one definition for preview and commit, inside one transaction, and skips a layer whose own result is invalid (a box squeezed until a member falls under a pixel); Swift's `edit.draft.isValid` and `firstIndex` guards are left out, since nothing can reach them. An edit that moves nothing records nothing, as in Swift: the history drops a transaction that changed nothing. `canTransform` and `groupTransformMembers` drop Swift's `isGroup` terms (a folder never holds pixels, `LayerHierarchy::validate` sees to it, so `asset` says the same), and `beginTransform` drops `transformEdit == nil` (`canTransform` reads it through `canEditLayers`). `groupTransformBox` keeps `max(1, …)`: rounding leaves a one-pixel layer at origin 1.3 a box under one pixel, and a test holds that. `DistortWarp::corners` came forward from 6.2 for the group box.
|
||||
- Swift's `async` session calls (`importImages`, `waitForProjectAccess`, `waitForFileRequest`) take a completion callback in the twin. A waiter runs at once when the session allows it; otherwise it is kept, and the setter that frees it posts each waiter to the event loop (`Qt::QueuedConnection`), where it looks again when its turn comes, as a Swift continuation re-enters its `while` loop: no waiter runs inside the setter, and the first may take the project before the second looks. `importImages` takes `QList<QUrl>` (a URL that is no local file is `unsupported`, Swift's `isFileURL` guard), decodes one file at a time through `QtConcurrent::run` and a `QFutureWatcher` (the worker catches `ImageImportError` and hands back the asset or the failure text; anything else is a bug and is left to crash), inserts on the UI thread, and wraps each request in one `Import Images` transaction. A request's callback is posted to the event loop too, so the caller goes on as a resumed Swift task does: after the transaction ended and, for the last request, after `isImporting` fell and the failures were published; for an earlier request while the next one decodes. Only a project that stops being busy frees project waiters: saying busy again leaves them in line. `showsBusy` is a single-shot `QTimer` of `busyIndicatorDelay` (250 ms), a `Qt::PreciseTimer` because a coarse one fires up to a twentieth early, and a child of the session so a test can read its remaining time; tests of time assert floors and states, never ceilings; `createDocument` clears its two flags and then resumes waiting file requests once, where Swift's two `didSet`s each try. The session therefore needs an event loop: every test that makes one uses `QTEST_GUILESS_MAIN`.
|
||||
@@ -182,6 +182,7 @@ Platform adaptations, each forced by a missing Qt analog:
|
||||
- Held keys, field values and resize snapping in the Move tool (13.13: `bca8f13`, `03d692c`, `686d8c7`, `c459f88`): `UI/HeldModifiers` twins Swift's class, a `QObject` singleton (`shared()`) with one signal, `changed()`. An application event filter reads key presses and releases and left-button presses and releases, Swift's `flagsChanged`, `keyDown`, `leftMouseDown` and `leftMouseUp`; a modifier key's own event sets or clears its bit, since Qt's event may not count it yet. Ctrl, Shift, Alt and Meta count (Swift's Command, Shift, Option, Control). Typing counts as none: Swift's `NSText` first responder is a focus widget with `Qt::WA_InputMethodEnabled`, so a `QLineEdit` and the canvas holding open text alike. `applicationStateChanged` stands for the two activation notifications (inactive clears; active reads `queryKeyboardModifiers`), and a `QMenu`'s `Hide` for `NSMenu.didEndTracking`. `TransformInspector` shows Auto Select as `transformAutoSelect != Ctrl` and the lock as `locksTransformRatio != Shift`, and a click while held writes the other way, as Swift's bindings; it synchronizes on `HeldModifiers::changed` too. Tooltips carry Swift's new words, Ctrl for Command. `transformPressLayer` follows Swift's: `autoSelect = transformAutoSelect != Ctrl`; Ctrl picks the layer under the pointer only with Auto Select off or with Shift (Ctrl+Shift adds either way). `TransformEdit.fromFields` sits last; `beginTransform(persistent, fromFields)` sets it, where Swift writes `transformEdit?.fromFields` in place (the twin's edit is read-only from outside). The bar's `change` opens a non-persistent `fromFields` edit; `finish` commits it, one step, when a field's scrub ends (`NumericScrub`'s `onEnd`) or the field loses focus. A focus borrowed by a menu, a popup or another window in front (`ActiveWindowFocusReason`, new: AppKit keeps the first responder when the app goes to the back) finishes nothing and keeps the typing. Escape in a field throws a `fromFields` edit away before handing the canvas the keys. The Swift path assumed: AppKit sends the Cancel button's Escape key equivalent (`configuredNativeShortcut(.escape)`) before the field editor, the only way `686d8c7`'s "Escape still throws the change away" can hold, since the field's own `onExitCommand` would end the focus and so apply the value. That path would also cancel a Ctrl+T edit from a field (and Return apply it); the twin keeps 6.1's reading there, a Ctrl+T edit waiting for the canvas's keys, a known difference held from 6.1. Flip H, Flip V and Sampling open a `fromFields` edit with no finish of their own, as Swift's do: it applies with the next field's finish, a canvas drag or anything that commits. `beginTransformDrag` commits a `fromFields` edit first, after `transformPressLayer` (so it picks nothing, as Swift's `picks`), so the two stay separate steps. Qt moves the focus before a press arrives, where SwiftUI's focus changes after `mouseDown`: a field left for a mouse press (`MouseFocusReason`) finishes from the event loop (a zero `QTimer`), so a typed value then a press over another layer drags the active layer under Auto Select, as in Swift (review 2's finding; `MoveBarPressTests`). `snappedResizePoint` lives in `Document/Crop.cpp` with Swift's: the edges each axis moves snap on their own to `transformSnapTargets` (the file's `nearest`, the first of ties), the nearer alone when proportional, the pointer nudged by one pixel's measured step; an upright layer only; guides set, or cleared when snapping is off, the drag is no resize or the layer turned. `dragTransform` snaps a resize unless Ctrl is held, proportional as `locksTransformRatio != Shift`. Dead or equivalent: `beginDistort` keeps `fromFields` where Swift's rebuild drops it, but a `fromFields` edit never reaches it (the canvas commits it before any handle drag). `snappedResizePoint`'s `before` read from `update(result)` or `update(point)` is equivalent (unlocked axes are independent, and proportional leaves one snap). The edge's tie (`<=`, the low edge) is reachable (the right handle half a pixel from its anchor, a one-pixel box) and held by `aTieTakesTheLowEdge`; the dragged handle's carried place by a drag past the anchor (`aLongDragSnapsTheEdgeItCarries`); Alt's centre resize by `altResizesFromTheCentreAndSnaps`. `QRectF::normalized` was dropped as dead (a resize keeps sizes positive and flips instead). Untested paths: the menu `Hide` and the activation's `queryKeyboardModifiers` (offscreen holds no real keyboard state). Tests: `MoveBarPressTests` (a typed value then a real press on another layer drags the active layer, two steps; Ctrl with a folder active and Auto Select on drags the folder past its box; a value left by a click in another field applies alone, one step), `MovePressFixtures.h` (the canvas fixture the three press files share), `TransformPressTests` (Swift's `commandFlipsAutoSelectWhileHeld` both ways and `commandShiftClickAddsALayerWithAutoSelectOn`, Ctrl outside the active layer, a fieLine truncated
|
||||
- The Smear's 1.4.2–1.4.3 fixes (13.16, part one: `7164dd4`, `ad9ad7c`, `504129d`, and `095da2f`'s commit half): Liquify is Swift's new GPU stroke on the CPU, as the twin has no Metal: `WarpStroke` keeps the layer as it found it (`m_original`, a copy made in the constructor) and a source offset per pixel (`m_offsets`, two floats each, zero at first); each dab copies the offsets under it to `m_scratch`, reads them bilinearly from behind the brush's travel, less the travel times the weight, writes them back, and draws the pixel afresh from the untouched layer, bilinear, held to its edges, each byte rounded once: `warp_push` of `MetalWarp.swift`, float for float (bytes over 255, mixed, times 255). Swift allocates the two textures on the first push; the twin allocates them with the stroke, before any dab (nothing changes the copy between), so a failure is `ExportError(render)` where `beginWarp` catches it, where Swift's guard returns in silence; Smudge's carried colour, allocated at the first dab inside `beginWarp`, fails the same way, and a dab's scratch that finds no memory skips that dab with a warning on `omaphoto.rendering`, Swift's guard on its scratch texture (review 2: it ran outside any catch). The colour reads keep the shader's `size - 2` as they are: the offsets' read above needs two columns and rows of the dab's area (`ix`, `iy`), which a canvas a pixel wide or tall lacks, so no read leaves the image; the clamps first written here for such canvases were dead and are removed (review 2). Smudge is `ad9ad7c` in both of Swift's paths: dabs every 0.005 of the width (a pixel at least), each dab lays what it carries at weight × strength, and carries what it just left, nothing older. `BrushSettings.blurRadius` (5) is the Blur's own Radius: the sample's sigma follows the Radius (see part two for the merged formula). `BrushControls` shows Radius for the Blur mode alone, after Strength: a scrubbable label (a tenth a point, 0.5–50), a 100-point slider over 0.5–20 in hundredths (past 20 Qt rests it at its end, so Swift's `min(20, …)` is left out), and a 42-point `PickerField` with "px": a readable number applies on Return or leaving, clamped, a number that is not finite takes 5 (Swift's binding), other text goes back, arrows step one (Shift ten) from the session's value, clamped; it prints Swift's `fractionLength(0...1)`, never grouped. `BrushStroke::maxDiameter` is 2100, so `finishWarp`'s tip four wider than a 2000 brush is no longer refused; `finishWarp` appends a dab centre only every twentieth of the brush (the last always), as Swift's; the thinning is `WarpStroke::committedPoints`, so the test reads it directly (`aCommitKeepsATwentiethAndTheLastPoint`). Part two (`095da2f`'s Blur half, on 13.11's grid sampling, cc9bf95 merged into port-d): `BrushStroke::Clone` is 13.11's `(image, placed, inGrid)` plus `render`, a function that makes a part of `image` (Swift's `cloneRender`; Swift keeps it beside `clone`, the twin inside it, so one assignment sets both), and `clone` is private behind `clone()` and `setClone()`, which drops the pieces cut so far (Swift's two `didSet`s). `clonePixels(key, tile)` is Swift's `clonePiece`: the tile mapped into sample pixels through 13.11's chain (grid, `pixelToDocument` unless `inGrid`, then `placed` to the image's pixels, Swift's `scaleX` and `scaleY`), two pixels round, made whole outward (`toAlignedRect`, Swift's `integral`), held to the image; an empty part paints nothing on that tile (the fill is skipped, as Swift's `if let piece`); otherwise the piece is `render(part)` or a copy, cut once per tile key and kept in `m_clonePieces`, and the tile's sample is drawn from it through the same transform shifted by the piece's corner, so every path reads the pixels the whole image would give. A piece that cannot be allocated is `ExportError(render)`, thrown on the calling thread with `publish`'s other copies, where Swift's `cropping` returning nil paints nothing. `blurSample(stroke)` returns the sharp sample in the stroke's grid (13.11's `region`, `fit` and budget unchanged) with `render` blurring a part at `sigma × fit`, padded by the Gaussian's reach (`ceil(3σ)`) and cut back, clear past the sample for pixels (`QImage::copy` fills outside with zeros), its edges repeated for a mask (Swift's `clampedToExtent`), so a piece equals that part of the whole byte for byte (Swift allows one level). The Radius sigma is Swift's `min(min(50, max(0.5, blurRadius)) / perPixel, sidePixels / 2)`. Merge: port-c's direct `clone =` writes became `setClone` (`beginBrush`, `finishWarp`, `cloneSample`'s callers, the tests and `documentClone`), every `Clone` initialiser names `render` (GCC's `-Wmissing-field-initializers`), and port-c's Blur sample tests compare the sharp sample (`framed`) and its softening through `render` (`soft`), with each brush size's old sigma now set as the Radius. Equivalent mutants and untested paths for part two: a piece, a padded copy or a cut-back blur that cannot Line truncated
|
||||
- Inner effects keep a pixel's alpha (14.1, Swift 1.4.6: `e7e295e`, `9cb57db`, `f25effa`): `LayerEffectsKernel` follows v1.4.9's `effects_compose`: the colour overlay, then the inner glow, then the inner shadow move each pixel's colour toward theirs times its alpha (MSL's `mix`, `recolour`), the overlay by its opacity, the inner two by their strength (`effects_inside`: 1 − the softened or moved shape, no longer times the shape); then the pixels go over the shadow, the outer glow and an outside stroke, and an inside stroke goes on top. So a half-clear pixel takes a colour fully and stays half clear, and effects beneath are not tinted. Swift's CPU path (`overlaid`, `recolor`, the surface's inner glow) serves machines without Metal and stays dead here, as 9.6b's. The effects button's tooltip says "Add layer effect". Tests: `ColorOverlayTests` (Swift's three; the inner shadow and inner glow on half-clear pixels, a shadow beneath untinted), the shader replay in `LayerEffectsKernelTests`.
|
||||
- RAW rounding and Importing… (14.2, Swift 1.4.6: `6345432`, `196342c`, `6e6ca38`, `0907f51`): `kernels/DitherPixels.c/.h` gain `196342c`'s `hash_noise` and `dither_quantize16`, its block body moved into `quantize_band` with a context struct, as 13.17's bands. `RawImporter::develop` works the look at 16 bits (`lround(65535 × …)`, opaque) into a buffer it allocates first (`ExportError(render)` without memory), then rounds it to 8 bits through `dither_quantize16`, where Swift renders Core Image at 16 bits and does the same; Swift's half-float working format names Core Image's own and has no twin. The session's `rawImporting` and `endRawDevelop` are Swift's: Import keeps the request and sets `rawImporting` while an answer waits, Cancel or an answer nobody waits for ends at once, and `finishDecode` ends it once the frame lands or fails (Swift's `defer`), after the layer is in. `RawDevelopSheet::setImporting` shows Swift's plate (a 32-point busy bar over "Importing…" in `PlaceholderText`, padded 16, on the window colour at 85% rounded 10), hides the preview's spinner, rests the sliders, Reset, Cancel and Import, and filters its dialog's Close and Escape while importing (Swift's `interactiveDismissDisabled`). Tests read RAW tones as a mean over the frame, rounded, since the noise moves single pixels a level; Qt's area average truncates, so a fitted preview reads a level low (`theQueueKeepsThePreviewsDecodeUntilReleased`). Holding the global pool freezes painting (Qt's raster engine fills on it), so the sheet's tests read its state before the event loop runs the develop, or use a frame large enough to take a while. Tests: `RawDitherTests` (Swift's two on the kernel; the triangular noise's three in four kept, alpha never passed, a develop dithered and repeatable), `RawDevelopTests` (importing, cancelling, failing, an answer nobody waits for, the plate, the locked dialog, the spinner and Reset while importing).
|
||||
- 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.
|
||||
|
||||
Reference in new issue
Block a user