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:
2026-09-24 21:25:48 -04:00
parent 8cdad3863d
commit 5f0b7ac799
3 changed files with 56 additions and 8 deletions
+28 -7
View File
@@ -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