704 lines
No EOL
16 KiB
C++
704 lines
No EOL
16 KiB
C++
// Copyright 2020 Phyronnaz
|
|
|
|
#pragma once
|
|
|
|
#include <limits>
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "VoxelValue.h"
|
|
#include "VoxelMinimal.h"
|
|
#include "VoxelUtilities/VoxelBaseUtilities.h"
|
|
#include "VoxelRange.generated.h"
|
|
|
|
class VOXEL_API FVoxelRangeFailStatus : private TThreadSingleton<FVoxelRangeFailStatus>
|
|
{
|
|
public:
|
|
static FVoxelRangeFailStatus& Get();
|
|
|
|
bool HasFailed() const { return bHasFailed; }
|
|
bool HasWarning() const { return bHasWarning; }
|
|
const TCHAR* GetMessage() const { return Message; }
|
|
|
|
public:
|
|
void Fail(const TCHAR* InError)
|
|
{
|
|
// Note: bHasFailed might be true already if the generated graph has scoped ifs that failed
|
|
if (!HasFailed())
|
|
{
|
|
bHasFailed = true;
|
|
Message = InError;
|
|
}
|
|
}
|
|
void Warning(const TCHAR* InError)
|
|
{
|
|
if (!HasFailed() && !HasWarning())
|
|
{
|
|
bHasWarning = true;
|
|
Message = InError;
|
|
}
|
|
}
|
|
void Reset()
|
|
{
|
|
bHasFailed = false;
|
|
bHasWarning = false;
|
|
Message = nullptr;
|
|
}
|
|
|
|
private:
|
|
bool bHasFailed = false;
|
|
bool bHasWarning = false;
|
|
|
|
const TCHAR* Message = nullptr;
|
|
|
|
friend TThreadSingleton<FVoxelRangeFailStatus>;
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct FVoxelBoolRange
|
|
{
|
|
bool bCanBeTrue = true;
|
|
bool bCanBeFalse = true;
|
|
|
|
FVoxelBoolRange() = default;
|
|
FVoxelBoolRange(bool bValue)
|
|
{
|
|
if (bValue)
|
|
{
|
|
bCanBeTrue = true;
|
|
bCanBeFalse = false;
|
|
}
|
|
else
|
|
{
|
|
bCanBeTrue = false;
|
|
bCanBeFalse = true;
|
|
}
|
|
}
|
|
FVoxelBoolRange(bool bCanBeTrue, bool bCanBeFalse)
|
|
: bCanBeTrue(bCanBeTrue)
|
|
, bCanBeFalse(bCanBeFalse)
|
|
{
|
|
check(bCanBeTrue || bCanBeFalse);
|
|
}
|
|
|
|
FString ToString() const
|
|
{
|
|
return bCanBeTrue && bCanBeFalse ? "true, false" : bCanBeTrue ? "true" : "false";
|
|
}
|
|
|
|
FVoxelBoolRange operator!() const
|
|
{
|
|
return { bCanBeFalse, bCanBeTrue };
|
|
}
|
|
FVoxelBoolRange operator&&(const FVoxelBoolRange& Other) const
|
|
{
|
|
if (!bCanBeFalse && !Other.bCanBeFalse)
|
|
{
|
|
return FVoxelBoolRange::True();
|
|
}
|
|
else if (!bCanBeTrue || !Other.bCanBeTrue)
|
|
{
|
|
return FVoxelBoolRange::False();
|
|
}
|
|
else
|
|
{
|
|
return FVoxelBoolRange::TrueOrFalse();
|
|
}
|
|
}
|
|
FVoxelBoolRange operator||(const FVoxelBoolRange& Other) const
|
|
{
|
|
if (!bCanBeFalse || !Other.bCanBeFalse)
|
|
{
|
|
return FVoxelBoolRange::True();
|
|
}
|
|
else if (!bCanBeTrue && !Other.bCanBeTrue)
|
|
{
|
|
return FVoxelBoolRange::False();
|
|
}
|
|
else
|
|
{
|
|
return FVoxelBoolRange::TrueOrFalse();
|
|
}
|
|
}
|
|
|
|
operator bool() const
|
|
{
|
|
if (bCanBeTrue && !bCanBeFalse)
|
|
{
|
|
return true;
|
|
}
|
|
else if (!bCanBeTrue && bCanBeFalse)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
checkVoxelSlow(bCanBeTrue && bCanBeFalse);
|
|
FVoxelRangeFailStatus::Get().Fail(TEXT("condition can be true or false"));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static FVoxelBoolRange True() { return { true, false }; }
|
|
static FVoxelBoolRange False() { return { false, true }; }
|
|
static FVoxelBoolRange TrueOrFalse() { return { true, true }; }
|
|
|
|
static bool If(const FVoxelBoolRange& Condition, bool bDefaultValue)
|
|
{
|
|
auto& RangeFailStatus = FVoxelRangeFailStatus::Get();
|
|
if (RangeFailStatus.HasFailed())
|
|
{
|
|
return true; // If already failed do nothing
|
|
}
|
|
const bool bCondition = Condition;
|
|
if (RangeFailStatus.HasFailed())
|
|
{
|
|
RangeFailStatus.Reset();
|
|
return bDefaultValue;
|
|
}
|
|
else
|
|
{
|
|
return bCondition;
|
|
}
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
template<typename T>
|
|
T NegativeInfinity();
|
|
template<typename T>
|
|
T PositiveInfinity();
|
|
|
|
template<>
|
|
inline constexpr float NegativeInfinity<float>()
|
|
{
|
|
return -std::numeric_limits<float>::infinity();
|
|
}
|
|
template<>
|
|
inline constexpr float PositiveInfinity<float>()
|
|
{
|
|
return std::numeric_limits<float>::infinity();
|
|
}
|
|
|
|
template<>
|
|
inline constexpr double NegativeInfinity<double>()
|
|
{
|
|
return -std::numeric_limits<double>::infinity();
|
|
}
|
|
template<>
|
|
inline constexpr double PositiveInfinity<double>()
|
|
{
|
|
return std::numeric_limits<double>::infinity();
|
|
}
|
|
|
|
template<>
|
|
inline constexpr int32 NegativeInfinity<int32>()
|
|
{
|
|
return MIN_int32;
|
|
}
|
|
template<>
|
|
inline constexpr int32 PositiveInfinity<int32>()
|
|
{
|
|
return MAX_int32;
|
|
}
|
|
|
|
template<>
|
|
inline constexpr uint16 NegativeInfinity<uint16>()
|
|
{
|
|
return MIN_uint16;
|
|
}
|
|
template<>
|
|
inline constexpr uint16 PositiveInfinity<uint16>()
|
|
{
|
|
return MAX_uint16;
|
|
}
|
|
|
|
template<>
|
|
inline constexpr FVoxelValue NegativeInfinity<FVoxelValue>()
|
|
{
|
|
return FVoxelValue::Full();
|
|
}
|
|
template<>
|
|
inline constexpr FVoxelValue PositiveInfinity<FVoxelValue>()
|
|
{
|
|
return FVoxelValue::Empty();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
template<typename T>
|
|
FString PrettyPrint(const T& Value)
|
|
{
|
|
return LexToString(Value);
|
|
}
|
|
|
|
template<>
|
|
inline FString PrettyPrint<float>(const float& Value)
|
|
{
|
|
return FString::SanitizeFloat(Value);
|
|
}
|
|
|
|
template<>
|
|
inline FString PrettyPrint<double>(const double& Value)
|
|
{
|
|
return FString::SanitizeFloat(Value);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
template<typename T>
|
|
struct TVoxelRange
|
|
{
|
|
T Min;
|
|
T Max;
|
|
|
|
TVoxelRange() = default;
|
|
TVoxelRange(T Value)
|
|
: Min(Value)
|
|
, Max(Value)
|
|
{
|
|
}
|
|
TVoxelRange(T Min, T Max)
|
|
: Min(Min)
|
|
, Max(Max)
|
|
{
|
|
ensure(Min <= Max);
|
|
}
|
|
template<typename TOther>
|
|
explicit TVoxelRange(const TVoxelRange<TOther>& Range)
|
|
: Min(Range.Min)
|
|
, Max(Range.Max)
|
|
{
|
|
}
|
|
|
|
public:
|
|
static constexpr bool bIsInteger = TIsIntegral<T>::Value;
|
|
|
|
public:
|
|
template<typename ...TArgs>
|
|
static TVoxelRange<T> FromList(TArgs... Values)
|
|
{
|
|
return { FVoxelUtilities::VariadicMin(Values...), FVoxelUtilities::VariadicMax(Values...) };
|
|
}
|
|
static TVoxelRange<T> Union(const TVoxelRange<T>& A, const TVoxelRange<T>& B)
|
|
{
|
|
return { FMath::Min(A.Min, B.Min), FMath::Max(A.Max, B.Max) };
|
|
}
|
|
static TVoxelRange<T> Intersection(const TVoxelRange<T>& A, const TVoxelRange<T>& B)
|
|
{
|
|
const T NewMin = FMath::Max(A.Min, B.Min);
|
|
const T NewMax = FMath::Min(A.Max, B.Max);
|
|
if (ensure(NewMin <= NewMax))
|
|
{
|
|
return { NewMin, NewMax };
|
|
}
|
|
else
|
|
{
|
|
return Union(A, B);
|
|
}
|
|
}
|
|
template<typename ...TArgs>
|
|
static TVoxelRange<T> Union(const TVoxelRange<T>& A, const TVoxelRange<T>& B, TArgs... Args)
|
|
{
|
|
return Union(Union(A, B), Args...);
|
|
}
|
|
template<typename ...TArgs>
|
|
static TVoxelRange<T> Intersection(const TVoxelRange<T>& A, const TVoxelRange<T>& B, TArgs... Args)
|
|
{
|
|
return Intersection(Intersection(A, B), Args...);
|
|
}
|
|
static TVoxelRange<T> Infinite()
|
|
{
|
|
return { NegativeInfinity<T>(), PositiveInfinity<T>() };
|
|
}
|
|
static TVoxelRange<T> PositiveInfinite()
|
|
{
|
|
return { 0, PositiveInfinity<T>() };
|
|
}
|
|
static TVoxelRange<T> NegativeInfinite()
|
|
{
|
|
return { NegativeInfinity<T>(), 0 };
|
|
}
|
|
|
|
FString ToString() const
|
|
{
|
|
return IsSingleValue() ? PrettyPrint(Min) : FString::Printf(TEXT("%s, %s"), *PrettyPrint(Min), *PrettyPrint(Max));
|
|
}
|
|
|
|
template<typename TOther>
|
|
bool Contains(const TOther& Other) const
|
|
{
|
|
return Min <= Other && Other <= Max;
|
|
}
|
|
template<typename TOther>
|
|
bool Contains(const TVoxelRange<TOther>& Other) const
|
|
{
|
|
return Min <= Other.Min && Other.Max <= Max;
|
|
}
|
|
template<typename TOther>
|
|
bool Intersects(const TVoxelRange<TOther>& Other) const
|
|
{
|
|
return Contains(Other.Min) || Contains(Other.Max) || Other.Contains(Min) || Other.Contains(Max);
|
|
}
|
|
|
|
bool IsSingleValue() const
|
|
{
|
|
return Min == Max;
|
|
}
|
|
T GetSingleValue() const
|
|
{
|
|
ensure(IsSingleValue());
|
|
return Min;
|
|
}
|
|
|
|
bool IsSingleSign() const
|
|
{
|
|
return Min == 0 || Max == 0 || (Min < 0) == (Max < 0);
|
|
}
|
|
T GetSign() const
|
|
{
|
|
ensure(IsSingleSign());
|
|
return Min == 0 ? FMath::Sign(Max) : FMath::Sign(Min);
|
|
}
|
|
T GetSign_NotZero() const
|
|
{
|
|
ensureVoxelSlow(IsSingleSign());
|
|
ensureVoxelSlow(Min != 0 && Max != 0);
|
|
ensureVoxelSlow((Min < 0) == (Max < 0));
|
|
return Min < 0 ? -1 : 1;
|
|
}
|
|
TVoxelRange<T> ExtendToInfinity() const
|
|
{
|
|
TVoxelRange<T> New = *this;
|
|
if (Min < 0)
|
|
{
|
|
New.Min = NegativeInfinity<T>();
|
|
New.Max = Max > 0 ? PositiveInfinity<T>() : 0;
|
|
}
|
|
else
|
|
{
|
|
New.Min = 0;
|
|
New.Max = PositiveInfinity<T>();
|
|
}
|
|
return New;
|
|
}
|
|
|
|
bool IsNegativeInfinity() const
|
|
{
|
|
return Min == NegativeInfinity<T>();
|
|
}
|
|
bool IsPositiveInfinity() const
|
|
{
|
|
return Max == PositiveInfinity<T>();
|
|
}
|
|
bool IsInfinity() const
|
|
{
|
|
return IsNegativeInfinity() || IsPositiveInfinity();
|
|
}
|
|
|
|
template<typename TOther>
|
|
TVoxelRange<T>& operator=(const TVoxelRange<TOther>& Other)
|
|
{
|
|
Min = Other.Min;
|
|
Max = Other.Max;
|
|
return *this;
|
|
}
|
|
|
|
template<typename F>
|
|
auto Apply(F Op) const
|
|
{
|
|
return TVoxelRange<decltype(Op(Min))>{ Op(Min), Op(Max) };
|
|
}
|
|
|
|
public:
|
|
template<typename TOther>
|
|
FVoxelBoolRange operator==(const TVoxelRange<TOther>& Other) const
|
|
{
|
|
if (IsSingleValue() && Other.IsSingleValue() && Min == Other.Min)
|
|
{
|
|
checkVoxelSlow(Max == Other.Max);
|
|
return FVoxelBoolRange::True();
|
|
}
|
|
else if (!Intersects(Other))
|
|
{
|
|
return FVoxelBoolRange::False();
|
|
}
|
|
else
|
|
{
|
|
return FVoxelBoolRange::TrueOrFalse();
|
|
}
|
|
}
|
|
template<typename TOther>
|
|
FVoxelBoolRange operator!=(const TVoxelRange<TOther>& Other) const
|
|
{
|
|
return !(*this == Other);
|
|
}
|
|
template<typename TOther>
|
|
FVoxelBoolRange operator<(const TVoxelRange<TOther>& Other) const
|
|
{
|
|
if (Max < Other.Min)
|
|
{
|
|
return FVoxelBoolRange::True();
|
|
}
|
|
else if (Other.Max <= Min)
|
|
{
|
|
return FVoxelBoolRange::False();
|
|
}
|
|
else
|
|
{
|
|
return FVoxelBoolRange::TrueOrFalse();
|
|
}
|
|
}
|
|
template<typename TOther>
|
|
FVoxelBoolRange operator>(const TVoxelRange<TOther>& Other) const
|
|
{
|
|
if (Min > Other.Max)
|
|
{
|
|
return FVoxelBoolRange::True();
|
|
}
|
|
else if (Other.Min >= Max)
|
|
{
|
|
return FVoxelBoolRange::False();
|
|
}
|
|
else
|
|
{
|
|
return FVoxelBoolRange::TrueOrFalse();
|
|
}
|
|
}
|
|
template<typename TOther>
|
|
FVoxelBoolRange operator<=(const TVoxelRange<TOther>& Other) const
|
|
{
|
|
return !(*this > Other);
|
|
}
|
|
template<typename TOther>
|
|
FVoxelBoolRange operator>=(const TVoxelRange<TOther>& Other) const
|
|
{
|
|
return !(*this < Other);
|
|
}
|
|
|
|
public:
|
|
template<typename TOther>
|
|
TVoxelRange<T> operator+(const TVoxelRange<TOther>& Other) const
|
|
{
|
|
return { Min + Other.Min, Max + Other.Max };
|
|
}
|
|
template<typename TOther>
|
|
TVoxelRange<T> operator-(const TVoxelRange<TOther>& Other) const
|
|
{
|
|
return { Min - Other.Max, Max - Other.Min };
|
|
}
|
|
TVoxelRange<T> operator-() const
|
|
{
|
|
return { -Max, -Min };
|
|
}
|
|
template<typename TOther>
|
|
TVoxelRange<T> operator*(const TVoxelRange<TOther>& Other) const
|
|
{
|
|
return TVoxelRange::FromList(Min * Other.Min, Min * Other.Max, Max * Other.Min, Max * Other.Max);
|
|
}
|
|
template<typename TOther>
|
|
TVoxelRange<T> operator/(const TVoxelRange<TOther>& Other) const
|
|
{
|
|
if (Other.IsSingleValue() && Other.GetSingleValue() == 0)
|
|
{
|
|
if (bIsInteger)
|
|
{
|
|
// That's how integer / 0 is handled in voxel graphs
|
|
return 0;
|
|
}
|
|
if (IsSingleValue() && GetSingleValue() == 0)
|
|
{
|
|
FVoxelRangeFailStatus::Get().Warning(TEXT("0 / 0 encountered, will result in a nan"));
|
|
return Infinite();
|
|
}
|
|
if (0 < Min)
|
|
{
|
|
// Single value: +inf
|
|
return PositiveInfinity<T>();
|
|
}
|
|
if (Max < 0)
|
|
{
|
|
// Single value: -inf
|
|
return NegativeInfinity<T>();
|
|
}
|
|
return Infinite();
|
|
}
|
|
|
|
if (!Other.Contains(0)) // Will also handle single value cases
|
|
{
|
|
if (Other.IsInfinity())
|
|
{
|
|
ensureVoxelSlow(Other.IsSingleSign()); // Does not contain 0
|
|
ensureVoxelSlow(Other.GetSign() != 0); // Else wouldn't be infinity, and does not contain 0
|
|
const auto Inf = ExtendToInfinity();
|
|
return TVoxelRange::FromList(Inf.Min / Other.GetSign_NotZero(), Inf.Max / Other.GetSign_NotZero());
|
|
}
|
|
return TVoxelRange::FromList(Min / Other.Min, Min / Other.Max, Max / Other.Min, Max / Other.Max);
|
|
}
|
|
else
|
|
{
|
|
if (Other.IsSingleSign())
|
|
{
|
|
ensureVoxelSlow(Other.GetSign() != 0); // Else would be a single value
|
|
const auto Inf = ExtendToInfinity();
|
|
return TVoxelRange::FromList(Inf.Min / Other.GetSign_NotZero(), Inf.Max / Other.GetSign_NotZero());
|
|
}
|
|
return Infinite();
|
|
}
|
|
}
|
|
|
|
public:
|
|
TVoxelRange<T> operator+(T Other) const
|
|
{
|
|
return { Min + Other, Max + Other };
|
|
}
|
|
TVoxelRange<T> operator-(T Other) const
|
|
{
|
|
return { Min - Other, Max - Other};
|
|
}
|
|
TVoxelRange<T> operator*(T Other) const
|
|
{
|
|
return { FMath::Min(Min * Other, Max * Other), FMath::Max(Min * Other, Max * Other) };
|
|
}
|
|
TVoxelRange<T> operator/(T Other) const
|
|
{
|
|
return { FMath::Min(Min / Other, Max / Other), FMath::Max(Min / Other, Max / Other) };
|
|
}
|
|
|
|
friend FVoxelBoolRange operator==(const TVoxelRange<T>& Range, T Other)
|
|
{
|
|
return Range == TVoxelRange<T>(Other);
|
|
}
|
|
friend FVoxelBoolRange operator<(const TVoxelRange<T>& Range, T Other)
|
|
{
|
|
return Range < TVoxelRange<T>(Other);
|
|
}
|
|
friend FVoxelBoolRange operator>(const TVoxelRange<T>& Range, T Other)
|
|
{
|
|
return Range > TVoxelRange<T>(Other);
|
|
}
|
|
friend FVoxelBoolRange operator<=(const TVoxelRange<T>& Range, T Other)
|
|
{
|
|
return Range <= TVoxelRange<T>(Other);
|
|
}
|
|
friend FVoxelBoolRange operator>=(const TVoxelRange<T>& Range, T Other)
|
|
{
|
|
return Range >= TVoxelRange<T>(Other);
|
|
}
|
|
friend TVoxelRange<T> operator-(T Other, const TVoxelRange<T>& Range)
|
|
{
|
|
return TVoxelRange<T>(Other) - Range;
|
|
}
|
|
friend TVoxelRange<T> operator+(T Other, const TVoxelRange<T>& Range)
|
|
{
|
|
return TVoxelRange<T>(Other) + Range;
|
|
}
|
|
friend TVoxelRange<T> operator*(T Other, const TVoxelRange<T>& Range)
|
|
{
|
|
return TVoxelRange<T>(Other) * Range;
|
|
}
|
|
friend TVoxelRange<T> operator/(T Other, const TVoxelRange<T>& Range)
|
|
{
|
|
return TVoxelRange<T>(Other) / Range;
|
|
}
|
|
|
|
template<typename U>
|
|
TVoxelRange<T>& operator-=(const U& Other)
|
|
{
|
|
*this = *this - Other;
|
|
return *this;
|
|
}
|
|
template<typename U>
|
|
TVoxelRange<T>& operator+=(const U& Other)
|
|
{
|
|
*this = *this + Other;
|
|
return *this;
|
|
}
|
|
template<typename U>
|
|
TVoxelRange<T>& operator*=(const U& Other)
|
|
{
|
|
*this = *this * Other;
|
|
return *this;
|
|
}
|
|
template<typename U>
|
|
TVoxelRange<T>& operator/=(const U& Other)
|
|
{
|
|
*this = *this / Other;
|
|
return *this;
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
template<typename T>
|
|
FArchive& operator<<(FArchive& Ar, TVoxelRange<T>& Range)
|
|
{
|
|
Ar << Range.Min;
|
|
Ar << Range.Max;
|
|
return Ar;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct FVoxelMaterialRange
|
|
{
|
|
FVoxelMaterialRange() = default;
|
|
FVoxelMaterialRange(const struct FVoxelMaterial&) {}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct FVoxelColorRange
|
|
{
|
|
FVoxelColorRange() = default;
|
|
FVoxelColorRange(const struct FColor&) {}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// For display and serialization
|
|
USTRUCT()
|
|
struct FVoxelRange
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
UPROPERTY(EditAnywhere, Category = "Voxel")
|
|
double Min = 0;
|
|
|
|
UPROPERTY(EditAnywhere, Category = "Voxel")
|
|
double Max = 0;
|
|
|
|
FVoxelRange() = default;
|
|
FVoxelRange(float Min, float Max)
|
|
: Min(Min)
|
|
, Max(Max)
|
|
{
|
|
}
|
|
FVoxelRange(const TVoxelRange<v_flt>& Range)
|
|
: Min(Range.Min)
|
|
, Max(Range.Max)
|
|
{
|
|
}
|
|
|
|
operator TVoxelRange<v_flt>() const
|
|
{
|
|
return { v_flt(Min), v_flt(Max) };
|
|
}
|
|
}; |