From ff89a4fa2177e1fa20dabf6d0e8463b0ad8d358d Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Thu, 24 Sep 2026 20:12:41 -0400 Subject: [PATCH] Specify perspective correction as FR-DEV-20 Issue #13 asks for a vertical and horizontal keystone, and its number was renumbered from FR-DEV-19 when the spec gave that to mask editing. The clause was never written into the register, so the work had nothing to trace to. It is written as part of framing: after the crop and the straightening, before the stored orientation and the lens warp, carrying framing's Compose attribute, and with the inscribed crop accounting for it. --- docs/dev/requirements.md | 13 +++++++++++++ docs/dev/traceability.md | 11 ++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/dev/requirements.md b/docs/dev/requirements.md index f1a3179..5e447df 100644 --- a/docs/dev/requirements.md +++ b/docs/dev/requirements.md @@ -605,6 +605,19 @@ Editing rhythm depends on the hands staying put: reaching for a menu breaks the edit the same way it breaks the pace of a cull. A binding nobody can discover is the same as no binding, which is why the generated book is part of the requirement rather than documentation of it. +**FR-DEV-20 — Perspective correction.** Framing shall include perspective correction — a vertical +and a horizontal keystone — applied before the crop. It runs in the coordinate chain after the +crop and the straightening and before the stored orientation and the lens warp, so that "vertical" +means vertical in the photograph as it is shown and the lens is still corrected on the full frame +it projected. It is part of framing and carries its `Compose` attribute, so a settings paste +withholds it by default for the same reason it withholds the crop. The correction never exposes +undefined area on its own; where it is combined with a straightening angle, the crop that avoids +the empty corners (`max_inscribed_crop`) accounts for it, and the crop is refitted when the gesture +ends. +Converging verticals are the commonest geometric fault in architectural and interior work, and +correcting them reframes the image — which is why the order relative to the crop is not a +preference, and why the correction belongs to composition and not to the colour operations. + ### 3.4 Display and interaction **FR-DSP-1 — Proxy-resolution rendering.** The develop view renders at the resolution actually diff --git a/docs/dev/traceability.md b/docs/dev/traceability.md index e0f438a..03871f9 100644 --- a/docs/dev/traceability.md +++ b/docs/dev/traceability.md @@ -11,16 +11,16 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n |---|---| | Source files scanned | 441 | | TRACES tags found | 1725 | -| Requirements defined | 189 | +| Requirements defined | 190 | | Requirements deferred (post-v1) | 24 | | Requirements covered | 158 | -| **Coverage** | **83.6%** (158/189) | +| **Coverage** | **83.2%** (158/190) | ### By type | Type | Covered | Defined | |---|---|---| -| FR | 120 | 131 | +| FR | 120 | 132 | | NFR | 34 | 51 | | R | 4 | 7 | @@ -65,7 +65,7 @@ _None._ | FR-DEV-10 | [`core/dr-gpu/src/mask.rs:1039`](../../core/dr-gpu/src/mask.rs#L1039), [`core/dr-gpu/src/mask.rs:1263`](../../core/dr-gpu/src/mask.rs#L1263), [`core/dr-gpu/src/mask.rs:1522`](../../core/dr-gpu/src/mask.rs#L1522), [`core/dr-gpu/src/mask.rs:422`](../../core/dr-gpu/src/mask.rs#L422), [`core/dr-gpu/src/mask.rs:464`](../../core/dr-gpu/src/mask.rs#L464), [`core/dr-gpu/src/mask.rs:58`](../../core/dr-gpu/src/mask.rs#L58), [`core/dr-gpu/src/mask.rs:60`](../../core/dr-gpu/src/mask.rs#L60), [`core/dr-gpu/src/mask.rs:846`](../../core/dr-gpu/src/mask.rs#L846), [`core/dr-gpu/src/mask.rs:85`](../../core/dr-gpu/src/mask.rs#L85), [`core/dr-gpu/src/mask.rs:925`](../../core/dr-gpu/src/mask.rs#L925), [`core/dr-gpu/src/shaders/mask.wgsl:117`](../../core/dr-gpu/src/shaders/mask.wgsl#L117), [`core/dr-gpu/src/shaders/mask.wgsl:59`](../../core/dr-gpu/src/shaders/mask.wgsl#L59), [`core/dr-gpu/tests/range_masks.rs:119`](../../core/dr-gpu/tests/range_masks.rs#L119), [`core/dr-gpu/tests/range_masks.rs:147`](../../core/dr-gpu/tests/range_masks.rs#L147), [`core/dr-gpu/tests/range_masks.rs:175`](../../core/dr-gpu/tests/range_masks.rs#L175), [`core/dr-gpu/tests/range_masks.rs:1`](../../core/dr-gpu/tests/range_masks.rs#L1), [`core/dr-gpu/tests/range_masks.rs:94`](../../core/dr-gpu/tests/range_masks.rs#L94), [`core/dr-pipeline/src/mask.rs:125`](../../core/dr-pipeline/src/mask.rs#L125), [`core/dr-pipeline/src/mask.rs:2890`](../../core/dr-pipeline/src/mask.rs#L2890), [`core/dr-pipeline/src/mask.rs:2903`](../../core/dr-pipeline/src/mask.rs#L2903), [`core/dr-pipeline/src/mask.rs:2922`](../../core/dr-pipeline/src/mask.rs#L2922), [`core/dr-pipeline/src/mask.rs:2936`](../../core/dr-pipeline/src/mask.rs#L2936), [`core/dr-pipeline/src/mask.rs:2946`](../../core/dr-pipeline/src/mask.rs#L2946), [`core/dr-pipeline/src/mask.rs:710`](../../core/dr-pipeline/src/mask.rs#L710), [`core/dr-pipeline/src/mask.rs:749`](../../core/dr-pipeline/src/mask.rs#L749), [`core/dr-pipeline/src/mask.rs:809`](../../core/dr-pipeline/src/mask.rs#L809), [`core/dr-pipeline/src/mask.rs:828`](../../core/dr-pipeline/src/mask.rs#L828), [`core/dr-pipeline/src/mask.rs:852`](../../core/dr-pipeline/src/mask.rs#L852), [`core/dr-pipeline/src/mask.rs:864`](../../core/dr-pipeline/src/mask.rs#L864), [`core/dr-pipeline/src/mask.rs:876`](../../core/dr-pipeline/src/mask.rs#L876), [`core/dr-pipeline/src/sidecar.rs:1369`](../../core/dr-pipeline/src/sidecar.rs#L1369), [`core/dr-pipeline/src/sidecar.rs:1548`](../../core/dr-pipeline/src/sidecar.rs#L1548), [`core/dr-pipeline/src/sidecar.rs:1637`](../../core/dr-pipeline/src/sidecar.rs#L1637), [`core/dr-pipeline/src/sidecar.rs:1741`](../../core/dr-pipeline/src/sidecar.rs#L1741), [`core/dr-pipeline/src/sidecar.rs:1949`](../../core/dr-pipeline/src/sidecar.rs#L1949), [`core/dr-pipeline/tests/mask_sidecar.rs:1157`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1157), [`core/dr-pipeline/tests/mask_sidecar.rs:1186`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1186), [`core/dr-pipeline/tests/mask_sidecar.rs:1204`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1204), [`core/dr-pipeline/tests/mask_sidecar.rs:1234`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1234), [`ui/dr-ui/src/develop/mask_ops.rs:341`](../../ui/dr-ui/src/develop/mask_ops.rs#L341), [`ui/dr-ui/src/develop/mask_ops.rs:638`](../../ui/dr-ui/src/develop/mask_ops.rs#L638), [`ui/dr-ui/src/develop/mask_ops.rs:665`](../../ui/dr-ui/src/develop/mask_ops.rs#L665), [`ui/dr-ui/src/develop/mask_ops.rs:697`](../../ui/dr-ui/src/develop/mask_ops.rs#L697), [`ui/dr-ui/src/develop/mask_ops.rs:721`](../../ui/dr-ui/src/develop/mask_ops.rs#L721), [`ui/dr-ui/src/develop/mask_ops.rs:727`](../../ui/dr-ui/src/develop/mask_ops.rs#L727), [`ui/dr-ui/src/develop/mask_ops.rs:734`](../../ui/dr-ui/src/develop/mask_ops.rs#L734), [`ui/dr-ui/src/develop/mask_ops.rs:754`](../../ui/dr-ui/src/develop/mask_ops.rs#L754), [`ui/dr-ui/src/labels.rs:63`](../../ui/dr-ui/src/labels.rs#L63), [`ui/dr-ui/src/masks_ui.rs:1216`](../../ui/dr-ui/src/masks_ui.rs#L1216), [`ui/dr-ui/src/masks_ui.rs:797`](../../ui/dr-ui/src/masks_ui.rs#L797) | | FR-DEV-12 | [`core/dr-pipeline/ops/colour_grading.yaml:1`](../../core/dr-pipeline/ops/colour_grading.yaml#L1) | | FR-DEV-16 | [`ui/dr-ui/src/lib.rs:441`](../../ui/dr-ui/src/lib.rs#L441), [`ui/dr-ui/src/masks_ui.rs:713`](../../ui/dr-ui/src/masks_ui.rs#L713), [`ui/dr-ui/ui/adjust.slint:956`](../../ui/dr-ui/ui/adjust.slint#L956), [`ui/dr-ui/ui/app.slint:2398`](../../ui/dr-ui/ui/app.slint#L2398), [`ui/dr-ui/ui/app.slint:2411`](../../ui/dr-ui/ui/app.slint#L2411), [`ui/dr-ui/ui/masks.slint:853`](../../ui/dr-ui/ui/masks.slint#L853) | -| FR-DEV-17 | [`core/dr-pipeline/src/orphan.rs:145`](../../core/dr-pipeline/src/orphan.rs#L145), [`core/dr-pipeline/src/orphan.rs:1`](../../core/dr-pipeline/src/orphan.rs#L1), [`ui/dr-ui/src/develop/framing.rs:1109`](../../ui/dr-ui/src/develop/framing.rs#L1109), [`ui/dr-ui/src/develop/framing.rs:133`](../../ui/dr-ui/src/develop/framing.rs#L133), [`ui/dr-ui/src/develop/framing.rs:169`](../../ui/dr-ui/src/develop/framing.rs#L169), [`ui/dr-ui/src/develop/framing.rs:235`](../../ui/dr-ui/src/develop/framing.rs#L235), [`ui/dr-ui/src/develop/framing.rs:243`](../../ui/dr-ui/src/develop/framing.rs#L243), [`ui/dr-ui/src/develop/session.rs:110`](../../ui/dr-ui/src/develop/session.rs#L110), [`ui/dr-ui/src/develop_ui.rs:1061`](../../ui/dr-ui/src/develop_ui.rs#L1061), [`ui/dr-ui/src/develop_ui.rs:972`](../../ui/dr-ui/src/develop_ui.rs#L972), [`ui/dr-ui/src/develop_ui.rs:998`](../../ui/dr-ui/src/develop_ui.rs#L998), [`ui/dr-ui/src/lib.rs:2207`](../../ui/dr-ui/src/lib.rs#L2207), [`ui/dr-ui/src/lib.rs:613`](../../ui/dr-ui/src/lib.rs#L613), [`ui/dr-ui/ui/adjust.slint:683`](../../ui/dr-ui/ui/adjust.slint#L683), [`ui/dr-ui/ui/adjust.slint:687`](../../ui/dr-ui/ui/adjust.slint#L687), [`ui/dr-ui/ui/app.slint:2641`](../../ui/dr-ui/ui/app.slint#L2641), [`ui/dr-ui/ui/crop.slint:51`](../../ui/dr-ui/ui/crop.slint#L51) | +| FR-DEV-17 | [`core/dr-pipeline/src/orphan.rs:142`](../../core/dr-pipeline/src/orphan.rs#L142), [`core/dr-pipeline/src/orphan.rs:1`](../../core/dr-pipeline/src/orphan.rs#L1), [`ui/dr-ui/src/develop/framing.rs:1109`](../../ui/dr-ui/src/develop/framing.rs#L1109), [`ui/dr-ui/src/develop/framing.rs:133`](../../ui/dr-ui/src/develop/framing.rs#L133), [`ui/dr-ui/src/develop/framing.rs:169`](../../ui/dr-ui/src/develop/framing.rs#L169), [`ui/dr-ui/src/develop/framing.rs:235`](../../ui/dr-ui/src/develop/framing.rs#L235), [`ui/dr-ui/src/develop/framing.rs:243`](../../ui/dr-ui/src/develop/framing.rs#L243), [`ui/dr-ui/src/develop/session.rs:110`](../../ui/dr-ui/src/develop/session.rs#L110), [`ui/dr-ui/src/develop_ui.rs:1061`](../../ui/dr-ui/src/develop_ui.rs#L1061), [`ui/dr-ui/src/develop_ui.rs:972`](../../ui/dr-ui/src/develop_ui.rs#L972), [`ui/dr-ui/src/develop_ui.rs:998`](../../ui/dr-ui/src/develop_ui.rs#L998), [`ui/dr-ui/src/lib.rs:2207`](../../ui/dr-ui/src/lib.rs#L2207), [`ui/dr-ui/src/lib.rs:613`](../../ui/dr-ui/src/lib.rs#L613), [`ui/dr-ui/ui/adjust.slint:683`](../../ui/dr-ui/ui/adjust.slint#L683), [`ui/dr-ui/ui/adjust.slint:687`](../../ui/dr-ui/ui/adjust.slint#L687), [`ui/dr-ui/ui/app.slint:2641`](../../ui/dr-ui/ui/app.slint#L2641), [`ui/dr-ui/ui/crop.slint:51`](../../ui/dr-ui/ui/crop.slint#L51) | | FR-DEV-18 | [`core/dr-pipeline/src/ops/dehaze.rs:1`](../../core/dr-pipeline/src/ops/dehaze.rs#L1), [`core/dr-pipeline/src/ops/dehaze.rs:221`](../../core/dr-pipeline/src/ops/dehaze.rs#L221) | | FR-DEV-19 | [`core/dr-pipeline/src/mask.rs:1341`](../../core/dr-pipeline/src/mask.rs#L1341) | | FR-DEV-19a | [`core/dr-gpu/src/mask.rs:655`](../../core/dr-gpu/src/mask.rs#L655), [`core/dr-gpu/src/mask.rs:773`](../../core/dr-gpu/src/mask.rs#L773), [`core/dr-gpu/tests/local_adjustments.rs:793`](../../core/dr-gpu/tests/local_adjustments.rs#L793), [`core/dr-gpu/tests/local_adjustments.rs:841`](../../core/dr-gpu/tests/local_adjustments.rs#L841), [`core/dr-pipeline/src/mask.rs:1022`](../../core/dr-pipeline/src/mask.rs#L1022), [`core/dr-pipeline/src/mask.rs:1633`](../../core/dr-pipeline/src/mask.rs#L1633), [`core/dr-pipeline/src/mask.rs:2964`](../../core/dr-pipeline/src/mask.rs#L2964), [`core/dr-pipeline/src/mask.rs:3085`](../../core/dr-pipeline/src/mask.rs#L3085), [`core/dr-pipeline/src/mask.rs:3120`](../../core/dr-pipeline/src/mask.rs#L3120), [`core/dr-pipeline/src/mask.rs:929`](../../core/dr-pipeline/src/mask.rs#L929), [`core/dr-pipeline/src/mask.rs:964`](../../core/dr-pipeline/src/mask.rs#L964), [`core/dr-pipeline/src/sidecar.rs:1259`](../../core/dr-pipeline/src/sidecar.rs#L1259), [`core/dr-pipeline/tests/mask_sidecar.rs:1347`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1347), [`core/dr-pipeline/tests/mask_sidecar.rs:1369`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1369), [`core/dr-pipeline/tests/mask_sidecar.rs:1395`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1395), [`ui/dr-ui/src/develop/masks.rs:431`](../../ui/dr-ui/src/develop/masks.rs#L431), [`ui/dr-ui/src/develop/masks.rs:492`](../../ui/dr-ui/src/develop/masks.rs#L492), [`ui/dr-ui/src/develop/masks.rs:511`](../../ui/dr-ui/src/develop/masks.rs#L511), [`ui/dr-ui/src/develop/masks.rs:546`](../../ui/dr-ui/src/develop/masks.rs#L546), [`ui/dr-ui/src/develop/session.rs:192`](../../ui/dr-ui/src/develop/session.rs#L192), [`ui/dr-ui/src/labels.rs:73`](../../ui/dr-ui/src/labels.rs#L73), [`ui/dr-ui/src/masks_ui.rs:1083`](../../ui/dr-ui/src/masks_ui.rs#L1083), [`ui/dr-ui/src/masks_ui.rs:1155`](../../ui/dr-ui/src/masks_ui.rs#L1155), [`ui/dr-ui/src/masks_ui.rs:1514`](../../ui/dr-ui/src/masks_ui.rs#L1514), [`ui/dr-ui/src/masks_ui.rs:1537`](../../ui/dr-ui/src/masks_ui.rs#L1537), [`ui/dr-ui/src/masks_ui.rs:270`](../../ui/dr-ui/src/masks_ui.rs#L270), [`ui/dr-ui/ui/masks.slint:151`](../../ui/dr-ui/ui/masks.slint#L151), [`ui/dr-ui/ui/masks.slint:176`](../../ui/dr-ui/ui/masks.slint#L176), [`ui/dr-ui/ui/masks.slint:338`](../../ui/dr-ui/ui/masks.slint#L338), [`ui/dr-ui/ui/masks.slint:375`](../../ui/dr-ui/ui/masks.slint#L375), [`ui/dr-ui/ui/masks.slint:417`](../../ui/dr-ui/ui/masks.slint#L417), [`ui/dr-ui/ui/masks.slint:463`](../../ui/dr-ui/ui/masks.slint#L463), [`ui/dr-ui/ui/masks.slint:791`](../../ui/dr-ui/ui/masks.slint#L791), [`ui/dr-ui/ui/masks.slint:876`](../../ui/dr-ui/ui/masks.slint#L876) | @@ -224,13 +224,14 @@ Defined in `requirements.md` and marked `(post-v1)` on the defining line. Not in ## Not yet tagged -31 of 189 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built. +32 of 190 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
Show untagged requirements - FR-CAT-14 - FR-CULL-6 - FR-CULL-7 +- FR-DEV-20 - FR-DEV-3g - FR-DSP-2 - FR-INF-2