Display for schedule data asset timeline, view only
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#include "NpcScheduleAssetCustomization.h"
|
||||||
|
|
||||||
|
#include "DetailCategoryBuilder.h"
|
||||||
|
#include "DetailLayoutBuilder.h"
|
||||||
|
#include "DetailWidgetRow.h"
|
||||||
|
|
||||||
|
#include "NpcScheduleTimeline.h"
|
||||||
|
#include "ProjectEleri/Ai/NpcScheduleAsset.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "NpcScheduleAssetCustomization"
|
||||||
|
|
||||||
|
TSharedRef<IDetailCustomization> FNpcScheduleAssetCustomization::MakeInstance() {
|
||||||
|
return MakeShareable(new FNpcScheduleAssetCustomization());
|
||||||
|
}
|
||||||
|
|
||||||
|
void FNpcScheduleAssetCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) {
|
||||||
|
// The asset being customized. The weak pointers the panel hands over are the whole reason the strip
|
||||||
|
// holds one of its own rather than a raw pointer: an asset can be deleted while a tab showing it is
|
||||||
|
// still open, and the strip has to draw an empty day rather than a dangling one.
|
||||||
|
TArray<TWeakObjectPtr<UObject>> Objects;
|
||||||
|
DetailBuilder.GetObjectsBeingCustomized(Objects);
|
||||||
|
|
||||||
|
UNpcScheduleAsset* Schedule = Objects.Num() > 0 ? Cast<UNpcScheduleAsset>(Objects[0].Get()) : nullptr;
|
||||||
|
|
||||||
|
// A category of this module's own, sorted after the one an asset's properties land in, so that the
|
||||||
|
// strip is drawn under every row the asset already had rather than beside them. It is created even
|
||||||
|
// when nothing is being customized, so that a panel left open on a deleted asset still has a strip
|
||||||
|
// in it saying so.
|
||||||
|
IDetailCategoryBuilder& TimelineCategory = DetailBuilder.EditCategory(
|
||||||
|
TEXT("ScheduleTimeline"),
|
||||||
|
LOCTEXT("TimelineCategoryLabel", "Day Timeline"),
|
||||||
|
ECategoryPriority::Uncommon);
|
||||||
|
|
||||||
|
// The whole row, name column and value column both, because the strip is a picture of the day and
|
||||||
|
// not the value of anything: it is laid out against the width of the panel it is handed, and this
|
||||||
|
// is the slot that hands it all of it.
|
||||||
|
TimelineCategory.AddCustomRow(LOCTEXT("TimelineRowLabel", "Day Timeline"))
|
||||||
|
.WholeRowContent()
|
||||||
|
[
|
||||||
|
SNew(SNpcScheduleTimeline)
|
||||||
|
.Schedule(Schedule)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
585
Source/ProjectEleriEditor/Private/NpcScheduleTimeline.cpp
Normal file
585
Source/ProjectEleriEditor/Private/NpcScheduleTimeline.cpp
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#include "NpcScheduleTimeline.h"
|
||||||
|
|
||||||
|
#include "Editor.h"
|
||||||
|
#include "EngineUtils.h"
|
||||||
|
#include "Rendering/DrawElements.h"
|
||||||
|
#include "Styling/CoreStyle.h"
|
||||||
|
#include "Templates/TypeHash.h"
|
||||||
|
#include "Widgets/SNullWidget.h"
|
||||||
|
|
||||||
|
#include "ProjectEleri/Ai/NpcScheduleAsset.h"
|
||||||
|
#include "ProjectEleri/Public/Interface/ActorPersistentIdInterface.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "NpcScheduleTimeline"
|
||||||
|
|
||||||
|
/*-----------------------------------------------------------------------------
|
||||||
|
How the strip is laid out
|
||||||
|
-----------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/** One day, in the minutes of the day that every start in a schedule is measured in. */
|
||||||
|
constexpr int32 MinutesPerDay = 24 * 60;
|
||||||
|
|
||||||
|
/** Where the strip starts and ends inside the widget, so that a slot sitting at midnight is not
|
||||||
|
* drawn against the panel's own edge and its label has somewhere to start. */
|
||||||
|
constexpr float StripLeftPadding = 8.0f;
|
||||||
|
constexpr float StripRightPadding = 8.0f;
|
||||||
|
|
||||||
|
/** The ruler row, which holds the hour labels and nothing else. */
|
||||||
|
constexpr float HourLabelHeight = 12.0f;
|
||||||
|
|
||||||
|
/** One row of slots. High enough to read a label in, and no higher, since a day is mostly one row. */
|
||||||
|
constexpr float LaneHeight = 26.0f;
|
||||||
|
constexpr float LaneGap = 3.0f;
|
||||||
|
|
||||||
|
/** The legend below the day: one line per action, each with the swatch that matches its slot. */
|
||||||
|
constexpr float LegendTopGap = 8.0f;
|
||||||
|
constexpr float LegendRowHeight = 14.0f;
|
||||||
|
constexpr float LegendSwatchWidth = 8.0f;
|
||||||
|
constexpr float LegendSwatchGap = 6.0f;
|
||||||
|
|
||||||
|
/** Below that, and under the day when there is no legend to draw. */
|
||||||
|
constexpr float BottomPadding = 2.0f;
|
||||||
|
|
||||||
|
/** What the strip asks for in a panel that has not decided how wide it should be yet. The width it
|
||||||
|
* actually gets is whatever the panel gives it, so this is only ever a starting point. */
|
||||||
|
constexpr float PreferredWidth = 520.0f;
|
||||||
|
|
||||||
|
/** A slot narrower than this is drawn as a bare block: no label fits in it, and a label spilling out
|
||||||
|
* of its own slot would read as belonging to the slot beside it. */
|
||||||
|
constexpr float MinimumLabelledSlotWidth = 68.0f;
|
||||||
|
|
||||||
|
/** A slot's own label: how far it sits from the slot's edges, and how tall the line it is drawn on is,
|
||||||
|
* which is what centres it in a lane. */
|
||||||
|
constexpr float SlotLabelInset = 6.0f;
|
||||||
|
constexpr float SlotLabelHeight = 12.0f;
|
||||||
|
|
||||||
|
/** Every this many hours the ruler carries a label. Written out rather than derived so that the
|
||||||
|
* labels line up with the times an author thinks in. */
|
||||||
|
constexpr int32 LabelledHourInterval = 3;
|
||||||
|
|
||||||
|
/** How long an anchor's name is trusted for before it is read out of the level again. The names are
|
||||||
|
* the one thing being drawn that can go stale without the schedule changing - an anchor renamed or
|
||||||
|
* deleted while this tab sat open - and re-reading them is a walk of the level, so it is done on a
|
||||||
|
* timer rather than every frame. */
|
||||||
|
constexpr double AnchorLabelRefreshPeriod = 2.0;
|
||||||
|
|
||||||
|
/** Colour of the trough the lanes sit in, of the hour marks, and of the text that is not a label of
|
||||||
|
* anything: the same three the rest of the editor's dark panels read against. */
|
||||||
|
const FLinearColor TroughColor(0.0f, 0.0f, 0.0f, 0.55f);
|
||||||
|
const FLinearColor LaneColor(1.0f, 1.0f, 1.0f, 0.03f);
|
||||||
|
const FLinearColor HourMarkColor(1.0f, 1.0f, 1.0f, 0.07f);
|
||||||
|
const FLinearColor LabelledHourMarkColor(1.0f, 1.0f, 1.0f, 0.16f);
|
||||||
|
const FLinearColor HourLabelColor(0.62f, 0.62f, 0.62f);
|
||||||
|
const FLinearColor LegendTextColor(0.80f, 0.80f, 0.80f);
|
||||||
|
const FLinearColor QuietTextColor(0.55f, 0.55f, 0.55f);
|
||||||
|
|
||||||
|
/** A slot's label is drawn dark on the slot's own colour, which is the only way one label style stays
|
||||||
|
* legible on six different backgrounds without a second colour per activity to go with them. */
|
||||||
|
const FLinearColor SlotLabelColor(0.06f, 0.06f, 0.06f, 0.90f);
|
||||||
|
|
||||||
|
/** The marker where a slot that runs past midnight carries on at the other end of the strip. Drawn on
|
||||||
|
* both halves, so that either end says which window it belongs to. */
|
||||||
|
const FLinearColor WrapMarkerColor(1.0f, 1.0f, 1.0f, 0.85f);
|
||||||
|
constexpr float WrapMarkerWidth = 3.0f;
|
||||||
|
|
||||||
|
/*-----------------------------------------------------------------------------
|
||||||
|
Reading the schedule and the level
|
||||||
|
-----------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/** The id a schedule would name this actor by. Implementing the interface is the whole of the "can
|
||||||
|
* this actor be an anchor" rule, because the interface is what carries the id a schedule stores.
|
||||||
|
*
|
||||||
|
* Written out rather than shared with the two other copies of it, exactly as ScheduleAssetRecheck
|
||||||
|
* says of its own: it is one line of casting, and the smaller fault next to a details panel that
|
||||||
|
* answers about one action at a time having to grow a second responsibility for a view of a day. */
|
||||||
|
FGuid GetAnchorId(AActor* Actor)
|
||||||
|
{
|
||||||
|
if (Actor == nullptr)
|
||||||
|
{
|
||||||
|
return FGuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
const IActorPersistentIdInterface* Identified = Cast<IActorPersistentIdInterface>(Actor);
|
||||||
|
return Identified != nullptr ? Identified->GetPersistentId() : FGuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every anchor the open level can answer for, by the id a schedule names it with.
|
||||||
|
*
|
||||||
|
* One walk for the whole strip rather than a walk per action, which is the rule the recheck sweep
|
||||||
|
* follows and for the same reason: the answer cannot change under the walk. The first actor to claim
|
||||||
|
* an id keeps it, which is also what a running game arrives at, both sides walking the level and
|
||||||
|
* taking what they meet first. */
|
||||||
|
void CollectAnchorNames(TMap<FGuid, FString>& OutAnchorNames)
|
||||||
|
{
|
||||||
|
UWorld* World = GEditor != nullptr ? GEditor->GetEditorWorldContext().World() : nullptr;
|
||||||
|
if (World == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (TActorIterator<AActor> It(World); It; ++It)
|
||||||
|
{
|
||||||
|
AActor* Actor = *It;
|
||||||
|
const FGuid AnchorId = GetAnchorId(Actor);
|
||||||
|
if (!AnchorId.IsValid() || OutAnchorNames.Contains(AnchorId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutAnchorNames.Add(AnchorId, Actor->GetActorNameOrLabel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What an activity is called, which is the name the enum itself carries rather than a table here: a
|
||||||
|
* behaviour added as an enum value is then named in the strip without anyone remembering this file
|
||||||
|
* exists. */
|
||||||
|
FText GetActivityLabel(ENpcActivity Activity)
|
||||||
|
{
|
||||||
|
const UEnum* Enum = StaticEnum<ENpcActivity>();
|
||||||
|
return Enum != nullptr ? Enum->GetDisplayNameTextByValue(static_cast<int64>(Activity)) : FText::GetEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The colour that activity is drawn in. Handed out per activity rather than hashed, because these
|
||||||
|
* are the ones that exist and they are worth telling apart at a glance: sleep dark, work warm, and
|
||||||
|
* the two quiet ones grey. */
|
||||||
|
FLinearColor GetActivityColor(ENpcActivity Activity)
|
||||||
|
{
|
||||||
|
switch (Activity)
|
||||||
|
{
|
||||||
|
case ENpcActivity::Idle: return FLinearColor(0.42f, 0.44f, 0.48f);
|
||||||
|
case ENpcActivity::Sleep: return FLinearColor(0.29f, 0.36f, 0.72f);
|
||||||
|
case ENpcActivity::Sit: return FLinearColor(0.22f, 0.55f, 0.60f);
|
||||||
|
case ENpcActivity::Stand: return FLinearColor(0.34f, 0.62f, 0.35f);
|
||||||
|
case ENpcActivity::Work: return FLinearColor(0.78f, 0.55f, 0.20f);
|
||||||
|
case ENpcActivity::Roam: return FLinearColor(0.66f, 0.35f, 0.55f);
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A behaviour added later gets a colour of its own instead of the grey of an unknown: the value is
|
||||||
|
// spread over the colour wheel by the golden angle, which lands the next few far from each other
|
||||||
|
// and from the six above without anyone having to come back here to choose it.
|
||||||
|
return FLinearColor::MakeFromHSV8(static_cast<uint8>(static_cast<uint8>(Activity) * 137), 140, 190);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "22:00" for a minute of the day. Culture invariant, because a clock is digits and not prose. */
|
||||||
|
FText MakeClockText(int32 MinutesOfDay)
|
||||||
|
{
|
||||||
|
const int32 Wrapped = ((MinutesOfDay % MinutesPerDay) + MinutesPerDay) % MinutesPerDay;
|
||||||
|
return FText::AsCultureInvariant(FString::Printf(TEXT("%02d:%02d"), Wrapped / 60, Wrapped % 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "22:00 - 06:00" for a window, which is the whole of what a slot has to say about when it is. */
|
||||||
|
FText MakeWindowText(int32 StartMinutes, int32 EndMinutes)
|
||||||
|
{
|
||||||
|
return FText::AsCultureInvariant(FString::Printf(
|
||||||
|
TEXT("%s - %s"), *MakeClockText(StartMinutes).ToString(), *MakeClockText(EndMinutes).ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What an action's anchor is called on the legend line: the name of the actor the id resolves to in
|
||||||
|
* this level, or what it means when it resolves to nothing. An id no actor claims is a real state -
|
||||||
|
* furniture that was deleted, or furniture in a cell that is not loaded - and not one to hide, so it
|
||||||
|
* is named with the id it is, which is also the only way to tell two unresolved anchors apart. */
|
||||||
|
FText MakeAnchorText(const FGuid& AnchorId, const TMap<FGuid, FString>& AnchorNames)
|
||||||
|
{
|
||||||
|
if (!AnchorId.IsValid())
|
||||||
|
{
|
||||||
|
return LOCTEXT("NoAnchor", "no anchor");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const FString* Name = AnchorNames.Find(AnchorId))
|
||||||
|
{
|
||||||
|
return FText::FromString(*Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FText::Format(
|
||||||
|
LOCTEXT("AnchorNotInLevel", "an anchor no actor in this level claims ({0})"),
|
||||||
|
FText::AsCultureInvariant(AnchorId.ToString(EGuidFormats::Digits)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "Sleep 22:00 - 06:00 Bed_01" for the legend, which is where an action is named in full. */
|
||||||
|
FText MakeLegendLine(const FText& ActivityText, const FText& WindowText, const FText& AnchorText)
|
||||||
|
{
|
||||||
|
return FText::Format(LOCTEXT("LegendLine", "{0} {1} {2}"), ActivityText, WindowText, AnchorText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*-----------------------------------------------------------------------------
|
||||||
|
The widget
|
||||||
|
-----------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
void SNpcScheduleTimeline::Construct(const FArguments& InArgs)
|
||||||
|
{
|
||||||
|
Schedule = InArgs._Schedule;
|
||||||
|
Font = FCoreStyle::GetDefaultFontStyle(TEXT("Regular"), 8);
|
||||||
|
|
||||||
|
// The strip paints itself and holds no widget of its own, and it says so with an empty child slot
|
||||||
|
// rather than by leaving the slot unset: the slot is what a compound widget arranges, and this is
|
||||||
|
// the one child it has to be given to arrange.
|
||||||
|
ChildSlot
|
||||||
|
[
|
||||||
|
SNullWidget::NullWidget
|
||||||
|
];
|
||||||
|
|
||||||
|
// The ruler is the same eight labels whatever is being drawn, so it is made once here rather than
|
||||||
|
// rebuilt along with the schedules that share it.
|
||||||
|
for (int32 Hour = 0; Hour < 24; Hour += LabelledHourInterval)
|
||||||
|
{
|
||||||
|
HourLabels.Add(FText::AsCultureInvariant(FString::Printf(TEXT("%02d:00"), Hour)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built here rather than left to the first tick, so that the first paint has a day to draw rather
|
||||||
|
// than an empty one. The time it is built at is 0 because there is no clock here yet, which is
|
||||||
|
// earlier than any anchor's name is trusted for: the first tick reads the level once more and
|
||||||
|
// leaves Construct with nothing to decide about when a name goes stale.
|
||||||
|
Rebuild(ComputeSignature(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SNpcScheduleTimeline::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
|
||||||
|
{
|
||||||
|
const uint32 Signature = ComputeSignature();
|
||||||
|
const bool bScheduleChanged = Signature != CachedSignature;
|
||||||
|
const bool bAnchorNamesStale = InCurrentTime - LastBuildTime >= AnchorLabelRefreshPeriod;
|
||||||
|
|
||||||
|
if (!bScheduleChanged && !bAnchorNamesStale)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rebuild(Signature, InCurrentTime);
|
||||||
|
|
||||||
|
// What has been painted was built from the schedule as it was, so the strip has to be painted
|
||||||
|
// again. An anchor renamed out from under an open tab is the only reason this happens without the
|
||||||
|
// asset changing, which is a repaint of one row every couple of seconds at worst: cheaper than
|
||||||
|
// keeping a subscription to every way an actor can be renamed, deleted or replaced by another.
|
||||||
|
Invalidate(EInvalidateWidgetReason::Paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32 SNpcScheduleTimeline::ComputeSignature() const
|
||||||
|
{
|
||||||
|
const UNpcScheduleAsset* Asset = Schedule.Get();
|
||||||
|
if (Asset == nullptr)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every value the strip draws is in here, so that anything which would change the day, the
|
||||||
|
// windows or the names on them comes out as a different signature. The anchor's recorded transform
|
||||||
|
// is deliberately not: the strip never draws where an anchor stands, so a recheck that refreshed
|
||||||
|
// one has nothing here to say.
|
||||||
|
uint32 Signature = PointerHash(Asset);
|
||||||
|
for (const FScheduledAction& Action : Asset->ScheduledActions)
|
||||||
|
{
|
||||||
|
Signature = HashCombine(Signature,
|
||||||
|
GetTypeHash(Action.StartTime.GetMinutesOfDay()),
|
||||||
|
GetTypeHash(static_cast<int32>(Action.Activity)),
|
||||||
|
GetTypeHash(Action.UsableObjectGuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SNpcScheduleTimeline::Rebuild(uint32 InSignature, double InCurrentTime)
|
||||||
|
{
|
||||||
|
CachedSignature = InSignature;
|
||||||
|
LastBuildTime = InCurrentTime;
|
||||||
|
|
||||||
|
Slots.Reset();
|
||||||
|
LaneCount = 0;
|
||||||
|
|
||||||
|
const UNpcScheduleAsset* Asset = Schedule.Get();
|
||||||
|
if (Asset == nullptr || Asset->ScheduledActions.Num() == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The day is read out of the starts and out of nothing else, because a start is all an action
|
||||||
|
// holds: sorted, so that the next entry is the next start in the day, and kept in the schedule's
|
||||||
|
// own order where two starts are equal, so that the lanes a tied pair lands on do not swap places
|
||||||
|
// between one rebuild and the next.
|
||||||
|
struct FStart
|
||||||
|
{
|
||||||
|
int32 Minutes = 0;
|
||||||
|
int32 ActionIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
TArray<FStart> Starts;
|
||||||
|
Starts.Reserve(Asset->ScheduledActions.Num());
|
||||||
|
for (int32 ActionIndex = 0; ActionIndex < Asset->ScheduledActions.Num(); ++ActionIndex)
|
||||||
|
{
|
||||||
|
Starts.Add(FStart{ Asset->ScheduledActions[ActionIndex].StartTime.GetMinutesOfDay(), ActionIndex });
|
||||||
|
}
|
||||||
|
|
||||||
|
Starts.Sort([](const FStart& A, const FStart& B)
|
||||||
|
{
|
||||||
|
return A.Minutes != B.Minutes ? A.Minutes < B.Minutes : A.ActionIndex < B.ActionIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
// The anchors are named with one walk of the open level rather than one per action, and the walk is
|
||||||
|
// skipped entirely - which is most of the time - when no action in this schedule names one.
|
||||||
|
TMap<FGuid, FString> AnchorNames;
|
||||||
|
const bool bAnchorNamed = Asset->ScheduledActions.ContainsByPredicate([](const FScheduledAction& Action)
|
||||||
|
{
|
||||||
|
return Action.UsableObjectGuid.IsValid();
|
||||||
|
});
|
||||||
|
if (bAnchorNamed)
|
||||||
|
{
|
||||||
|
CollectAnchorNames(AnchorNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32 SortedIndex = 0; SortedIndex < Starts.Num(); ++SortedIndex)
|
||||||
|
{
|
||||||
|
const FStart Start = Starts[SortedIndex];
|
||||||
|
const FScheduledAction& Action = Asset->ScheduledActions[Start.ActionIndex];
|
||||||
|
|
||||||
|
// The next start that is a different one, looked for rather than read off the next entry,
|
||||||
|
// because two actions authored at the same minute claim one window between them and are told
|
||||||
|
// apart by their lane instead of by a window of their own.
|
||||||
|
int32 NextMinutes = INDEX_NONE;
|
||||||
|
for (int32 LaterIndex = SortedIndex + 1; LaterIndex < Starts.Num(); ++LaterIndex)
|
||||||
|
{
|
||||||
|
if (Starts[LaterIndex].Minutes != Start.Minutes)
|
||||||
|
{
|
||||||
|
NextMinutes = Starts[LaterIndex].Minutes;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No later start at all means the day wraps round and the earliest start closes this window,
|
||||||
|
// which is the 06:00 that closes a 22:00-06:00 night. When that earliest start is this one's
|
||||||
|
// own minute then every action in the schedule was authored at one minute, and there is no
|
||||||
|
// window to be had between them: the whole day is theirs, which is how a schedule whose start
|
||||||
|
// and end are the same is read, see FScheduledAction.
|
||||||
|
bool bWholeDay = false;
|
||||||
|
if (NextMinutes == INDEX_NONE)
|
||||||
|
{
|
||||||
|
NextMinutes = Starts[0].Minutes;
|
||||||
|
bWholeDay = NextMinutes == Start.Minutes;
|
||||||
|
if (!bWholeDay)
|
||||||
|
{
|
||||||
|
NextMinutes += MinutesPerDay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The lane is however many actions before this one in the day claim the same window. Windows
|
||||||
|
// worked out this way never overlap unless they are the same window, so one lane per earlier
|
||||||
|
// claim is the whole of the separation there is to make.
|
||||||
|
int32 LaneIndex = 0;
|
||||||
|
for (int32 EarlierIndex = 0; EarlierIndex < SortedIndex; ++EarlierIndex)
|
||||||
|
{
|
||||||
|
if (Starts[EarlierIndex].Minutes == Start.Minutes)
|
||||||
|
{
|
||||||
|
++LaneIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaneCount = FMath::Max(LaneCount, LaneIndex + 1);
|
||||||
|
|
||||||
|
const FText ActivityText = GetActivityLabel(Action.Activity);
|
||||||
|
const FText WindowText = bWholeDay
|
||||||
|
? LOCTEXT("WholeDayWindow", "all day")
|
||||||
|
: MakeWindowText(Start.Minutes, NextMinutes);
|
||||||
|
const FText AnchorText = MakeAnchorText(Action.UsableObjectGuid, AnchorNames);
|
||||||
|
|
||||||
|
FSlotView& Slot = Slots.AddDefaulted_GetRef();
|
||||||
|
|
||||||
|
// A whole day is drawn from midnight rather than from the minute it was authored at: the strip
|
||||||
|
// is the day, and an action that covers it covers all of it.
|
||||||
|
Slot.StartMinutes = bWholeDay ? 0 : Start.Minutes;
|
||||||
|
Slot.EndMinutes = bWholeDay ? MinutesPerDay : NextMinutes;
|
||||||
|
Slot.LaneIndex = LaneIndex;
|
||||||
|
Slot.Color = GetActivityColor(Action.Activity);
|
||||||
|
Slot.BlockLabel = FText::Format(LOCTEXT("SlotLabel", "{0} {1}"), ActivityText, WindowText);
|
||||||
|
Slot.LegendLabel = MakeLegendLine(ActivityText, WindowText, AnchorText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector2D SNpcScheduleTimeline::ComputeDesiredSize(float LayoutScaleMultiplier) const
|
||||||
|
{
|
||||||
|
const int32 Lanes = FMath::Max(1, LaneCount);
|
||||||
|
const float DayHeight = Lanes * LaneHeight + (Lanes - 1) * LaneGap;
|
||||||
|
const float LegendHeight = Slots.Num() > 0 ? LegendTopGap + Slots.Num() * LegendRowHeight : 0.0f;
|
||||||
|
|
||||||
|
return FVector2D(PreferredWidth, HourLabelHeight + DayHeight + LegendHeight + BottomPadding);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 SNpcScheduleTimeline::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
|
||||||
|
{
|
||||||
|
// The width is read here and nowhere else. Everything below is placed against whatever the panel
|
||||||
|
// gave the row this frame, which is the whole of how the strip follows the panel being resized
|
||||||
|
// without anybody having to lay it out again.
|
||||||
|
const FVector2f LocalSize = AllottedGeometry.GetLocalSize();
|
||||||
|
const float StripLeft = StripLeftPadding;
|
||||||
|
const float StripWidth = FMath::Max(1.0f, LocalSize.X - StripLeftPadding - StripRightPadding);
|
||||||
|
|
||||||
|
const int32 Lanes = FMath::Max(1, LaneCount);
|
||||||
|
const float DayTop = HourLabelHeight;
|
||||||
|
const float DayBottom = DayTop + Lanes * LaneHeight + (Lanes - 1) * LaneGap;
|
||||||
|
const float LegendTop = DayBottom + LegendTopGap;
|
||||||
|
|
||||||
|
const FSlateBrush* const WhiteBrush = FCoreStyle::Get().GetBrush(TEXT("WhiteBrush"));
|
||||||
|
const ESlateDrawEffect DrawEffects = bParentEnabled
|
||||||
|
? ESlateDrawEffect::None
|
||||||
|
: ESlateDrawEffect::DisabledEffect;
|
||||||
|
|
||||||
|
/** A minute of the day as a place along the strip. */
|
||||||
|
const auto XOfMinute = [StripLeft, StripWidth](int32 MinutesOfDay)
|
||||||
|
{
|
||||||
|
return StripLeft + (StripWidth * static_cast<float>(MinutesOfDay)) / static_cast<float>(MinutesPerDay);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The top of a lane: the day's own rows, stacked under the ruler. */
|
||||||
|
const auto LaneTop = [DayTop](int32 LaneIndex)
|
||||||
|
{
|
||||||
|
return DayTop + LaneIndex * (LaneHeight + LaneGap);
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto DrawRect = [&](float X, float Y, float RectWidth, float RectHeight, const FLinearColor& Color, int32 Layer)
|
||||||
|
{
|
||||||
|
if (RectWidth <= 0.0f || RectHeight <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FSlateDrawElement::MakeBox(OutDrawElements, Layer,
|
||||||
|
AllottedGeometry.ToPaintGeometry(FVector2f(RectWidth, RectHeight), FSlateLayoutTransform(FVector2f(X, Y))),
|
||||||
|
WhiteBrush, DrawEffects, Color);
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto DrawText = [&](float X, float Y, float TextWidth, float TextHeight, const FText& Text, const FLinearColor& Color, int32 Layer)
|
||||||
|
{
|
||||||
|
if (TextWidth <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FSlateDrawElement::MakeText(OutDrawElements, Layer,
|
||||||
|
AllottedGeometry.ToPaintGeometry(FVector2f(TextWidth, TextHeight), FSlateLayoutTransform(FVector2f(X, Y))),
|
||||||
|
Text, Font, DrawEffects, Color);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* The trough, the lanes in it, and the hour marks over both, so that an hour reads as a line
|
||||||
|
across a whole day rather than as a mark inside one lane. The marks are the ruler's and the slots
|
||||||
|
are drawn over them: an hour inside a slot is drawn dark on that slot's own colour, see below. */
|
||||||
|
DrawRect(StripLeft, DayTop, StripWidth, DayBottom - DayTop, TroughColor, LayerId);
|
||||||
|
|
||||||
|
for (int32 LaneIndex = 0; LaneIndex < Lanes; ++LaneIndex)
|
||||||
|
{
|
||||||
|
DrawRect(StripLeft, LaneTop(LaneIndex), StripWidth, LaneHeight, LaneColor, LayerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32 Hour = 0; Hour <= 24; ++Hour)
|
||||||
|
{
|
||||||
|
const float HourX = XOfMinute(Hour * 60);
|
||||||
|
const bool bLabelled = Hour % LabelledHourInterval == 0;
|
||||||
|
|
||||||
|
DrawRect(HourX, DayTop, 1.0f, DayBottom - DayTop,
|
||||||
|
bLabelled ? LabelledHourMarkColor : HourMarkColor, LayerId + 1);
|
||||||
|
|
||||||
|
if (bLabelled && Hour < 24)
|
||||||
|
{
|
||||||
|
// Drawn to the right of its own mark rather than centred on it, so that none of the labels
|
||||||
|
// is ever cut in half by the edge of the strip.
|
||||||
|
DrawText(HourX + 3.0f, 0.0f, StripWidth - (HourX + 3.0f - StripLeft), HourLabelHeight,
|
||||||
|
HourLabels[Hour / LabelledHourInterval], HourLabelColor, LayerId + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One half of a slot. The day ends at midnight, so a window that runs past it is drawn as two
|
||||||
|
halves with a marker on the edges where they would meet, and either half names the window it is
|
||||||
|
part of: the half with room to say when the whole of it is says it, and is the same answer at
|
||||||
|
both ends of the strip. */
|
||||||
|
const auto DrawSlotHalf = [&](float X, float RectWidth, float Y, const FSlotView& Slot, float MarkerX)
|
||||||
|
{
|
||||||
|
DrawRect(X, Y, RectWidth, LaneHeight, Slot.Color, LayerId + 2);
|
||||||
|
|
||||||
|
// A brighter edge along the top, so that two slots of the same colour meeting at an hour are
|
||||||
|
// still two slots.
|
||||||
|
FLinearColor TopColor = Slot.Color * 1.35f;
|
||||||
|
TopColor.A = 1.0f;
|
||||||
|
DrawRect(X, Y, RectWidth, 2.0f, TopColor, LayerId + 3);
|
||||||
|
|
||||||
|
if (MarkerX >= 0.0f)
|
||||||
|
{
|
||||||
|
DrawRect(MarkerX, Y, WrapMarkerWidth, LaneHeight, WrapMarkerColor, LayerId + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RectWidth < MinimumLabelledSlotWidth)
|
||||||
|
{
|
||||||
|
// Nothing is legible in a slot this narrow, and a label spilling out of its own slot would
|
||||||
|
// read as belonging to the one beside it. The legend below names it in full instead.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clipped to the slot it names, for the same reason: a slot that cannot hold its whole label
|
||||||
|
// shows as much of it as it can and not a letter of the next slot's.
|
||||||
|
const FGeometry HalfGeometry = AllottedGeometry.MakeChild(
|
||||||
|
FVector2f(RectWidth, LaneHeight), FSlateLayoutTransform(FVector2f(X, Y)));
|
||||||
|
const FGeometry LabelGeometry = HalfGeometry.MakeChild(
|
||||||
|
FVector2f(FMath::Max(0.0f, RectWidth - SlotLabelInset * 2.0f), SlotLabelHeight),
|
||||||
|
FSlateLayoutTransform(FVector2f(SlotLabelInset, (LaneHeight - SlotLabelHeight) * 0.5f)));
|
||||||
|
|
||||||
|
OutDrawElements.PushClip(FSlateClippingZone(HalfGeometry));
|
||||||
|
FSlateDrawElement::MakeText(OutDrawElements, LayerId + 4, LabelGeometry.ToPaintGeometry(),
|
||||||
|
Slot.BlockLabel, Font, DrawEffects, SlotLabelColor);
|
||||||
|
OutDrawElements.PopClip();
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const FSlotView& Slot : Slots)
|
||||||
|
{
|
||||||
|
const float Y = LaneTop(Slot.LaneIndex);
|
||||||
|
|
||||||
|
// The part of the slot that fits inside the day, which is all of it unless the window wraps.
|
||||||
|
const int32 FirstEndMinutes = FMath::Min(Slot.EndMinutes, MinutesPerDay);
|
||||||
|
const float FirstX = XOfMinute(Slot.StartMinutes);
|
||||||
|
const float FirstWidth = XOfMinute(FirstEndMinutes) - FirstX;
|
||||||
|
const bool bWraps = Slot.EndMinutes > MinutesPerDay;
|
||||||
|
|
||||||
|
DrawSlotHalf(FirstX, FirstWidth, Y, Slot,
|
||||||
|
bWraps ? XOfMinute(MinutesPerDay) - WrapMarkerWidth : -1.0f);
|
||||||
|
|
||||||
|
if (bWraps)
|
||||||
|
{
|
||||||
|
// The rest of the window, at the start of the same day: a 22:00-06:00 night is a slot from
|
||||||
|
// 22:00 to midnight here, and from midnight to 06:00 at the left edge of the strip.
|
||||||
|
DrawSlotHalf(StripLeft, XOfMinute(Slot.EndMinutes - MinutesPerDay) - StripLeft, Y, Slot, StripLeft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Slots.Num() == 0)
|
||||||
|
{
|
||||||
|
// Said in the trough rather than left blank, because a day with nothing in it is either a
|
||||||
|
// schedule nobody has written yet or an asset that is no longer there, and the two read the
|
||||||
|
// same from here without something to tell them apart.
|
||||||
|
DrawText(StripLeft + SlotLabelInset, DayTop + (LaneHeight - SlotLabelHeight) * 0.5f,
|
||||||
|
StripWidth - SlotLabelInset * 2.0f, SlotLabelHeight,
|
||||||
|
Schedule.IsValid()
|
||||||
|
? LOCTEXT("EmptySchedule", "No actions in this schedule, so the day is empty.")
|
||||||
|
: LOCTEXT("NoSchedule", "No schedule is being drawn."),
|
||||||
|
QuietTextColor, LayerId + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The legend, in the order the day reads rather than the order the array holds, so that
|
||||||
|
// walking it left to right walks the slots left to right as well.
|
||||||
|
int32 RowIndex = 0;
|
||||||
|
for (const FSlotView& Slot : Slots)
|
||||||
|
{
|
||||||
|
const float RowY = LegendTop + RowIndex * LegendRowHeight;
|
||||||
|
|
||||||
|
DrawRect(StripLeft, RowY + (LegendRowHeight - LegendSwatchWidth) * 0.5f,
|
||||||
|
LegendSwatchWidth, LegendSwatchWidth, Slot.Color, LayerId + 5);
|
||||||
|
DrawText(StripLeft + LegendSwatchWidth + LegendSwatchGap, RowY,
|
||||||
|
FMath::Max(0.0f, StripWidth - LegendSwatchWidth - LegendSwatchGap), SlotLabelHeight,
|
||||||
|
Slot.LegendLabel, LegendTextColor, LayerId + 5);
|
||||||
|
|
||||||
|
++RowIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayerId + 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "ItemDataAssetCustomization.h"
|
#include "ItemDataAssetCustomization.h"
|
||||||
#include "ItemEditorWindow.h"
|
#include "ItemEditorWindow.h"
|
||||||
#include "NpcScheduleActionCustomization.h"
|
#include "NpcScheduleActionCustomization.h"
|
||||||
|
#include "NpcScheduleAssetCustomization.h"
|
||||||
#include "ScheduleAssetRecheck.h"
|
#include "ScheduleAssetRecheck.h"
|
||||||
|
|
||||||
#define LOCTEXT_NAMESPACE "ProjectEleriEditorModule"
|
#define LOCTEXT_NAMESPACE "ProjectEleriEditorModule"
|
||||||
@@ -35,6 +36,14 @@ void FProjectEleriEditorModule::StartupModule()
|
|||||||
FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FNpcScheduleActionCustomization::MakeInstance)
|
FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FNpcScheduleActionCustomization::MakeInstance)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Details customization for the schedule asset itself, which draws the day its actions describe as
|
||||||
|
// a strip under everything else the panel shows. Registered under the class's script name, which is
|
||||||
|
// the C++ class name without its U prefix.
|
||||||
|
PropEd.RegisterCustomClassLayout(
|
||||||
|
TEXT("NpcScheduleAsset"),
|
||||||
|
FOnGetDetailCustomizationInstance::CreateStatic(&FNpcScheduleAssetCustomization::MakeInstance)
|
||||||
|
);
|
||||||
|
|
||||||
// Register the Item Editor as a docking tab.
|
// Register the Item Editor as a docking tab.
|
||||||
if (FGlobalTabmanager::Get()->HasTabSpawner(ItemEditorTabName))
|
if (FGlobalTabmanager::Get()->HasTabSpawner(ItemEditorTabName))
|
||||||
{
|
{
|
||||||
@@ -75,6 +84,7 @@ void FProjectEleriEditorModule::ShutdownModule()
|
|||||||
FPropertyEditorModule& PropEd = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
FPropertyEditorModule& PropEd = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
||||||
PropEd.UnregisterCustomClassLayout(TEXT("ItemDataAsset"));
|
PropEd.UnregisterCustomClassLayout(TEXT("ItemDataAsset"));
|
||||||
PropEd.UnregisterCustomPropertyTypeLayout(TEXT("ScheduledAction"));
|
PropEd.UnregisterCustomPropertyTypeLayout(TEXT("ScheduledAction"));
|
||||||
|
PropEd.UnregisterCustomClassLayout(TEXT("NpcScheduleAsset"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FGlobalTabmanager::Get()->HasTabSpawner(ItemEditorTabName))
|
if (FGlobalTabmanager::Get()->HasTabSpawner(ItemEditorTabName))
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IDetailCustomization.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Details panel layout for UNpcScheduleAsset: everything the asset already had, with the day its
|
||||||
|
* actions describe drawn underneath it as a strip, see SNpcScheduleTimeline.
|
||||||
|
*
|
||||||
|
* All this does is put the strip there. It adds no row for any property and takes none away, so the
|
||||||
|
* actions themselves keep the rows the panel and FNpcScheduleActionCustomization give them: the strip
|
||||||
|
* is a view of what those rows say, and a second place to write an action would be a second answer to
|
||||||
|
* what a day is.
|
||||||
|
*
|
||||||
|
* The strip goes in a category of its own sorted after the one the asset's properties land in, which
|
||||||
|
* is what puts it at the bottom of the editor. A category is as close as a details customization can
|
||||||
|
* get to the bottom of anything: the panel is one scrolling column and everything in it is a row of
|
||||||
|
* it, so the bottom of the panel is the bottom of the layout, under every row rather than pinned to
|
||||||
|
* the edge of the window the way a status bar would be.
|
||||||
|
*/
|
||||||
|
class FNpcScheduleAssetCustomization : public IDetailCustomization {
|
||||||
|
public:
|
||||||
|
static TSharedRef<IDetailCustomization> MakeInstance();
|
||||||
|
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
|
||||||
|
};
|
||||||
119
Source/ProjectEleriEditor/Public/NpcScheduleTimeline.h
Normal file
119
Source/ProjectEleriEditor/Public/NpcScheduleTimeline.h
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Fonts/SlateFontInfo.h"
|
||||||
|
#include "UObject/WeakObjectPtrTemplates.h"
|
||||||
|
#include "Widgets/SCompoundWidget.h"
|
||||||
|
|
||||||
|
class UNpcScheduleAsset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A schedule's day drawn as one strip: an hour ruler across the top, one row of slots under it with
|
||||||
|
* the actions that claim them, and a legend under that naming every action, the window it holds and
|
||||||
|
* the anchor it happens at.
|
||||||
|
*
|
||||||
|
* It reads the day out of the actions' start times, because a start is all an FScheduledAction has:
|
||||||
|
* the end of a window is the next start in the schedule, which is also the rule the asset states for
|
||||||
|
* itself, so a 06:00-22:00 and a 22:00-06:00 pair tile the day, a window that runs past midnight is
|
||||||
|
* drawn as two halves of one slot with a marker where it carries on, and a schedule whose actions all
|
||||||
|
* start at one minute covers the whole day with each of them, which is what the game reads it as.
|
||||||
|
*
|
||||||
|
* Two actions authored at the same minute therefore claim one slot between them, and that is the only
|
||||||
|
* case that needs more than one row: the day is drawn on as many lanes as there are claims on the
|
||||||
|
* most-claimed minute, so a minute two actions fight over is seen rather than drawn twice in the same
|
||||||
|
* place.
|
||||||
|
*
|
||||||
|
* Nothing here is interactive. It says what the asset says and leaves the writing to the array above
|
||||||
|
* it, which is the one place an action can be authored.
|
||||||
|
*
|
||||||
|
* The strip draws itself from whatever width the panel gives it, read back at paint time, so it grows
|
||||||
|
* and shrinks with the details panel without ever being laid out again to do it. What is worked out
|
||||||
|
* ahead of painting - the slots, the ruler, the colours, the names - is worked out when the schedule
|
||||||
|
* changes and not while painting, because a rebuild is also what walks the level to name the anchors
|
||||||
|
* and that is not something to do once a frame.
|
||||||
|
*/
|
||||||
|
class SNpcScheduleTimeline : public SCompoundWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SLATE_BEGIN_ARGS(SNpcScheduleTimeline)
|
||||||
|
: _Schedule(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
/** The asset to draw. Weak because a details panel outlives the asset it was opened on, and a
|
||||||
|
* deleted asset has to leave an empty strip rather than a dangling one. */
|
||||||
|
SLATE_ARGUMENT(TWeakObjectPtr<UNpcScheduleAsset>, Schedule)
|
||||||
|
SLATE_END_ARGS()
|
||||||
|
|
||||||
|
void Construct(const FArguments& InArgs);
|
||||||
|
|
||||||
|
//~ Begin SWidget interface
|
||||||
|
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
|
||||||
|
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
|
||||||
|
//~ End SWidget interface
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** One slot of the day: the window one action claims, which lane it is drawn on because something
|
||||||
|
* else claims that window too, and the two labels it is drawn with. Worked out by Rebuild. */
|
||||||
|
struct FSlotView
|
||||||
|
{
|
||||||
|
/** First minute of the day in the slot, 0 at midnight. */
|
||||||
|
int32 StartMinutes = 0;
|
||||||
|
|
||||||
|
/** The minute the slot ends at, exclusive, and past midnight rather than wrapped to it, so a
|
||||||
|
* slot that carries on at the start of the next day can be told from one that does not. */
|
||||||
|
int32 EndMinutes = 0;
|
||||||
|
|
||||||
|
/** Row the slot is drawn on. Only ever more than 0 under a window another action claims too. */
|
||||||
|
int32 LaneIndex = 0;
|
||||||
|
|
||||||
|
/** Colour of the activity, so that a legend row and its slot cannot disagree about it. */
|
||||||
|
FLinearColor Color = FLinearColor::Gray;
|
||||||
|
|
||||||
|
/** Activity and window, as it reads inside the slot: "Sleep 22:00 - 06:00". */
|
||||||
|
FText BlockLabel;
|
||||||
|
|
||||||
|
/** Activity, window and anchor, as it reads in the legend. Longer than BlockLabel because a
|
||||||
|
* slot can be far too narrow to hold anything and the legend is where the rest is said. */
|
||||||
|
FText LegendLabel;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A cheap read of everything the strip draws, so that a change can be noticed by reading the
|
||||||
|
* asset rather than by subscribing to every way one of its actions can be written: an element
|
||||||
|
* added, removed, reordered or edited inside a row all come out as a different signature here.
|
||||||
|
* What the strip does not draw is deliberately not in it, so moving an anchor to the side of the
|
||||||
|
* room it was already picked at does not make the strip rebuild itself. */
|
||||||
|
uint32 ComputeSignature() const;
|
||||||
|
|
||||||
|
/** Works the slots, the lanes and the legend out of the schedule, and names the anchors they point
|
||||||
|
* at with one walk of the open level. Does not repaint: the caller does that, because a rebuild
|
||||||
|
* during Construct happens before there is anything to repaint. */
|
||||||
|
void Rebuild(uint32 InSignature, double InCurrentTime);
|
||||||
|
|
||||||
|
/** The asset being drawn. Null while nothing has been assigned or after it was deleted. */
|
||||||
|
TWeakObjectPtr<UNpcScheduleAsset> Schedule;
|
||||||
|
|
||||||
|
/** Every slot of the day, in the order the day reads: earliest start first, and actions that share
|
||||||
|
* a start in the order the schedule holds them. The legend is drawn straight off this as well, so
|
||||||
|
* that a line and the slot it belongs to cannot come apart. */
|
||||||
|
TArray<FSlotView> Slots;
|
||||||
|
|
||||||
|
/** How many lanes the slots need. 1 whenever no two actions were authored at the same minute. */
|
||||||
|
int32 LaneCount = 0;
|
||||||
|
|
||||||
|
/** Labels of the hour ruler, one every third hour, made once because they never change. */
|
||||||
|
TArray<FText> HourLabels;
|
||||||
|
|
||||||
|
/** The signature of the schedule the strip was last built from. */
|
||||||
|
uint32 CachedSignature = 0;
|
||||||
|
|
||||||
|
/** When that build happened, so that the anchor names can be re-read on a slow timer. */
|
||||||
|
double LastBuildTime = 0.0;
|
||||||
|
|
||||||
|
/** The font the ruler, the slots and the legend are drawn in. */
|
||||||
|
FSlateFontInfo Font;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user