Let framing say it wants the canvas, instead of the panel knowing
The generated panel opened with a special case:
if op.id == dr_pipeline::framing::ID { continue; }
and a paragraph explaining that framing's eight parameters are eight bad
controls — four crop edges you would have to type coordinates into, a "rotate"
slider running 0..3, two switches — so `GeometryPanel` presents them as the
gestures they are instead.
Every word of that is true, and none of it was the frontend's to know. It is a
fact about the operation, and ARCH §4.3a is explicit that a frontend deciding
things by *naming a stage* is the boundary being crossed: a second frontend
would have had to learn the same special case, and nothing in the capability
output said why it existed.
Framing now declares a `Presentation` preferring `WidgetKind::CropOverlay`,
with a demand of two-dimensional dragging and — deliberately — no precise
pointing, since FR-UI-7 grows the handles to the modality and a crop is
forgiving. The widget owns all eight parameters rather than only the rect: a
frontend taking this on takes the whole framing control surface, and leaving
rotation and the flips behind would scatter them into the generated panel
underneath a crop control that already exists.
The panel's rule is now general. `supported` answers whether a kind is
implemented *anywhere* — drawn in the panel like the tone curve, or hosted on
the canvas like the crop — and `is_on_canvas` settles which afterwards, so the
two cannot disagree about the same kind. Any stage preferring an on-canvas
widget is skipped, with nothing named. A frontend that implements neither still
gets the eight sliders: tedious, complete, and the guarantee the whole hint
mechanism rests on.
**The tests were asserting against the wrong thing.** `rows_of` was a
hand-written simulation of the row generator, complete with its own copy of the
framing skip, so the suite was checking a second implementation kept in step by
hand. It was not in step: giving framing a presentation changed the real panel
and the simulation disagreed, which is exactly how a green suite hides a
regression. It now calls `rows_from`, and `rows_of_unfiltered` is gone.
`framing_is_not_generated_as_sliders` survives but asserts by routing rather
than by counting — no row may carry framing's capability index — so it cannot
be satisfied by two miscounts cancelling out. Alongside it, an invented stage
preferring a widget this frontend lacks, falling back to one the canvas hosts,
must also be skipped: if that ever needs a name added to pass, the special case
has grown back.
Not included: moving the crop overlay's markup out of app.slint into
controls.slint. It is cosmetic next to the above and app.slint is in another
session's working set.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,10 @@
|
||||
use std::f32::consts::PI;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use crate::descriptor::{LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit};
|
||||
use crate::descriptor::{
|
||||
LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation, Scale, Unit,
|
||||
WidgetDemand, WidgetKind,
|
||||
};
|
||||
use crate::operation::Affects;
|
||||
|
||||
pub const ID: OpId = OpId("framing");
|
||||
@@ -62,6 +65,14 @@ pub const CROP_H: ParamId = ParamId("crop_h");
|
||||
/// the edits actually are.
|
||||
pub const MAX_STRAIGHTEN: f32 = 45.0;
|
||||
|
||||
/// The parameters the framing widget owns — every one of them.
|
||||
///
|
||||
/// In the order the widget expects: the rect first, then the angle it is
|
||||
/// straightened by, then the exact reorientations.
|
||||
static FRAMING_PARAMS: [ParamId; 8] = [
|
||||
CROP_X, CROP_Y, CROP_W, CROP_H, ANGLE, ROTATION, FLIP_H, FLIP_V,
|
||||
];
|
||||
|
||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
||||
id: ID,
|
||||
label: LocalizedKey("op.framing"),
|
||||
@@ -242,6 +253,44 @@ impl Framing {
|
||||
&DESCRIPTOR
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3a | FR-DEV-3b | FR-UI-7
|
||||
/// How framing would like to be presented.
|
||||
///
|
||||
/// **This is what stops a frontend having to name this stage.** Rendered
|
||||
/// generically these eight parameters are eight bad controls: four crop
|
||||
/// edges the photographer would have to type coordinates into, a "rotate"
|
||||
/// slider running 0..3, and two switches. Every one of them is a worse
|
||||
/// control than the gesture it stands for — a crop is dragged on the
|
||||
/// photograph and a quarter turn is a button.
|
||||
///
|
||||
/// Before this existed, the frontend knew that by *checking the operation
|
||||
/// id* and skipping it, which is precisely the naming ARCH §4.3a forbids:
|
||||
/// a second frontend would have had to learn the same special case, and
|
||||
/// nothing in the capability output said why. Now the preference is
|
||||
/// declared, the demand says what the widget needs, and a frontend that
|
||||
/// cannot meet it falls back to the eight sliders — tedious, but complete,
|
||||
/// which is the guarantee the whole hint mechanism rests on.
|
||||
///
|
||||
/// The widget owns **all eight** parameters rather than only the rect: a
|
||||
/// frontend that takes this on is taking on the whole framing control
|
||||
/// surface, and leaving rotation and the flips behind would scatter them
|
||||
/// into the generated panel underneath a crop control that already exists.
|
||||
pub fn presentation(&self) -> Option<Presentation> {
|
||||
Some(Presentation {
|
||||
widgets: &[WidgetKind::CropOverlay],
|
||||
demand: WidgetDemand {
|
||||
// A crop rect is dragged by its corners; nothing about that
|
||||
// reduces to one axis at a time.
|
||||
two_dimensional: true,
|
||||
// Deliberately false. The handles are large and a crop is
|
||||
// forgiving — FR-UI-7 has the interaction regions grow to the
|
||||
// modality, so a thumb is as workable as a mouse.
|
||||
precise_pointing: false,
|
||||
},
|
||||
params: &FRAMING_PARAMS,
|
||||
})
|
||||
}
|
||||
|
||||
/// What this stage affects, for invalidation scoping (FR-DEV-3d).
|
||||
pub fn affects(&self) -> Affects {
|
||||
Affects::Geometry
|
||||
|
||||
@@ -186,10 +186,12 @@ impl EditGraph {
|
||||
facet: p.facet,
|
||||
})
|
||||
.collect(),
|
||||
// Framing is not an `Operation`, so it has no `presentation` to
|
||||
// ask for. A crop overlay is a viewport interaction rather than a
|
||||
// panel widget, which is a different mechanism again.
|
||||
presentation: None,
|
||||
// Framing is not an `Operation`, but it has the same thing to say
|
||||
// about how it wants drawing: a crop is dragged on the photograph.
|
||||
// Declaring it here is what lets the frontend skip generating
|
||||
// sliders for framing *without naming framing* — see
|
||||
// `Framing::presentation`.
|
||||
presentation: self.framing.presentation(),
|
||||
};
|
||||
|
||||
ops.chain(std::iter::once(framing)).collect()
|
||||
|
||||
Reference in New Issue
Block a user