Name the frame's category for the decision, not the maths

`Attribute::Geometry` becomes `Attribute::Compose`, and `film_sim` moves
from `[tone, colour]` to `[effect]`.

Two categories were doing the wrong job. "Geometry" describes what crop,
straighten and the quarter turns do to coordinates — but it describes lens
distortion correction exactly as well, and that is not a compositional
choice at all. Naming the attribute for the photographer's decision is what
separates it from `Optics`: one is what the lens did, the other is what
they chose. The maths the two have in common is not the thing worth
filing them under.

A film stock declared both `tone` and `colour`, so "Kodachrome" appeared in
the Light group beside exposure and again in Colour beside white balance —
two places, neither of which is where anyone looks for it. It is neither:
`Effect` is defined in this same file as "applied rather than corrected — a
look, not a fix", which is what a stock is. That it moves tone and colour
is true of every look, and is not what the attribute is for.

`from_name` still accepts "geometry" on the way in. That string is
persisted in `develop.copy_attributes`, and an entry it fails to parse is
not an error — `presets::scope_for` logs it and drops it — so without the
alias an existing settings file would have quietly narrowed what a paste
carries. `name` writes the current spelling, so the file migrates itself
the first time it is saved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-05 14:34:03 +02:00
co-authored by Claude Opus 5
parent 95e854b5a2
commit 8e7b1350bf
14 changed files with 55 additions and 34 deletions
+7 -7
View File
@@ -1021,15 +1021,15 @@ impl DevelopSession {
/// (FR-DEV-3a). An attribute nothing carries is left out rather than
/// offered as a tab that opens onto nothing.
///
/// Geometry is excluded: its one operation prefers an on-canvas widget and
/// is skipped by the row builder, so a Geometry tab would be empty of rows
/// while `GeometryPanel` holds the real controls.
/// Compose is excluded: its one operation prefers an on-canvas widget and
/// is skipped by the row builder, so a Compose tab would be empty of rows
/// while `ComposePanel` holds the real controls.
pub fn tabs(&self) -> Vec<(dr_pipeline::Attribute, String)> {
use dr_pipeline::Attribute;
let caps = self.scoped_capabilities();
Attribute::ALL
.into_iter()
.filter(|a| *a != Attribute::Geometry)
.filter(|a| *a != Attribute::Compose)
.filter(|a| {
caps.iter().any(|c| {
c.attributes.contains(a)
@@ -1171,7 +1171,7 @@ pub(crate) fn supported(widget: WidgetKind) -> bool {
// Drawn in the panel.
WidgetKind::ToneCurve => true,
// Hosted on the canvas: the overlay is drawn over the photograph and
// the panel contributes `GeometryPanel`, the affordance that turns it
// the panel contributes `ComposePanel`, the affordance that turns it
// on.
WidgetKind::CropOverlay => true,
// Not implemented. Listed rather than caught by a wildcard so the next
@@ -1254,7 +1254,7 @@ pub(crate) fn rows_filtered(
// terms, with nothing named.
//
// Skipped rather than rendered as an affordance row, because
// the affordance is `GeometryPanel` — a bespoke control for a
// the affordance is `ComposePanel` — a bespoke control for a
// known stage, which is a thing the interface is entitled to
// build (ARCH §4.3a draws the line at the *generated* panel
// naming stages, not at the interface having hand-made
@@ -5748,7 +5748,7 @@ mod tests {
#[test]
fn framing_is_not_generated_as_sliders() {
// `GeometryPanel` presents crop, rotation, flips and straightening as
// `ComposePanel` presents crop, rotation, flips and straightening as
// the gestures they are. If the generic path emitted them too the
// sidebar would carry both — including four "Crop Left/Top/Width/
// Height" sliders no one can compose a photograph with.
+1 -1
View File
@@ -127,7 +127,7 @@ fn catalogued(key: &str) -> Option<&'static str> {
"attr.colour" => "Colour",
"attr.detail" => "Detail",
"attr.optics" => "Optics",
"attr.geometry" => "Geometry",
"attr.compose" => "Compose",
"attr.effect" => "Effects",
"op.white_balance" => "White Balance",
+2 -2
View File
@@ -210,7 +210,7 @@ fn label_for(attribute: dr_pipeline::Attribute) -> &'static str {
Attribute::Colour => "Colour",
Attribute::Detail => "Detail",
Attribute::Optics => "Optics",
Attribute::Geometry => "Geometry",
Attribute::Compose => "Compose",
Attribute::Effect => "Effect",
}
}
@@ -548,7 +548,7 @@ pub fn render(
// holding no crop loses nothing to the setting, and saying so anyway would
// train the user to ignore the line.
window.set_settings_framing_withheld(
clipboard.holds_framing() && !scope.has(dr_pipeline::Attribute::Geometry),
clipboard.holds_framing() && !scope.has(dr_pipeline::Attribute::Compose),
);
}
+2 -2
View File
@@ -535,7 +535,7 @@ component ParamControl inherits Rectangle {
// This does not weaken ARCH §4.3: nothing here reads a parameter *value* out
// of a descriptor or routes by index. It calls named session actions, which is
// what a bespoke widget for a known stage is entitled to do.
export component GeometryPanel inherits Rectangle {
export component ComposePanel inherits Rectangle {
in property <bool> enabled: true;
in property <float> angle: 0.0;
in property <float> max-straighten: 45.0;
@@ -586,7 +586,7 @@ export component GeometryPanel inherits Rectangle {
// naming the group, flagging that it holds an edit, and offering the
// reset — without the hiding.
GroupHeading {
title: "GEOMETRY";
title: "COMPOSE";
modified: root.modified;
has-reset: root.modified;
reset => { root.reset(); }
+2 -2
View File
@@ -1,5 +1,5 @@
import { Theme } from "theme.slint";
import { AdjustPanel, GeometryPanel, GroupStrip, ParamRow, TransferPanel, ViewMode } from "adjust.slint";
import { AdjustPanel, ComposePanel, GroupStrip, ParamRow, TransferPanel, ViewMode } from "adjust.slint";
import { CategoryRow, GradientHandle, GradientHandles, HandleRole, MaskPanel, MaskRow, SubjectRow } from "masks.slint";
import { SpotHandle, SpotHandles, SpotPanel, SpotRole } from "spots.slint";
import { CropOverlay } from "crop.slint";
@@ -2471,7 +2471,7 @@ in property <bool> panel-visible: true;
// made rather than how it is applied: the frame is decided
// by eye first and the pipeline runs it last (see
// `dr_pipeline::framing` on the coordinate order).
if !root.local-mode && !root.repairing: GeometryPanel {
if !root.local-mode && !root.repairing: ComposePanel {
enabled: root.adjust-enabled;
angle: root.straighten;
max-straighten: root.max-straighten;
+1 -1
View File
@@ -121,7 +121,7 @@ export component HistoryPanel inherits Rectangle {
spacing: Theme.gap-sm;
alignment: start;
// A heading, not a collapsible. `GeometryPanel` carries the argument
// A heading, not a collapsible. `ComposePanel` carries the argument
// and it holds here: the list is last in the column, so what a lid
// would save is scrolling past nothing.
HorizontalLayout {