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
+5 -5
View File
@@ -59,7 +59,7 @@ use crate::graph::EditGraph;
///
/// It also dissolves a special case. Framing used to be excluded by an
/// explicit test against one operation's id; it is now excluded because
/// [`Attribute::Geometry`] is not in the default set, and the argument below
/// [`Attribute::Compose`] is not in the default set, and the argument below
/// is a statement about a kind of edit rather than about a particular node.
///
/// # Why geometry is out by default
@@ -116,7 +116,7 @@ impl Scope {
/// its shape. The default.
pub fn adjustments() -> Self {
Self {
bits: Self::all_bits() & !Self::bit(Attribute::Geometry),
bits: Self::all_bits() & !Self::bit(Attribute::Compose),
carries_unclassified: true,
}
}
@@ -278,7 +278,7 @@ impl Preset {
pub fn touches_framing(&self) -> bool {
self.params
.keys()
.any(|(op, _)| attributes_of(op).is_some_and(|a| a.contains(&Attribute::Geometry)))
.any(|(op, _)| attributes_of(op).is_some_and(|a| a.contains(&Attribute::Compose)))
}
/// How many operations this preset touches, in scope.
@@ -1254,8 +1254,8 @@ mod tests {
.into_iter()
.filter(|a| !default_scope.has(*a))
.collect();
assert_eq!(excluded, vec![Attribute::Geometry]);
assert!(Scope::everything().has(Attribute::Geometry));
assert_eq!(excluded, vec![Attribute::Compose]);
assert!(Scope::everything().has(Attribute::Compose));
}
#[test]