Skip to content

Commit 40bfce5

Browse files
Ink Open Sourcecopybara-github
authored andcommitted
Add brush behavior binary operators kAndThen, kOrElse, and kXorElse
These three operators allow for greater control over how to handle null values (e.g. unavailable source data) in brush behaviors: * `kOrElse` returns the first non-null input, allowing for fallback branches in a brush behavior. If a subtree based on some source (like stylus tilt) is unavailable, the behavior can fall back to a substitute implementation. * `kAndThen` returns the second input, but only if the first input is non-null. This can help when two separate behaviors are meant to work together, but one relies on a source that might be unavailable; `kAndThen` can be used in the other behavior to ensure that it also gets disabled when that source is unavailable. * `kXorElse` is more niche, but can be used to combine two subtrees where only one should normally be defined at a time, and there's no defined way to combine them if both are defined. It can also be used with a constant node to form a sort of "logical not" for null values. PiperOrigin-RevId: 875239647
1 parent 7fe5a44 commit 40bfce5

File tree

8 files changed

+163
-4
lines changed

8 files changed

+163
-4
lines changed

ink/brush/brush_behavior.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@ bool IsValidBehaviorBinaryOp(BrushBehavior::BinaryOp operation) {
217217
case BrushBehavior::BinaryOp::kSum:
218218
case BrushBehavior::BinaryOp::kMin:
219219
case BrushBehavior::BinaryOp::kMax:
220+
case BrushBehavior::BinaryOp::kAndThen:
221+
case BrushBehavior::BinaryOp::kOrElse:
222+
case BrushBehavior::BinaryOp::kXorElse:
220223
return true;
221224
}
222225
return false;
@@ -671,6 +674,12 @@ std::string ToFormattedString(BrushBehavior::BinaryOp operation) {
671674
return "kMin";
672675
case BrushBehavior::BinaryOp::kMax:
673676
return "kMax";
677+
case BrushBehavior::BinaryOp::kAndThen:
678+
return "kAndThen";
679+
case BrushBehavior::BinaryOp::kOrElse:
680+
return "kOrElse";
681+
case BrushBehavior::BinaryOp::kXorElse:
682+
return "kXorElse";
674683
}
675684
return absl::StrCat("BinaryOp(", static_cast<int>(operation), ")");
676685
}

ink/brush/brush_behavior.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,9 @@ struct BrushBehavior {
407407
kSum, // A + B
408408
kMin, // min(A, B)
409409
kMax, // max(A, B)
410+
kAndThen, // returns null if A is null, otherwise returns B
411+
kOrElse, // returns A if A isn't null, otherwise returns B
412+
kXorElse, // if exactly one input isn't null, returns it, otherwise null
410413
};
411414
// LINT.ThenChange(
412415
// fuzz_domains.cc:binary_op,

ink/brush/brush_behavior_test.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ TEST(BrushBehaviorTest, StringifyBinaryOp) {
231231
EXPECT_EQ(absl::StrCat(BrushBehavior::BinaryOp::kSum), "kSum");
232232
EXPECT_EQ(absl::StrCat(BrushBehavior::BinaryOp::kMin), "kMin");
233233
EXPECT_EQ(absl::StrCat(BrushBehavior::BinaryOp::kMax), "kMax");
234+
EXPECT_EQ(absl::StrCat(BrushBehavior::BinaryOp::kAndThen), "kAndThen");
235+
EXPECT_EQ(absl::StrCat(BrushBehavior::BinaryOp::kOrElse), "kOrElse");
236+
EXPECT_EQ(absl::StrCat(BrushBehavior::BinaryOp::kXorElse), "kXorElse");
234237
EXPECT_EQ(absl::StrCat(static_cast<BrushBehavior::BinaryOp>(147)),
235238
"BinaryOp(147)");
236239
}

ink/brush/fuzz_domains.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ Domain<BrushBehavior::BinaryOp> ArbitraryBrushBehaviorBinaryOp() {
130130
BrushBehavior::BinaryOp::kSum,
131131
BrushBehavior::BinaryOp::kMin,
132132
BrushBehavior::BinaryOp::kMax,
133+
BrushBehavior::BinaryOp::kAndThen,
134+
BrushBehavior::BinaryOp::kOrElse,
135+
BrushBehavior::BinaryOp::kXorElse,
133136
});
134137
}
135138
// LINT.ThenChange(brush_behavior.h:binary_op)

