Offer intersection in the mask panel: an Intersect button and a third chip state
The pipeline could now keep only where two selections agree, but the panel had no way to ask for it: the part row's chip flipped between + and -, and the buttons under the parts joined an added or a subtracted correction. An "∩ Intersect" button joins a painted part that intersects, and the chip on a part row cycles + -> - -> ∩ and round, so an existing part can be turned into an intersection without being repainted. Both go through the same session calls as before, indexing Join::ALL, whose first two entries kept their places. The chip is now a tagged gesture, so it is in the gesture book.
This commit is contained in:
@@ -543,7 +543,9 @@ impl DevelopSession {
|
||||
.record(&self.graph, Edit::Action(labels::step::MASK_PART_REMOVED));
|
||||
}
|
||||
|
||||
/// Change how a part joins: added to the mask, or taken out of it.
|
||||
/// TRACES: FR-DEV-19a
|
||||
/// Change how a part joins: added to the mask, taken out of it, or kept
|
||||
/// only where the mask already was. `join` indexes `Join::ALL`.
|
||||
pub fn set_mask_part_join(&mut self, id: &str, index: usize, join: usize) {
|
||||
use dr_pipeline::mask::Join;
|
||||
let Some(&join) = Join::ALL.get(join) else {
|
||||
|
||||
@@ -1534,6 +1534,31 @@ mod tests {
|
||||
assert_eq!(s.mask_feather(&id), 0.03, "and the base was left alone");
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-19a
|
||||
/// The panel's third join reaches the layer: the "∩ Intersect" button
|
||||
/// joins an intersecting part, the part row reports it as the chip's third
|
||||
/// state, and cycling the chip on from there comes back round to a union.
|
||||
#[test]
|
||||
fn the_intersect_button_joins_a_part_that_intersects() {
|
||||
use dr_pipeline::mask::Join;
|
||||
let Some(mut s) = session() else {
|
||||
eprintln!("no adapter; skipping");
|
||||
return;
|
||||
};
|
||||
let id = s.add_gradient_mask(true).expect("radial");
|
||||
s.set_active_mask(Some(&id));
|
||||
|
||||
let index = s.add_mask_part(&id, 2).expect("a part was joined");
|
||||
let join = |s: &DevelopSession| {
|
||||
s.masks().get(&id).expect("layer").part(index).expect("part").join
|
||||
};
|
||||
assert_eq!(join(&s), Join::Intersect);
|
||||
assert_eq!(s.mask_parts(&id)[index].2, 2, "the chip shows ∩");
|
||||
|
||||
s.set_mask_part_join(&id, index, (2 + 1) % 3);
|
||||
assert_eq!(join(&s), Join::Union, "and the chip cycles back to +");
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3 | FR-UI-1
|
||||
/// The fault this pass exists for, stated as a test.
|
||||
///
|
||||
|
||||
+28
-7
@@ -89,7 +89,8 @@ export struct PartRow {
|
||||
id: string,
|
||||
/// What this part selects — "brush", "subject", "radial".
|
||||
label: string,
|
||||
/// 0 adds to the mask, 1 takes away from it. Meaningless on the first.
|
||||
/// 0 adds to the mask, 1 takes away from it, 2 keeps only where both
|
||||
/// are (the order of `Join::ALL`). Meaningless on the first.
|
||||
join: int,
|
||||
/// Whether the brush and the edge controls point at this part.
|
||||
selected: bool,
|
||||
@@ -178,7 +179,7 @@ component MaskEntry inherits Rectangle {
|
||||
callback part-join-picked(int, int);
|
||||
callback part-hidden-toggled(int, bool);
|
||||
callback part-removed(int);
|
||||
/// Join a fresh painted correction: 0 adds, 1 subtracts.
|
||||
/// Join a fresh painted correction: 0 adds, 1 subtracts, 2 intersects.
|
||||
callback part-added(int);
|
||||
/// TRACES: FR-DEV-19c
|
||||
callback shown-toggled(bool);
|
||||
@@ -362,20 +363,30 @@ component MaskEntry inherits Rectangle {
|
||||
padding-right: Theme.gap-sm;
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
// One chip, two states, because there are two ways to
|
||||
// join and a photographer flips between them while
|
||||
// looking at the picture rather than at a menu.
|
||||
// GESTURE: Change how a part joins its mask
|
||||
// where: Develop
|
||||
// touch: Tap the + / − / ∩ chip on the part's row
|
||||
// pointer: Click the + / − / ∩ chip on the part's row
|
||||
// why: A chip that cycles rather than a menu, because
|
||||
// a photographer flips a join while looking at
|
||||
// the picture, not at the panel: add, take
|
||||
// away, keep only where both agree, and round.
|
||||
//
|
||||
// TRACES: FR-DEV-19a
|
||||
join := TouchArea {
|
||||
width: Theme.touch-target;
|
||||
mouse-cursor: part.base ? MouseCursor.default : MouseCursor.pointer;
|
||||
enabled: root.enabled && !part.base;
|
||||
clicked => { root.part-join-picked(i, part.join == 0 ? 1 : 0); }
|
||||
clicked => { root.part-join-picked(i, Math.mod(part.join + 1, 3)); }
|
||||
|
||||
Label {
|
||||
// The first part joins nothing — a chip saying
|
||||
// "adds" on it would be a control that moves and
|
||||
// changes no pixel.
|
||||
text: part.base ? "◆" : (part.join == 0 ? "+" : "−");
|
||||
text: part.base ? "◆"
|
||||
: part.join == 0 ? "+"
|
||||
: part.join == 1 ? "−"
|
||||
: "∩";
|
||||
emphasised: join.has-hover || part.selected;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
@@ -448,6 +459,16 @@ component MaskEntry inherits Rectangle {
|
||||
enabled: root.enabled;
|
||||
clicked => { root.part-added(1); }
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-19a
|
||||
// Keep only what this part also covers: paint where the mask
|
||||
// should survive and everything else falls away — the sky
|
||||
// that is also bright, the subject that is also skin.
|
||||
Button {
|
||||
text: "∩ Intersect";
|
||||
enabled: root.enabled;
|
||||
clicked => { root.part-added(2); }
|
||||
}
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-19b
|
||||
|
||||
Reference in New Issue
Block a user