ink/storage/brush.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ proto::BrushBehavior::BinaryOp EncodeBrushBehaviorBinaryOp(
6161
return proto::BrushBehavior::BINARY_OP_MIN;
6262
case BrushBehavior::BinaryOp::kMax:
6363
return proto::BrushBehavior::BINARY_OP_MAX;
64+
case BrushBehavior::BinaryOp::kAndThen:
65+
return proto::BrushBehavior::BINARY_OP_AND_THEN;
66+
case BrushBehavior::BinaryOp::kOrElse:
67+
return proto::BrushBehavior::BINARY_OP_OR_ELSE;
68+
case BrushBehavior::BinaryOp::kXorElse:
69+
return proto::BrushBehavior::BINARY_OP_XOR_ELSE;
6470
}
6571
return proto::BrushBehavior::BINARY_OP_UNSPECIFIED;
6672
}
@@ -76,6 +82,12 @@ absl::StatusOr<BrushBehavior::BinaryOp> DecodeBrushBehaviorBinaryOp(
7682
return BrushBehavior::BinaryOp::kMin;
7783
case proto::BrushBehavior::BINARY_OP_MAX:
7884
return BrushBehavior::BinaryOp::kMax;
85+
case proto::BrushBehavior::BINARY_OP_AND_THEN:
86+
return BrushBehavior::BinaryOp::kAndThen;
87+
case proto::BrushBehavior::BINARY_OP_OR_ELSE:
88+
return BrushBehavior::BinaryOp::kOrElse;
89+
case proto::BrushBehavior::BINARY_OP_XOR_ELSE:
90+
return BrushBehavior::BinaryOp::kXorElse;
7991
default:
8092
return absl::InvalidArgumentError(absl::StrCat(
8193
"invalid ink.proto.BrushBehavior.BinaryOp value: ", binary_op_proto));

ink/storage/proto/brush_family.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,15 @@ message BrushBehavior {
945945

946946
// max(A, B), or null if either is null
947947
BINARY_OP_MAX = 4;
948+
949+
// null if A is null, or B otherwise
950+
BINARY_OP_AND_THEN = 5;
951+
952+
// A if A isn't null, or B otherwise
953+
BINARY_OP_OR_ELSE = 6;
954+
955+
// A if B is null, or B if A is null, or null if neither is null
956+
BINARY_OP_XOR_ELSE = 7;
948957
}
949958
// LINT.ThenChange(../../brush/brush_behavior.h:binary_op)
950959

ink/strokes/internal/brush_tip_modeler_helpers.cc

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -480,11 +480,25 @@ void ProcessBehaviorNodeImpl(const BrushBehavior::BinaryOpNode& node,
480480
context.stack.pop_back();
481481
float first_input = context.stack.back();
482482
float* result = &context.stack.back();
483-
if (IsNullBehaviorNodeValue(first_input) ||
484-
IsNullBehaviorNodeValue(second_input)) {
485-
*result = kNullBehaviorNodeValue;
486-
return;
483+
484+
// Some operations always return null if either input is null.
485+
switch (node.operation) {
486+
case BrushBehavior::BinaryOp::kProduct:
487+
case BrushBehavior::BinaryOp::kSum:
488+
case BrushBehavior::BinaryOp::kMin:
489+
case BrushBehavior::BinaryOp::kMax:
490+
case BrushBehavior::BinaryOp::kAndThen:
491+
if (IsNullBehaviorNodeValue(first_input) ||
492+
IsNullBehaviorNodeValue(second_input)) {
493+
*result = kNullBehaviorNodeValue;
494+
return;
495+
}
496+
break;
497+
case BrushBehavior::BinaryOp::kOrElse:
498+
case BrushBehavior::BinaryOp::kXorElse:
499+
break;
487500
}
501+
488502
switch (node.operation) {
489503
case BrushBehavior::BinaryOp::kProduct:
490504
*result = first_input * second_input;
@@ -498,7 +512,21 @@ void ProcessBehaviorNodeImpl(const BrushBehavior::BinaryOpNode& node,
498512
case BrushBehavior::BinaryOp::kMax:
499513
*result = std::max(first_input, second_input);
500514
break;
515+
case BrushBehavior::BinaryOp::kAndThen:
516+
*result = second_input;
517+
break;
518+
case BrushBehavior::BinaryOp::kOrElse:
519+
*result =
520+
IsNullBehaviorNodeValue(first_input) ? second_input : first_input;
521+
break;
522+
case BrushBehavior::BinaryOp::kXorElse:
523+
*result = IsNullBehaviorNodeValue(first_input) ? second_input
524+
: IsNullBehaviorNodeValue(second_input)
525+
? first_input
526+
: kNullBehaviorNodeValue;
527+
break;
501528
}
529+
502530
// If any of the above operations resulted in a non-finite value
503531
// (e.g. overflow to infinity), treat the result as null.
504532
if (!std::isfinite(*result)) {

ink/strokes/internal/brush_tip_modeler_helpers_test.cc

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,98 @@ TEST_F(ProcessBehaviorNodeTest, BinaryOpNodeMax) {
13461346
EXPECT_THAT(stack_, ElementsAre(NullNodeValueMatcher()));
13471347
}
13481348

1349+
TEST_F(ProcessBehaviorNodeTest, BinaryOpNodeAndThen) {
1350+
BrushBehavior::BinaryOpNode binary_op_node = {
1351+
.operation = BrushBehavior::BinaryOp::kAndThen};
1352+
1353+
// `kAndThen` returns the second input as long as the first input isn't null.
1354+
stack_.push_back(2.0f);
1355+
stack_.push_back(7.0f);
1356+
ProcessBehaviorNode(binary_op_node, context_);
1357+
EXPECT_THAT(stack_, ElementsAre(7.0f));
1358+
1359+
stack_.clear();
1360+
stack_.push_back(2.0f);
1361+
stack_.push_back(kNullBehaviorNodeValue);
1362+
ProcessBehaviorNode(binary_op_node, context_);
1363+
EXPECT_THAT(stack_, ElementsAre(NullNodeValueMatcher()));
1364+
1365+
// If the first input is null, `kAndThen` always returns null.
1366+
stack_.clear();
1367+
stack_.push_back(kNullBehaviorNodeValue);
1368+
stack_.push_back(7.0f);
1369+
ProcessBehaviorNode(binary_op_node, context_);
1370+
EXPECT_THAT(stack_, ElementsAre(NullNodeValueMatcher()));
1371+
1372+
stack_.clear();
1373+
stack_.push_back(kNullBehaviorNodeValue);
1374+
stack_.push_back(kNullBehaviorNodeValue);
1375+
ProcessBehaviorNode(binary_op_node, context_);
1376+
EXPECT_THAT(stack_, ElementsAre(NullNodeValueMatcher()));
1377+
}
1378+
1379+
TEST_F(ProcessBehaviorNodeTest, BinaryOpNodeOrElse) {
1380+
BrushBehavior::BinaryOpNode binary_op_node = {
1381+
.operation = BrushBehavior::BinaryOp::kOrElse};
1382+
1383+
// `kOrElse` returns the first input as long as it isn't null.
1384+
stack_.push_back(2.0f);
1385+
stack_.push_back(7.0f);
1386+
ProcessBehaviorNode(binary_op_node, context_);
1387+
EXPECT_THAT(stack_, ElementsAre(2.0f));
1388+
1389+
stack_.clear();
1390+
stack_.push_back(2.0f);
1391+
stack_.push_back(kNullBehaviorNodeValue);
1392+
ProcessBehaviorNode(binary_op_node, context_);
1393+
EXPECT_THAT(stack_, ElementsAre(2.0f));
1394+
1395+
// If the first input is null, `kOrElse` returns the second input.
1396+
stack_.clear();
1397+
stack_.push_back(kNullBehaviorNodeValue);
1398+
stack_.push_back(7.0f);
1399+
ProcessBehaviorNode(binary_op_node, context_);
1400+
EXPECT_THAT(stack_, ElementsAre(7.0f));
1401+
1402+
stack_.clear();
1403+
stack_.push_back(kNullBehaviorNodeValue);
1404+
stack_.push_back(kNullBehaviorNodeValue);
1405+
ProcessBehaviorNode(binary_op_node, context_);
1406+
EXPECT_THAT(stack_, ElementsAre(NullNodeValueMatcher()));
1407+
}
1408+
1409+
TEST_F(ProcessBehaviorNodeTest, BinaryOpNodeXorElse) {
1410+
BrushBehavior::BinaryOpNode binary_op_node = {
1411+
.operation = BrushBehavior::BinaryOp::kXorElse};
1412+
1413+
// If neither input is null, `kXorElse` returns null.
1414+
stack_.push_back(2.0f);
1415+
stack_.push_back(7.0f);
1416+
ProcessBehaviorNode(binary_op_node, context_);
1417+
EXPECT_THAT(stack_, ElementsAre(NullNodeValueMatcher()));
1418+
1419+
// If only the first input is null, `kXorElse` returns the second input.
1420+
stack_.clear();
1421+
stack_.push_back(kNullBehaviorNodeValue);
1422+
stack_.push_back(7.0f);
1423+
ProcessBehaviorNode(binary_op_node, context_);
1424+
EXPECT_THAT(stack_, ElementsAre(7.0f));
1425+
1426+
// If only the second input is null, `kXorElse` returns the first input.
1427+
stack_.clear();
1428+
stack_.push_back(2.0f);
1429+
stack_.push_back(kNullBehaviorNodeValue);
1430+
ProcessBehaviorNode(binary_op_node, context_);
1431+
EXPECT_THAT(stack_, ElementsAre(2.0f));
1432+
1433+
// If both inputs are null, `kXorElse` returns null.
1434+
stack_.clear();
1435+
stack_.push_back(kNullBehaviorNodeValue);
1436+
stack_.push_back(kNullBehaviorNodeValue);
1437+
ProcessBehaviorNode(binary_op_node, context_);
1438+
EXPECT_THAT(stack_, ElementsAre(NullNodeValueMatcher()));
1439+
}
1440+
13491441
TEST_F(ProcessBehaviorNodeTest, InterpolationNodeLerp) {
13501442
BrushBehavior::InterpolationNode interpolation_node = {
13511443
.interpolation = BrushBehavior::Interpolation::kLerp,

0 commit comments

Comments
 (0)