1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3
4#include "editor.h"
5
6#include <base/color.h>
7#include <base/math.h>
8#include <base/str.h>
9#include <base/vmath.h>
10
11#include <engine/font_icons.h>
12#include <engine/graphics.h>
13#include <engine/input.h>
14#include <engine/keys.h>
15#include <engine/shared/config.h>
16
17#include <game/editor/editor.h>
18#include <game/editor/editor_actions.h>
19#include <game/editor/editor_trackers.h>
20#include <game/editor/mapitems/envelope.h>
21#include <game/editor/mapitems/map.h>
22
23static const char *const CURVE_TYPE_NAMES[] = {"Step", "Linear", "Slow", "Fast", "Smooth", "Bezier"};
24static const char *const CURVE_TYPE_NAMES_SHORT[] = {"N", "L", "S", "F", "M", "B"};
25static_assert(std::size(CURVE_TYPE_NAMES) == NUM_CURVETYPES);
26static_assert(std::size(CURVE_TYPE_NAMES_SHORT) == NUM_CURVETYPES);
27
28static const char *CurveTypeNameShort(int CurveType)
29{
30 if(in_range<int>(a: CurveType, lower: 0, upper: std::size(CURVE_TYPE_NAMES_SHORT) - 1))
31 {
32 return CURVE_TYPE_NAMES_SHORT[CurveType];
33 }
34 return "!?";
35}
36
37static float ClampDelta(float Val, float Delta, float Min, float Max)
38{
39 if(Val + Delta <= Min)
40 return Min - Val;
41 if(Val + Delta >= Max)
42 return Max - Val;
43 return Delta;
44}
45
46class CTimeStep
47{
48public:
49 template<class T>
50 CTimeStep(T t)
51 {
52 if constexpr(std::is_same_v<T, std::chrono::milliseconds>)
53 m_Unit = ETimeUnit::MILLISECONDS;
54 else if constexpr(std::is_same_v<T, std::chrono::seconds>)
55 m_Unit = ETimeUnit::SECONDS;
56 else
57 m_Unit = ETimeUnit::MINUTES;
58
59 m_Value = t;
60 }
61
62 CTimeStep operator*(int k) const
63 {
64 return CTimeStep(m_Value * k, m_Unit);
65 }
66
67 CTimeStep operator-(const CTimeStep &Other)
68 {
69 return CTimeStep(m_Value - Other.m_Value, m_Unit);
70 }
71
72 void Format(char *pBuffer, size_t BufferSize)
73 {
74 int Milliseconds = m_Value.count() % 1000;
75 int Seconds = std::chrono::duration_cast<std::chrono::seconds>(d: m_Value).count() % 60;
76 int Minutes = std::chrono::duration_cast<std::chrono::minutes>(d: m_Value).count();
77
78 switch(m_Unit)
79 {
80 case ETimeUnit::MILLISECONDS:
81 if(Minutes != 0)
82 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%d:%02d.%03dmin", Minutes, Seconds, Milliseconds);
83 else if(Seconds != 0)
84 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%d.%03ds", Seconds, Milliseconds);
85 else
86 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%dms", Milliseconds);
87 break;
88 case ETimeUnit::SECONDS:
89 if(Minutes != 0)
90 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%d:%02dmin", Minutes, Seconds);
91 else
92 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%ds", Seconds);
93 break;
94 case ETimeUnit::MINUTES:
95 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%dmin", Minutes);
96 break;
97 }
98 }
99
100 float AsSeconds() const
101 {
102 return std::chrono::duration_cast<std::chrono::duration<float>>(d: m_Value).count();
103 }
104
105private:
106 enum class ETimeUnit
107 {
108 MILLISECONDS,
109 SECONDS,
110 MINUTES,
111 } m_Unit;
112 std::chrono::milliseconds m_Value;
113
114 CTimeStep(std::chrono::milliseconds Value, ETimeUnit Unit)
115 {
116 m_Value = Value;
117 m_Unit = Unit;
118 }
119};
120
121void CEditor::RenderEnvelopeEditor(CUIRect View)
122{
123 Map()->m_SelectedEnvelope = Map()->m_vpEnvelopes.empty() ? -1 : std::clamp(val: Map()->m_SelectedEnvelope, lo: 0, hi: (int)Map()->m_vpEnvelopes.size() - 1);
124 std::shared_ptr<CEnvelope> pEnvelope = Map()->m_vpEnvelopes.empty() ? nullptr : Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
125
126 static EEnvelopeEditorOp s_Operation = EEnvelopeEditorOp::NONE;
127 static std::vector<float> s_vAccurateDragValuesX = {};
128 static std::vector<float> s_vAccurateDragValuesY = {};
129 static float s_MouseXStart = 0.0f;
130 static float s_MouseYStart = 0.0f;
131
132 static CLineInput s_NameInput;
133
134 CUIRect ToolBar, CurveBar, ColorBar, DragBar;
135 View.HSplitTop(Cut: 30.0f, pTop: &DragBar, pBottom: nullptr);
136 DragBar.y -= 2.0f;
137 DragBar.w += 2.0f;
138 DragBar.h += 4.0f;
139 DoEditorDragBar(View, pDragBar: &DragBar, Side: EDragSide::TOP, pValue: &m_aExtraEditorSplits[EXTRAEDITOR_ENVELOPES]);
140 View.HSplitTop(Cut: 15.0f, pTop: &ToolBar, pBottom: &View);
141 View.HSplitTop(Cut: 15.0f, pTop: &CurveBar, pBottom: &View);
142 ToolBar.Margin(Cut: 2.0f, pOtherRect: &ToolBar);
143 CurveBar.Margin(Cut: 2.0f, pOtherRect: &CurveBar);
144
145 bool CurrentEnvelopeSwitched = false;
146
147 // do the toolbar
148 static int s_ActiveChannels = 0xf;
149 {
150 CUIRect Button;
151
152 // redo button
153 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
154 static int s_RedoButton = 0;
155 if(DoButton_FontIcon(pId: &s_RedoButton, pText: FontIcon::REDO, Checked: Map()->m_EnvelopeEditorHistory.CanRedo() ? 0 : -1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+Y] Redo the last action.", Corners: IGraphics::CORNER_R, FontSize: 11.0f) == 1)
156 {
157 Map()->m_EnvelopeEditorHistory.Redo();
158 }
159
160 // undo button
161 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
162 ToolBar.VSplitRight(Cut: 10.0f, pLeft: &ToolBar, pRight: nullptr);
163 static int s_UndoButton = 0;
164 if(DoButton_FontIcon(pId: &s_UndoButton, pText: FontIcon::UNDO, Checked: Map()->m_EnvelopeEditorHistory.CanUndo() ? 0 : -1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+Z] Undo the last action.", Corners: IGraphics::CORNER_L, FontSize: 11.0f) == 1)
165 {
166 Map()->m_EnvelopeEditorHistory.Undo();
167 }
168
169 ToolBar.VSplitRight(Cut: 50.0f, pLeft: &ToolBar, pRight: &Button);
170 static int s_NewSoundButton = 0;
171 if(DoButton_Editor(pId: &s_NewSoundButton, pText: "Sound+", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Create a new sound envelope."))
172 {
173 Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionEnvelopeAdd>(args: Map(), args: CEnvelope::EType::SOUND));
174 pEnvelope = Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
175 CurrentEnvelopeSwitched = true;
176 }
177
178 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
179 ToolBar.VSplitRight(Cut: 50.0f, pLeft: &ToolBar, pRight: &Button);
180 static int s_New4dButton = 0;
181 if(DoButton_Editor(pId: &s_New4dButton, pText: "Color+", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Create a new color envelope."))
182 {
183 Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionEnvelopeAdd>(args: Map(), args: CEnvelope::EType::COLOR));
184 pEnvelope = Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
185 CurrentEnvelopeSwitched = true;
186 }
187
188 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
189 ToolBar.VSplitRight(Cut: 50.0f, pLeft: &ToolBar, pRight: &Button);
190 static int s_New2dButton = 0;
191 if(DoButton_Editor(pId: &s_New2dButton, pText: "Pos.+", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Create a new position envelope."))
192 {
193 Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionEnvelopeAdd>(args: Map(), args: CEnvelope::EType::POSITION));
194 pEnvelope = Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
195 CurrentEnvelopeSwitched = true;
196 }
197
198 if(Map()->m_SelectedEnvelope >= 0)
199 {
200 // Delete button
201 ToolBar.VSplitRight(Cut: 10.0f, pLeft: &ToolBar, pRight: nullptr);
202 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
203 static int s_DeleteButton = 0;
204 if(DoButton_Editor(pId: &s_DeleteButton, pText: "✗", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Delete this envelope."))
205 {
206 auto vpObjectReferences = Map()->DeleteEnvelope(Index: Map()->m_SelectedEnvelope);
207 Map()->m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeDelete>(args: Map(), args&: Map()->m_SelectedEnvelope, args&: vpObjectReferences, args&: pEnvelope));
208
209 Map()->m_SelectedEnvelope = Map()->m_vpEnvelopes.empty() ? -1 : std::clamp(val: Map()->m_SelectedEnvelope, lo: 0, hi: (int)Map()->m_vpEnvelopes.size() - 1);
210 pEnvelope = Map()->m_vpEnvelopes.empty() ? nullptr : Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
211 Map()->OnModify();
212 }
213 }
214
215 // check again, because the last envelope might has been deleted
216 if(Map()->m_SelectedEnvelope >= 0)
217 {
218 // Move right button
219 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
220 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
221 static int s_MoveRightButton = 0;
222 if(DoButton_Ex(pId: &s_MoveRightButton, pText: "→", Checked: (Map()->m_SelectedEnvelope >= (int)Map()->m_vpEnvelopes.size() - 1 ? -1 : 0), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Move this envelope to the right.", Corners: IGraphics::CORNER_R))
223 {
224 int MoveTo = Map()->m_SelectedEnvelope + 1;
225 int MoveFrom = Map()->m_SelectedEnvelope;
226 Map()->m_SelectedEnvelope = Map()->MoveEnvelope(IndexFrom: MoveFrom, IndexTo: MoveTo);
227 if(Map()->m_SelectedEnvelope != MoveFrom)
228 {
229 Map()->m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEdit>(args: Map(), args&: Map()->m_SelectedEnvelope, args: CEditorActionEnvelopeEdit::EEditType::ORDER, args&: MoveFrom, args&: Map()->m_SelectedEnvelope));
230 pEnvelope = Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
231 Map()->OnModify();
232 }
233 }
234
235 // Move left button
236 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
237 static int s_MoveLeftButton = 0;
238 if(DoButton_Ex(pId: &s_MoveLeftButton, pText: "←", Checked: (Map()->m_SelectedEnvelope <= 0 ? -1 : 0), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Move this envelope to the left.", Corners: IGraphics::CORNER_L))
239 {
240 int MoveTo = Map()->m_SelectedEnvelope - 1;
241 int MoveFrom = Map()->m_SelectedEnvelope;
242 Map()->m_SelectedEnvelope = Map()->MoveEnvelope(IndexFrom: MoveFrom, IndexTo: MoveTo);
243 if(Map()->m_SelectedEnvelope != MoveFrom)
244 {
245 Map()->m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEdit>(args: Map(), args&: Map()->m_SelectedEnvelope, args: CEditorActionEnvelopeEdit::EEditType::ORDER, args&: MoveFrom, args&: Map()->m_SelectedEnvelope));
246 pEnvelope = Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
247 Map()->OnModify();
248 }
249 }
250
251 if(pEnvelope)
252 {
253 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
254 ToolBar.VSplitRight(Cut: 20.0f, pLeft: &ToolBar, pRight: &Button);
255 static int s_ZoomOutButton = 0;
256 if(DoButton_FontIcon(pId: &s_ZoomOutButton, pText: FontIcon::MINUS, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[NumPad-] Zoom out horizontally, hold shift to zoom vertically.", Corners: IGraphics::CORNER_R, FontSize: 9.0f))
257 {
258 if(Input()->ShiftIsPressed())
259 m_ZoomEnvelopeY.ScaleValue(Factor: 1.1f);
260 else
261 m_ZoomEnvelopeX.ScaleValue(Factor: 1.1f);
262 }
263
264 ToolBar.VSplitRight(Cut: 20.0f, pLeft: &ToolBar, pRight: &Button);
265 static int s_ResetZoomButton = 0;
266 if(DoButton_FontIcon(pId: &s_ResetZoomButton, pText: FontIcon::MAGNIFYING_GLASS, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[NumPad*] Reset zoom to default value.", Corners: IGraphics::CORNER_NONE, FontSize: 9.0f))
267 ResetZoomEnvelope(pEnvelope, ActiveChannels: s_ActiveChannels);
268
269 ToolBar.VSplitRight(Cut: 20.0f, pLeft: &ToolBar, pRight: &Button);
270 static int s_ZoomInButton = 0;
271 if(DoButton_FontIcon(pId: &s_ZoomInButton, pText: FontIcon::PLUS, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[NumPad+] Zoom in horizontally, hold shift to zoom vertically.", Corners: IGraphics::CORNER_L, FontSize: 9.0f))
272 {
273 if(Input()->ShiftIsPressed())
274 m_ZoomEnvelopeY.ScaleValue(Factor: 1.0f / 1.1f);
275 else
276 m_ZoomEnvelopeX.ScaleValue(Factor: 1.0f / 1.1f);
277 }
278 }
279
280 // Margin on the right side
281 ToolBar.VSplitRight(Cut: 7.0f, pLeft: &ToolBar, pRight: nullptr);
282 }
283
284 CUIRect Shifter, Inc, Dec;
285 ToolBar.VSplitLeft(Cut: 60.0f, pLeft: &Shifter, pRight: &ToolBar);
286 Shifter.VSplitRight(Cut: 15.0f, pLeft: &Shifter, pRight: &Inc);
287 Shifter.VSplitLeft(Cut: 15.0f, pLeft: &Dec, pRight: &Shifter);
288 char aBuf[64];
289 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d/%d", Map()->m_SelectedEnvelope + 1, (int)Map()->m_vpEnvelopes.size());
290
291 ColorRGBA EnvColor = ColorRGBA(1, 1, 1, 0.5f);
292 if(!Map()->m_vpEnvelopes.empty())
293 {
294 EnvColor = Map()->IsEnvelopeUsed(EnvelopeIndex: Map()->m_SelectedEnvelope) ? ColorRGBA(1, 0.7f, 0.7f, 0.5f) : ColorRGBA(0.7f, 1, 0.7f, 0.5f);
295 }
296
297 static int s_EnvelopeSelector = 0;
298 auto NewValueRes = UiDoValueSelector(pId: &s_EnvelopeSelector, pRect: &Shifter, pLabel: aBuf, Current: Map()->m_SelectedEnvelope + 1, Min: 1, Max: Map()->m_vpEnvelopes.size(), Step: 1, Scale: 1.0f, pToolTip: "Select the envelope.", IsDegree: false, IsHex: false, Corners: IGraphics::CORNER_NONE, pColor: &EnvColor, ShowValue: false);
299 int NewValue = NewValueRes.m_Value;
300 if(NewValue - 1 != Map()->m_SelectedEnvelope)
301 {
302 Map()->m_SelectedEnvelope = NewValue - 1;
303 CurrentEnvelopeSwitched = true;
304 }
305
306 static int s_PrevButton = 0;
307 if(DoButton_FontIcon(pId: &s_PrevButton, pText: FontIcon::MINUS, Checked: 0, pRect: &Dec, Flags: BUTTONFLAG_LEFT, pToolTip: "Select previous envelope.", Corners: IGraphics::CORNER_L, FontSize: 7.0f))
308 {
309 Map()->m_SelectedEnvelope--;
310 if(Map()->m_SelectedEnvelope < 0)
311 Map()->m_SelectedEnvelope = Map()->m_vpEnvelopes.size() - 1;
312 CurrentEnvelopeSwitched = true;
313 }
314
315 static int s_NextButton = 0;
316 if(DoButton_FontIcon(pId: &s_NextButton, pText: FontIcon::PLUS, Checked: 0, pRect: &Inc, Flags: BUTTONFLAG_LEFT, pToolTip: "Select next envelope.", Corners: IGraphics::CORNER_R, FontSize: 7.0f))
317 {
318 Map()->m_SelectedEnvelope++;
319 if(Map()->m_SelectedEnvelope >= (int)Map()->m_vpEnvelopes.size())
320 Map()->m_SelectedEnvelope = 0;
321 CurrentEnvelopeSwitched = true;
322 }
323
324 if(pEnvelope)
325 {
326 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: nullptr, pRight: &ToolBar);
327 ToolBar.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolBar);
328 Ui()->DoLabel(pRect: &Button, pText: "Name:", Size: 10.0f, Align: TEXTALIGN_MR);
329
330 ToolBar.VSplitLeft(Cut: 3.0f, pLeft: nullptr, pRight: &ToolBar);
331 ToolBar.VSplitLeft(Cut: ToolBar.w > ToolBar.h * 40 ? 80.0f : 60.0f, pLeft: &Button, pRight: &ToolBar);
332
333 s_NameInput.SetBuffer(pStr: pEnvelope->m_aName, MaxSize: sizeof(pEnvelope->m_aName));
334 if(DoEditBox(pLineInput: &s_NameInput, pRect: &Button, FontSize: 10.0f, Corners: IGraphics::CORNER_ALL, pToolTip: "The name of the selected envelope."))
335 {
336 Map()->OnModify();
337 }
338 }
339 }
340
341 const bool ShowColorBar = pEnvelope && pEnvelope->GetChannels() == 4;
342 if(ShowColorBar)
343 {
344 View.HSplitTop(Cut: 20.0f, pTop: &ColorBar, pBottom: &View);
345 ColorBar.HMargin(Cut: 2.0f, pOtherRect: &ColorBar);
346 }
347
348 RenderBackground(View, Texture: m_CheckerTexture, Size: 32.0f, Brightness: 0.1f);
349
350 if(pEnvelope)
351 {
352 if(m_ResetZoomEnvelope)
353 {
354 m_ResetZoomEnvelope = false;
355 ResetZoomEnvelope(pEnvelope, ActiveChannels: s_ActiveChannels);
356 }
357
358 ColorRGBA aColors[] = {ColorRGBA(1, 0.2f, 0.2f), ColorRGBA(0.2f, 1, 0.2f), ColorRGBA(0.2f, 0.2f, 1), ColorRGBA(1, 1, 0.2f)};
359
360 CUIRect Button;
361
362 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: &Button, pRight: &ToolBar);
363
364 static const char *const CHANNEL_NAMES[CEnvPoint::MAX_CHANNELS][CEnvPoint::MAX_CHANNELS] = {
365 {"V", "", "", ""},
366 {"", "", "", ""},
367 {"X", "Y", "R", ""},
368 {"R", "G", "B", "A"},
369 };
370
371 static const char *const CHANNEL_DESCRIPTIONS[CEnvPoint::MAX_CHANNELS][CEnvPoint::MAX_CHANNELS] = {
372 {"Volume of the envelope.", "", "", ""},
373 {"", "", "", ""},
374 {"X-axis of the envelope.", "Y-axis of the envelope.", "Rotation of the envelope.", ""},
375 {"Red value of the envelope.", "Green value of the envelope.", "Blue value of the envelope.", "Alpha value of the envelope."},
376 };
377
378 static int s_aChannelButtons[CEnvPoint::MAX_CHANNELS] = {0};
379 int Bit = 1;
380
381 for(int i = 0; i < CEnvPoint::MAX_CHANNELS; i++, Bit <<= 1)
382 {
383 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: &Button, pRight: &ToolBar);
384 if(i < pEnvelope->GetChannels())
385 {
386 int Corners = IGraphics::CORNER_NONE;
387 if(pEnvelope->GetChannels() == 1)
388 Corners = IGraphics::CORNER_ALL;
389 else if(i == 0)
390 Corners = IGraphics::CORNER_L;
391 else if(i == pEnvelope->GetChannels() - 1)
392 Corners = IGraphics::CORNER_R;
393
394 if(DoButton_Env(pId: &s_aChannelButtons[i], pText: CHANNEL_NAMES[pEnvelope->GetChannels() - 1][i], Checked: s_ActiveChannels & Bit, pRect: &Button, pToolTip: CHANNEL_DESCRIPTIONS[pEnvelope->GetChannels() - 1][i], Color: aColors[i], Corners))
395 s_ActiveChannels ^= Bit;
396 }
397 }
398
399 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: nullptr, pRight: &ToolBar);
400 ToolBar.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolBar);
401
402 static int s_EnvelopeEditorId = 0;
403 static int s_EnvelopeEditorButtonUsed = -1;
404 const bool ShouldPan = s_Operation == EEnvelopeEditorOp::NONE && (Ui()->MouseButton(Index: 2) || (Ui()->MouseButton(Index: 0) && Input()->ModifierIsPressed()));
405 if(m_pContainerPanned == &s_EnvelopeEditorId)
406 {
407 if(!ShouldPan)
408 m_pContainerPanned = nullptr;
409 else
410 {
411 m_OffsetEnvelopeX += Ui()->MouseDeltaX() / Graphics()->ScreenWidth() * Ui()->Screen()->w / View.w;
412 m_OffsetEnvelopeY -= Ui()->MouseDeltaY() / Graphics()->ScreenHeight() * Ui()->Screen()->h / View.h;
413 }
414 }
415
416 if(Ui()->MouseInside(pRect: &View) && m_Dialog == DIALOG_NONE)
417 {
418 Ui()->SetHotItem(&s_EnvelopeEditorId);
419
420 if(ShouldPan && m_pContainerPanned == nullptr)
421 m_pContainerPanned = &s_EnvelopeEditorId;
422
423 if(Input()->KeyPress(Key: KEY_KP_MULTIPLY) && CLineInput::GetActiveInput() == nullptr)
424 ResetZoomEnvelope(pEnvelope, ActiveChannels: s_ActiveChannels);
425 if(Input()->ShiftIsPressed())
426 {
427 if(Input()->KeyPress(Key: KEY_KP_MINUS) && CLineInput::GetActiveInput() == nullptr)
428 m_ZoomEnvelopeY.ScaleValue(Factor: 1.1f);
429 if(Input()->KeyPress(Key: KEY_KP_PLUS) && CLineInput::GetActiveInput() == nullptr)
430 m_ZoomEnvelopeY.ScaleValue(Factor: 1.0f / 1.1f);
431 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
432 m_ZoomEnvelopeY.ScaleValue(Factor: 1.1f);
433 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
434 m_ZoomEnvelopeY.ScaleValue(Factor: 1.0f / 1.1f);
435 }
436 else
437 {
438 if(Input()->KeyPress(Key: KEY_KP_MINUS) && CLineInput::GetActiveInput() == nullptr)
439 m_ZoomEnvelopeX.ScaleValue(Factor: 1.1f);
440 if(Input()->KeyPress(Key: KEY_KP_PLUS) && CLineInput::GetActiveInput() == nullptr)
441 m_ZoomEnvelopeX.ScaleValue(Factor: 1.0f / 1.1f);
442 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
443 m_ZoomEnvelopeX.ScaleValue(Factor: 1.1f);
444 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
445 m_ZoomEnvelopeX.ScaleValue(Factor: 1.0f / 1.1f);
446 }
447 }
448
449 if(Ui()->HotItem() == &s_EnvelopeEditorId)
450 {
451 // do stuff
452 if(Ui()->MouseButton(Index: 0))
453 {
454 s_EnvelopeEditorButtonUsed = 0;
455 if(s_Operation != EEnvelopeEditorOp::BOX_SELECT && !Input()->ModifierIsPressed())
456 {
457 s_Operation = EEnvelopeEditorOp::BOX_SELECT;
458 s_MouseXStart = Ui()->MouseX();
459 s_MouseYStart = Ui()->MouseY();
460 }
461 }
462 else if(s_EnvelopeEditorButtonUsed == 0)
463 {
464 if(Ui()->DoDoubleClickLogic(pId: &s_EnvelopeEditorId) && !Input()->ModifierIsPressed())
465 {
466 // add point
467 float Time = ScreenToEnvelopeX(View, x: Ui()->MouseX());
468 ColorRGBA Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
469 pEnvelope->Eval(Time: std::clamp(val: Time, lo: 0.0f, hi: pEnvelope->EndTime()), Result&: Channels, Channels: 4);
470
471 const CFixedTime FixedTime = CFixedTime::FromSeconds(Seconds: Time);
472 bool TimeFound = false;
473 for(CEnvPoint &Point : pEnvelope->m_vPoints)
474 {
475 if(Point.m_Time == FixedTime)
476 TimeFound = true;
477 }
478
479 if(!TimeFound)
480 Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionAddEnvelopePoint>(args: Map(), args&: Map()->m_SelectedEnvelope, args: FixedTime, args&: Channels));
481
482 if(FixedTime < CFixedTime(0))
483 RemoveTimeOffsetEnvelope(pEnvelope);
484 Map()->OnModify();
485 }
486 s_EnvelopeEditorButtonUsed = -1;
487 }
488
489 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
490 str_copy(dst&: m_aTooltip, src: "Double click to create a new point. Use shift to change the zoom axis. Press S to scale selected envelope points.");
491 }
492
493 UpdateZoomEnvelopeX(View);
494 UpdateZoomEnvelopeY(View);
495
496 {
497 float UnitsPerLineY = 0.001f;
498 static const float UNIT_PER_LINE_OPTIONS_Y[] = {0.005f, 0.01f, 0.025f, 0.05f, 0.1f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f, 16.0f, 32.0f, 2 * 32.0f, 5 * 32.0f, 10 * 32.0f, 20 * 32.0f, 50 * 32.0f, 100 * 32.0f};
499 for(float Value : UNIT_PER_LINE_OPTIONS_Y)
500 {
501 if(Value / m_ZoomEnvelopeY.GetValue() * View.h < 40.0f)
502 UnitsPerLineY = Value;
503 }
504 int NumLinesY = m_ZoomEnvelopeY.GetValue() / UnitsPerLineY + 1;
505
506 Ui()->ClipEnable(pRect: &View);
507 Graphics()->TextureClear();
508 Graphics()->LinesBegin();
509 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.2f);
510
511 float BaseValue = static_cast<int>(m_OffsetEnvelopeY * m_ZoomEnvelopeY.GetValue() / UnitsPerLineY) * UnitsPerLineY;
512 for(int i = 0; i <= NumLinesY; i++)
513 {
514 float Value = UnitsPerLineY * i - BaseValue;
515 IGraphics::CLineItem LineItem(View.x, EnvelopeToScreenY(View, y: Value), View.x + View.w, EnvelopeToScreenY(View, y: Value));
516 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
517 }
518
519 Graphics()->LinesEnd();
520
521 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
522 for(int i = 0; i <= NumLinesY; i++)
523 {
524 float Value = UnitsPerLineY * i - BaseValue;
525 char aValueBuffer[16];
526 if(UnitsPerLineY >= 1.0f)
527 {
528 str_format(buffer: aValueBuffer, buffer_size: sizeof(aValueBuffer), format: "%d", static_cast<int>(Value));
529 }
530 else
531 {
532 str_format(buffer: aValueBuffer, buffer_size: sizeof(aValueBuffer), format: "%.3f", Value);
533 }
534 Ui()->TextRender()->Text(x: View.x, y: EnvelopeToScreenY(View, y: Value) + 4.0f, Size: 8.0f, pText: aValueBuffer);
535 }
536 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
537 Ui()->ClipDisable();
538 }
539
540 // handle time bar
541 {
542 if(s_Operation == EEnvelopeEditorOp::NONE)
543 {
544 UpdateHotEnvelopeObject(View, pEnvelope: pEnvelope.get(), ActiveChannels: s_ActiveChannels);
545 }
546
547 ColorRGBA BarColor;
548 if(Ui()->CheckActiveItem(pId: &Map()->m_EnvelopeEvaluator.m_AnimateTime))
549 {
550 if(s_Operation == EEnvelopeEditorOp::SELECT)
551 {
552 float dx = s_MouseXStart - Ui()->MouseX();
553 float dy = s_MouseYStart - Ui()->MouseY();
554
555 if(dx * dx + dy * dy > 20.0f)
556 s_Operation = EEnvelopeEditorOp::DRAG_TIME_BAR;
557 }
558
559 if(s_Operation == EEnvelopeEditorOp::DRAG_TIME_BAR)
560 {
561 float DeltaX = ScreenToEnvelopeDX(View, DeltaX: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 0.05f : 1.0f);
562 Map()->m_EnvelopeEvaluator.m_AnimateTime += DeltaX / Map()->m_EnvelopeEvaluator.m_AnimateSpeed;
563 Map()->m_EnvelopeEvaluator.m_AnimateTime = std::max(a: Map()->m_EnvelopeEvaluator.m_AnimateTime, b: 0.0f);
564 }
565
566 if(!Ui()->MouseButton(Index: 0))
567 {
568 Ui()->SetActiveItem(nullptr);
569 s_Operation = EEnvelopeEditorOp::NONE;
570 }
571
572 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
573 BarColor = ColorRGBA(1.0f, 1.0f, 0.0f, 0.8f);
574 str_copy(dst&: m_aTooltip, src: "Timebar. Press left-click to drag. Hold ctrl to be more precise.");
575 }
576 else if(Ui()->HotItem() == &Map()->m_EnvelopeEvaluator.m_AnimateTime)
577 {
578 if(Ui()->MouseButton(Index: 0))
579 {
580 Ui()->SetActiveItem(&Map()->m_EnvelopeEvaluator.m_AnimateTime);
581 s_Operation = EEnvelopeEditorOp::SELECT;
582
583 s_MouseXStart = Ui()->MouseX();
584 s_MouseYStart = Ui()->MouseY();
585 }
586
587 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
588 BarColor = ColorRGBA(1.0f, 1.0f, 0.0f, 0.8f);
589 str_copy(dst&: m_aTooltip, src: "Timebar. Press left-click to drag. Hold ctrl to be more precise.");
590 }
591 else
592 BarColor = ColorRGBA(1.0f, 1.0f, 0.0f, 0.5f);
593
594 float Time = Map()->m_EnvelopeEvaluator.m_AnimateTime * Map()->m_EnvelopeEvaluator.m_AnimateSpeed;
595 const float BarWidth = 1.5f;
596 CUIRect TimeBar{
597 .x: EnvelopeToScreenX(View, x: Time) - BarWidth / 2.0f,
598 .y: View.y,
599 .w: BarWidth,
600 .h: View.h,
601 };
602 TimeBar.Draw(Color: BarColor, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
603
604 if(Time > pEnvelope->EndTime())
605 {
606 float LoopedTime = std::fmod(x: Time, y: pEnvelope->EndTime());
607 TimeBar.x = EnvelopeToScreenX(View, x: LoopedTime) - BarWidth / 2.0f;
608 TimeBar.Draw(Color: BarColor, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
609 }
610 }
611
612 {
613 using namespace std::chrono_literals;
614 CTimeStep UnitsPerLineX = 1ms;
615 static const CTimeStep UNIT_PER_LINE_OPTIONS_X[] = {5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2s, 5s, 10s, 15s, 30s, 1min};
616 for(CTimeStep Value : UNIT_PER_LINE_OPTIONS_X)
617 {
618 if(Value.AsSeconds() / m_ZoomEnvelopeX.GetValue() * View.w < 160.0f)
619 UnitsPerLineX = Value;
620 }
621 int NumLinesX = m_ZoomEnvelopeX.GetValue() / UnitsPerLineX.AsSeconds() + 1;
622
623 Ui()->ClipEnable(pRect: &View);
624 Graphics()->TextureClear();
625 Graphics()->LinesBegin();
626 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.2f);
627
628 CTimeStep BaseValue = UnitsPerLineX * static_cast<int>(m_OffsetEnvelopeX * m_ZoomEnvelopeX.GetValue() / UnitsPerLineX.AsSeconds());
629 for(int i = 0; i <= NumLinesX; i++)
630 {
631 float Value = UnitsPerLineX.AsSeconds() * i - BaseValue.AsSeconds();
632 IGraphics::CLineItem LineItem(EnvelopeToScreenX(View, x: Value), View.y, EnvelopeToScreenX(View, x: Value), View.y + View.h);
633 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
634 }
635
636 Graphics()->LinesEnd();
637
638 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
639 for(int i = 0; i <= NumLinesX; i++)
640 {
641 CTimeStep Value = UnitsPerLineX * i - BaseValue;
642 if(Value.AsSeconds() >= 0)
643 {
644 char aValueBuffer[16];
645 Value.Format(pBuffer: aValueBuffer, BufferSize: sizeof(aValueBuffer));
646
647 Ui()->TextRender()->Text(x: EnvelopeToScreenX(View, x: Value.AsSeconds()) + 1.0f, y: View.y + View.h - 8.0f, Size: 8.0f, pText: aValueBuffer);
648 }
649 }
650 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
651 Ui()->ClipDisable();
652 }
653
654 // render lines
655 {
656 float EndX = View.x + View.w;
657 float StartX = std::clamp(val: View.x + View.w * m_OffsetEnvelopeX, lo: View.x, hi: View.x + View.w);
658
659 float EndTime = ScreenToEnvelopeX(View, x: EndX);
660 float StartTime = ScreenToEnvelopeX(View, x: StartX);
661
662 Ui()->ClipEnable(pRect: &View);
663 Graphics()->TextureClear();
664 IGraphics::CLineItemBatch LineItemBatch;
665 for(int c = 0; c < pEnvelope->GetChannels(); c++)
666 {
667 Graphics()->LinesBatchBegin(pBatch: &LineItemBatch);
668 if(s_ActiveChannels & (1 << c))
669 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1);
670 else
671 Graphics()->SetColor(r: aColors[c].r * 0.5f, g: aColors[c].g * 0.5f, b: aColors[c].b * 0.5f, a: 1);
672
673 const int Steps = static_cast<int>(((EndX - StartX) / Ui()->Screen()->w) * Graphics()->ScreenWidth());
674 const float StepTime = (EndTime - StartTime) / static_cast<float>(Steps);
675 const float StepSize = (EndX - StartX) / static_cast<float>(Steps);
676
677 ColorRGBA Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
678 pEnvelope->Eval(Time: StartTime, Result&: Channels, Channels: c + 1);
679 float PrevTime = StartTime;
680 float PrevX = StartX;
681 float PrevY = EnvelopeToScreenY(View, y: Channels[c]);
682 for(int Step = 1; Step <= Steps; Step++)
683 {
684 float CurrentTime = StartTime + Step * StepTime;
685 if(CurrentTime >= EndTime)
686 {
687 CurrentTime = EndTime - 0.001f;
688 if(CurrentTime <= PrevTime)
689 break;
690 }
691
692 Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
693 pEnvelope->Eval(Time: CurrentTime, Result&: Channels, Channels: c + 1);
694 const float CurrentX = StartX + Step * StepSize;
695 const float CurrentY = EnvelopeToScreenY(View, y: Channels[c]);
696
697 const IGraphics::CLineItem Item = IGraphics::CLineItem(PrevX, PrevY, CurrentX, CurrentY);
698 Graphics()->LinesBatchDraw(pBatch: &LineItemBatch, pArray: &Item, Num: 1);
699
700 PrevTime = CurrentTime;
701 PrevX = CurrentX;
702 PrevY = CurrentY;
703 }
704 Graphics()->LinesBatchEnd(pBatch: &LineItemBatch);
705 }
706 Ui()->ClipDisable();
707 }
708
709 CUIRect InactiveRegionLeft{
710 .x: View.x,
711 .y: View.y,
712 .w: std::max(a: 0.0f, b: EnvelopeToScreenX(View, x: 0.0f) - View.x),
713 .h: View.h,
714 };
715 CUIRect InactiveRegionRight{
716 .x: EnvelopeToScreenX(View, x: pEnvelope->EndTime()),
717 .y: View.y,
718 .w: std::max(a: 0.0f, b: View.x + View.w - EnvelopeToScreenX(View, x: pEnvelope->EndTime())),
719 .h: View.h,
720 };
721 InactiveRegionLeft.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
722 InactiveRegionRight.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
723
724 // render tangents for bezier curves
725 {
726 Ui()->ClipEnable(pRect: &View);
727 Graphics()->TextureClear();
728 Graphics()->LinesBegin();
729 for(int c = 0; c < pEnvelope->GetChannels(); c++)
730 {
731 if(!(s_ActiveChannels & (1 << c)))
732 continue;
733
734 for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++)
735 {
736 float PosX = EnvelopeToScreenX(View, x: pEnvelope->m_vPoints[i].m_Time.AsSeconds());
737 float PosY = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]));
738
739 // Out-Tangent
740 if(i < (int)pEnvelope->m_vPoints.size() - 1 && pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER)
741 {
742 float TangentX = EnvelopeToScreenX(View, x: (pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]).AsSeconds());
743 float TangentY = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]));
744
745 if(Map()->IsTangentOutPointSelected(Index: i, Channel: c))
746 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
747 else
748 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 0.4f);
749
750 IGraphics::CLineItem LineItem(TangentX, TangentY, PosX, PosY);
751 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
752 }
753
754 // In-Tangent
755 if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER)
756 {
757 float TangentX = EnvelopeToScreenX(View, x: (pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]).AsSeconds());
758 float TangentY = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]));
759
760 if(Map()->IsTangentInPointSelected(Index: i, Channel: c))
761 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
762 else
763 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 0.4f);
764
765 IGraphics::CLineItem LineItem(TangentX, TangentY, PosX, PosY);
766 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
767 }
768 }
769 }
770 Graphics()->LinesEnd();
771 Ui()->ClipDisable();
772 }
773
774 // render curve options
775 {
776 for(int i = 0; i < (int)pEnvelope->m_vPoints.size() - 1; i++)
777 {
778 float t0 = pEnvelope->m_vPoints[i].m_Time.AsSeconds();
779 float t1 = pEnvelope->m_vPoints[i + 1].m_Time.AsSeconds();
780
781 CUIRect CurveButton;
782 CurveButton.x = EnvelopeToScreenX(View, x: t0 + (t1 - t0) * 0.5f);
783 CurveButton.y = CurveBar.y;
784 CurveButton.h = CurveBar.h;
785 CurveButton.w = CurveBar.h;
786 CurveButton.x -= CurveButton.w / 2.0f;
787 const void *pId = &pEnvelope->m_vPoints[i].m_Curvetype;
788 if(CurveButton.x >= View.x)
789 {
790 const int ButtonResult = DoButton_Editor(pId, pText: CurveTypeNameShort(CurveType: pEnvelope->m_vPoints[i].m_Curvetype), Checked: 0, pRect: &CurveButton, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Switch curve type (N = step, L = linear, S = slow, F = fast, M = smooth, B = bezier).");
791 if(ButtonResult == 1)
792 {
793 const int PrevCurve = pEnvelope->m_vPoints[i].m_Curvetype;
794 const int Direction = Input()->ShiftIsPressed() ? -1 : 1;
795 pEnvelope->m_vPoints[i].m_Curvetype = (pEnvelope->m_vPoints[i].m_Curvetype + Direction + NUM_CURVETYPES) % NUM_CURVETYPES;
796
797 Map()->m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEditPoint>(args: Map(),
798 args&: Map()->m_SelectedEnvelope, args&: i, args: 0, args: CEditorActionEnvelopeEditPoint::EEditType::CURVE_TYPE, args: PrevCurve, args&: pEnvelope->m_vPoints[i].m_Curvetype));
799 Map()->OnModify();
800 }
801 else if(ButtonResult == 2)
802 {
803 m_PopupEnvelopeSelectedPoint = i;
804 static SPopupMenuId s_PopupCurvetypeId;
805 Ui()->DoPopupMenu(pId: &s_PopupCurvetypeId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 80, Height: (float)NUM_CURVETYPES * 14.0f + 10.0f, pContext: this, pfnFunc: PopupEnvelopeCurvetype);
806 }
807 }
808 }
809 }
810
811 // render colorbar
812 if(ShowColorBar)
813 {
814 RenderEnvelopeEditorColorBar(ColorBar, pEnvelope);
815 }
816
817 // render handles
818 if(CurrentEnvelopeSwitched)
819 {
820 Map()->DeselectEnvPoints();
821 m_ResetZoomEnvelope = true;
822 }
823
824 {
825 static SPopupMenuId s_PopupEnvPointId;
826 const auto &&ShowPopupEnvPoint = [&]() {
827 Ui()->DoPopupMenu(pId: &s_PopupEnvPointId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 150, Height: 56 + (pEnvelope->GetChannels() == 4 && !Map()->IsTangentSelected() ? 16.0f : 0.0f), pContext: this, pfnFunc: PopupEnvPoint);
828 };
829
830 if(s_Operation == EEnvelopeEditorOp::NONE)
831 {
832 UpdateHotEnvelopeObject(View, pEnvelope: pEnvelope.get(), ActiveChannels: s_ActiveChannels);
833 if(!Ui()->MouseButton(Index: 0))
834 Map()->m_EnvOpTracker.Stop(Switch: false);
835 }
836 else
837 {
838 Map()->m_EnvOpTracker.Begin(Operation: s_Operation);
839 }
840
841 Ui()->ClipEnable(pRect: &View);
842 Graphics()->TextureClear();
843 Graphics()->QuadsBegin();
844 for(int c = 0; c < pEnvelope->GetChannels(); c++)
845 {
846 if(!(s_ActiveChannels & (1 << c)))
847 continue;
848
849 for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++)
850 {
851 // point handle
852 {
853 CUIRect Final;
854 Final.x = EnvelopeToScreenX(View, x: pEnvelope->m_vPoints[i].m_Time.AsSeconds());
855 Final.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]));
856 Final.x -= 2.0f;
857 Final.y -= 2.0f;
858 Final.w = 4.0f;
859 Final.h = 4.0f;
860
861 const void *pId = &pEnvelope->m_vPoints[i].m_aValues[c];
862
863 if(Map()->IsEnvPointSelected(Index: i, Channel: c))
864 {
865 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
866 CUIRect Background = {
867 .x: Final.x - 0.2f * Final.w,
868 .y: Final.y - 0.2f * Final.h,
869 .w: Final.w * 1.4f,
870 .h: Final.h * 1.4f};
871 IGraphics::CQuadItem QuadItem(Background.x, Background.y, Background.w, Background.h);
872 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
873 }
874
875 if(Ui()->CheckActiveItem(pId))
876 {
877 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
878
879 if(s_Operation == EEnvelopeEditorOp::SELECT)
880 {
881 float dx = s_MouseXStart - Ui()->MouseX();
882 float dy = s_MouseYStart - Ui()->MouseY();
883
884 if(dx * dx + dy * dy > 20.0f)
885 {
886 s_Operation = EEnvelopeEditorOp::DRAG_POINT;
887
888 if(!Map()->IsEnvPointSelected(Index: i, Channel: c))
889 Map()->SelectEnvPoint(Index: i, Channel: c);
890 }
891 }
892
893 if(s_Operation == EEnvelopeEditorOp::DRAG_POINT || s_Operation == EEnvelopeEditorOp::DRAG_POINT_X || s_Operation == EEnvelopeEditorOp::DRAG_POINT_Y)
894 {
895 if(Input()->ShiftIsPressed())
896 {
897 if(s_Operation == EEnvelopeEditorOp::DRAG_POINT || s_Operation == EEnvelopeEditorOp::DRAG_POINT_Y)
898 {
899 s_Operation = EEnvelopeEditorOp::DRAG_POINT_X;
900 s_vAccurateDragValuesX.clear();
901 for(auto [SelectedIndex, _] : Map()->m_vSelectedEnvelopePoints)
902 s_vAccurateDragValuesX.push_back(x: pEnvelope->m_vPoints[SelectedIndex].m_Time.GetInternal());
903 }
904 else
905 {
906 float DeltaX = ScreenToEnvelopeDX(View, DeltaX: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f);
907
908 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
909 {
910 int SelectedIndex = Map()->m_vSelectedEnvelopePoints[k].first;
911 CFixedTime BoundLow = CFixedTime::FromSeconds(Seconds: ScreenToEnvelopeX(View, x: View.x));
912 CFixedTime BoundHigh = CFixedTime::FromSeconds(Seconds: ScreenToEnvelopeX(View, x: View.x + View.w));
913 for(int j = 0; j < SelectedIndex; j++)
914 {
915 if(!Map()->IsEnvPointSelected(Index: j))
916 BoundLow = std::max(a: pEnvelope->m_vPoints[j].m_Time + CFixedTime(1), b: BoundLow);
917 }
918 for(int j = SelectedIndex + 1; j < (int)pEnvelope->m_vPoints.size(); j++)
919 {
920 if(!Map()->IsEnvPointSelected(Index: j))
921 BoundHigh = std::min(a: pEnvelope->m_vPoints[j].m_Time - CFixedTime(1), b: BoundHigh);
922 }
923
924 DeltaX = ClampDelta(Val: s_vAccurateDragValuesX[k], Delta: DeltaX, Min: BoundLow.GetInternal(), Max: BoundHigh.GetInternal());
925 }
926 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
927 {
928 int SelectedIndex = Map()->m_vSelectedEnvelopePoints[k].first;
929 s_vAccurateDragValuesX[k] += DeltaX;
930 pEnvelope->m_vPoints[SelectedIndex].m_Time = CFixedTime(std::round(x: s_vAccurateDragValuesX[k]));
931 }
932 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
933 {
934 int SelectedIndex = Map()->m_vSelectedEnvelopePoints[k].first;
935 if(SelectedIndex == 0 && pEnvelope->m_vPoints[SelectedIndex].m_Time != CFixedTime(0))
936 {
937 RemoveTimeOffsetEnvelope(pEnvelope);
938 float Offset = s_vAccurateDragValuesX[k];
939 for(auto &Value : s_vAccurateDragValuesX)
940 Value -= Offset;
941 break;
942 }
943 }
944 }
945 }
946 else
947 {
948 if(s_Operation == EEnvelopeEditorOp::DRAG_POINT || s_Operation == EEnvelopeEditorOp::DRAG_POINT_X)
949 {
950 s_Operation = EEnvelopeEditorOp::DRAG_POINT_Y;
951 s_vAccurateDragValuesY.clear();
952 for(auto [SelectedIndex, SelectedChannel] : Map()->m_vSelectedEnvelopePoints)
953 s_vAccurateDragValuesY.push_back(x: pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]);
954 }
955 else
956 {
957 float DeltaY = ScreenToEnvelopeDY(View, DeltaY: Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f);
958 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
959 {
960 auto [SelectedIndex, SelectedChannel] = Map()->m_vSelectedEnvelopePoints[k];
961 s_vAccurateDragValuesY[k] -= DeltaY;
962 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: s_vAccurateDragValuesY[k]);
963
964 if(pEnvelope->GetChannels() == 1 || pEnvelope->GetChannels() == 4)
965 {
966 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::clamp(val: pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel], lo: 0, hi: 1024);
967 s_vAccurateDragValuesY[k] = std::clamp<float>(val: s_vAccurateDragValuesY[k], lo: 0, hi: 1024);
968 }
969 }
970 }
971 }
972 }
973
974 if(s_Operation == EEnvelopeEditorOp::CONTEXT_MENU)
975 {
976 if(!Ui()->MouseButton(Index: 1))
977 {
978 if(Map()->m_vSelectedEnvelopePoints.size() == 1)
979 {
980 Map()->m_UpdateEnvPointInfo = true;
981 ShowPopupEnvPoint();
982 }
983 else if(Map()->m_vSelectedEnvelopePoints.size() > 1)
984 {
985 static SPopupMenuId s_PopupEnvPointMultiId;
986 Ui()->DoPopupMenu(pId: &s_PopupEnvPointMultiId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 100, Height: 22, pContext: this, pfnFunc: PopupEnvPointMulti);
987 }
988 Ui()->SetActiveItem(nullptr);
989 s_Operation = EEnvelopeEditorOp::NONE;
990 }
991 }
992 else if(!Ui()->MouseButton(Index: 0))
993 {
994 Ui()->SetActiveItem(nullptr);
995 Map()->m_SelectedQuadEnvelope = -1;
996
997 if(s_Operation == EEnvelopeEditorOp::SELECT)
998 {
999 if(Input()->ShiftIsPressed())
1000 Map()->ToggleEnvPoint(Index: i, Channel: c);
1001 else
1002 Map()->SelectEnvPoint(Index: i, Channel: c);
1003 }
1004
1005 s_Operation = EEnvelopeEditorOp::NONE;
1006 Map()->OnModify();
1007 }
1008
1009 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1010 }
1011 else if(Ui()->HotItem() == pId)
1012 {
1013 if(Ui()->MouseButton(Index: 0))
1014 {
1015 Ui()->SetActiveItem(pId);
1016 s_Operation = EEnvelopeEditorOp::SELECT;
1017 Map()->m_SelectedQuadEnvelope = Map()->m_SelectedEnvelope;
1018
1019 s_MouseXStart = Ui()->MouseX();
1020 s_MouseYStart = Ui()->MouseY();
1021 }
1022 else if(Ui()->MouseButtonClicked(Index: 1))
1023 {
1024 if(Input()->ShiftIsPressed())
1025 {
1026 Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionDeleteEnvelopePoint>(args: Map(), args&: Map()->m_SelectedEnvelope, args&: i));
1027 }
1028 else
1029 {
1030 s_Operation = EEnvelopeEditorOp::CONTEXT_MENU;
1031 if(!Map()->IsEnvPointSelected(Index: i, Channel: c))
1032 Map()->SelectEnvPoint(Index: i, Channel: c);
1033 Ui()->SetActiveItem(pId);
1034 }
1035 }
1036
1037 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
1038 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1039 str_copy(dst&: m_aTooltip, src: "Envelope point. Left mouse to drag. Hold ctrl to be more precise. Hold shift to alter time. Shift+right click to delete.");
1040 m_pUiGotContext = pId;
1041 }
1042 else
1043 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1.0f);
1044
1045 IGraphics::CQuadItem QuadItem(Final.x, Final.y, Final.w, Final.h);
1046 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
1047 }
1048
1049 // tangent handles for bezier curves
1050 if(i >= 0 && i < (int)pEnvelope->m_vPoints.size())
1051 {
1052 // Out-Tangent handle
1053 if(i < (int)pEnvelope->m_vPoints.size() - 1 && pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER)
1054 {
1055 CUIRect Final;
1056 Final.x = EnvelopeToScreenX(View, x: (pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]).AsSeconds());
1057 Final.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]));
1058 Final.x -= 2.0f;
1059 Final.y -= 2.0f;
1060 Final.w = 4.0f;
1061 Final.h = 4.0f;
1062
1063 // handle logic
1064 const void *pId = &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c];
1065
1066 if(Map()->IsTangentOutPointSelected(Index: i, Channel: c))
1067 {
1068 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1069 IGraphics::CFreeformItem FreeformItem(
1070 Final.x + Final.w / 2.0f,
1071 Final.y - 1,
1072 Final.x + Final.w / 2.0f,
1073 Final.y - 1,
1074 Final.x + Final.w + 1,
1075 Final.y + Final.h + 1,
1076 Final.x - 1,
1077 Final.y + Final.h + 1);
1078 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
1079 }
1080
1081 if(Ui()->CheckActiveItem(pId))
1082 {
1083 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
1084
1085 if(s_Operation == EEnvelopeEditorOp::SELECT)
1086 {
1087 float dx = s_MouseXStart - Ui()->MouseX();
1088 float dy = s_MouseYStart - Ui()->MouseY();
1089
1090 if(dx * dx + dy * dy > 20.0f)
1091 {
1092 s_Operation = EEnvelopeEditorOp::DRAG_POINT;
1093
1094 s_vAccurateDragValuesX = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c].GetInternal())};
1095 s_vAccurateDragValuesY = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c])};
1096
1097 if(!Map()->IsTangentOutPointSelected(Index: i, Channel: c))
1098 Map()->SelectTangentOutPoint(Index: i, Channel: c);
1099 }
1100 }
1101
1102 if(s_Operation == EEnvelopeEditorOp::DRAG_POINT)
1103 {
1104 float DeltaX = ScreenToEnvelopeDX(View, DeltaX: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f);
1105 float DeltaY = ScreenToEnvelopeDY(View, DeltaY: Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f);
1106 s_vAccurateDragValuesX[0] += DeltaX;
1107 s_vAccurateDragValuesY[0] -= DeltaY;
1108
1109 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = CFixedTime(std::round(x: s_vAccurateDragValuesX[0]));
1110 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c] = std::round(x: s_vAccurateDragValuesY[0]);
1111
1112 // clamp time value
1113 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = std::clamp(val: pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c], lo: CFixedTime(0), hi: CFixedTime::FromSeconds(Seconds: ScreenToEnvelopeX(View, x: View.x + View.w)) - pEnvelope->m_vPoints[i].m_Time);
1114 s_vAccurateDragValuesX[0] = std::clamp<float>(val: s_vAccurateDragValuesX[0], lo: 0, hi: (CFixedTime::FromSeconds(Seconds: ScreenToEnvelopeX(View, x: View.x + View.w)) - pEnvelope->m_vPoints[i].m_Time).GetInternal());
1115 }
1116
1117 if(s_Operation == EEnvelopeEditorOp::CONTEXT_MENU)
1118 {
1119 if(!Ui()->MouseButton(Index: 1))
1120 {
1121 if(Map()->IsTangentOutPointSelected(Index: i, Channel: c))
1122 {
1123 Map()->m_UpdateEnvPointInfo = true;
1124 ShowPopupEnvPoint();
1125 }
1126 Ui()->SetActiveItem(nullptr);
1127 s_Operation = EEnvelopeEditorOp::NONE;
1128 }
1129 }
1130 else if(!Ui()->MouseButton(Index: 0))
1131 {
1132 Ui()->SetActiveItem(nullptr);
1133 Map()->m_SelectedQuadEnvelope = -1;
1134
1135 if(s_Operation == EEnvelopeEditorOp::SELECT)
1136 Map()->SelectTangentOutPoint(Index: i, Channel: c);
1137
1138 s_Operation = EEnvelopeEditorOp::NONE;
1139 Map()->OnModify();
1140 }
1141
1142 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1143 }
1144 else if(Ui()->HotItem() == pId)
1145 {
1146 if(Ui()->MouseButton(Index: 0))
1147 {
1148 Ui()->SetActiveItem(pId);
1149 s_Operation = EEnvelopeEditorOp::SELECT;
1150 Map()->m_SelectedQuadEnvelope = Map()->m_SelectedEnvelope;
1151
1152 s_MouseXStart = Ui()->MouseX();
1153 s_MouseYStart = Ui()->MouseY();
1154 }
1155 else if(Ui()->MouseButtonClicked(Index: 1))
1156 {
1157 if(Input()->ShiftIsPressed())
1158 {
1159 Map()->SelectTangentOutPoint(Index: i, Channel: c);
1160 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = CFixedTime(0);
1161 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c] = 0.0f;
1162 Map()->OnModify();
1163 }
1164 else
1165 {
1166 s_Operation = EEnvelopeEditorOp::CONTEXT_MENU;
1167 Map()->SelectTangentOutPoint(Index: i, Channel: c);
1168 Ui()->SetActiveItem(pId);
1169 }
1170 }
1171
1172 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
1173 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1174 str_copy(dst&: m_aTooltip, src: "Bezier out-tangent. Left mouse to drag. Hold ctrl to be more precise. Shift+right click to reset.");
1175 m_pUiGotContext = pId;
1176 }
1177 else
1178 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1.0f);
1179
1180 // draw triangle
1181 IGraphics::CFreeformItem FreeformItem(Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w, Final.y + Final.h, Final.x, Final.y + Final.h);
1182 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
1183 }
1184
1185 // In-Tangent handle
1186 if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER)
1187 {
1188 CUIRect Final;
1189 Final.x = EnvelopeToScreenX(View, x: (pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]).AsSeconds());
1190 Final.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]));
1191 Final.x -= 2.0f;
1192 Final.y -= 2.0f;
1193 Final.w = 4.0f;
1194 Final.h = 4.0f;
1195
1196 // handle logic
1197 const void *pId = &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c];
1198
1199 if(Map()->IsTangentInPointSelected(Index: i, Channel: c))
1200 {
1201 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1202 IGraphics::CFreeformItem FreeformItem(
1203 Final.x + Final.w / 2.0f,
1204 Final.y - 1,
1205 Final.x + Final.w / 2.0f,
1206 Final.y - 1,
1207 Final.x + Final.w + 1,
1208 Final.y + Final.h + 1,
1209 Final.x - 1,
1210 Final.y + Final.h + 1);
1211 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
1212 }
1213
1214 if(Ui()->CheckActiveItem(pId))
1215 {
1216 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
1217
1218 if(s_Operation == EEnvelopeEditorOp::SELECT)
1219 {
1220 float dx = s_MouseXStart - Ui()->MouseX();
1221 float dy = s_MouseYStart - Ui()->MouseY();
1222
1223 if(dx * dx + dy * dy > 20.0f)
1224 {
1225 s_Operation = EEnvelopeEditorOp::DRAG_POINT;
1226
1227 s_vAccurateDragValuesX = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c].GetInternal())};
1228 s_vAccurateDragValuesY = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c])};
1229
1230 if(!Map()->IsTangentInPointSelected(Index: i, Channel: c))
1231 Map()->SelectTangentInPoint(Index: i, Channel: c);
1232 }
1233 }
1234
1235 if(s_Operation == EEnvelopeEditorOp::DRAG_POINT)
1236 {
1237 float DeltaX = ScreenToEnvelopeDX(View, DeltaX: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f);
1238 float DeltaY = ScreenToEnvelopeDY(View, DeltaY: Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f);
1239 s_vAccurateDragValuesX[0] += DeltaX;
1240 s_vAccurateDragValuesY[0] -= DeltaY;
1241
1242 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = CFixedTime(std::round(x: s_vAccurateDragValuesX[0]));
1243 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c] = std::round(x: s_vAccurateDragValuesY[0]);
1244
1245 // clamp time value
1246 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = std::clamp(val: pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c], lo: CFixedTime::FromSeconds(Seconds: ScreenToEnvelopeX(View, x: View.x)) - pEnvelope->m_vPoints[i].m_Time, hi: CFixedTime(0));
1247 s_vAccurateDragValuesX[0] = std::clamp<float>(val: s_vAccurateDragValuesX[0], lo: (CFixedTime::FromSeconds(Seconds: ScreenToEnvelopeX(View, x: View.x)) - pEnvelope->m_vPoints[i].m_Time).GetInternal(), hi: 0);
1248 }
1249
1250 if(s_Operation == EEnvelopeEditorOp::CONTEXT_MENU)
1251 {
1252 if(!Ui()->MouseButton(Index: 1))
1253 {
1254 if(Map()->IsTangentInPointSelected(Index: i, Channel: c))
1255 {
1256 Map()->m_UpdateEnvPointInfo = true;
1257 ShowPopupEnvPoint();
1258 }
1259 Ui()->SetActiveItem(nullptr);
1260 s_Operation = EEnvelopeEditorOp::NONE;
1261 }
1262 }
1263 else if(!Ui()->MouseButton(Index: 0))
1264 {
1265 Ui()->SetActiveItem(nullptr);
1266 Map()->m_SelectedQuadEnvelope = -1;
1267
1268 if(s_Operation == EEnvelopeEditorOp::SELECT)
1269 Map()->SelectTangentInPoint(Index: i, Channel: c);
1270
1271 s_Operation = EEnvelopeEditorOp::NONE;
1272 Map()->OnModify();
1273 }
1274
1275 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1276 }
1277 else if(Ui()->HotItem() == pId)
1278 {
1279 if(Ui()->MouseButton(Index: 0))
1280 {
1281 Ui()->SetActiveItem(pId);
1282 s_Operation = EEnvelopeEditorOp::SELECT;
1283 Map()->m_SelectedQuadEnvelope = Map()->m_SelectedEnvelope;
1284
1285 s_MouseXStart = Ui()->MouseX();
1286 s_MouseYStart = Ui()->MouseY();
1287 }
1288 else if(Ui()->MouseButtonClicked(Index: 1))
1289 {
1290 if(Input()->ShiftIsPressed())
1291 {
1292 Map()->SelectTangentInPoint(Index: i, Channel: c);
1293 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = CFixedTime(0);
1294 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c] = 0.0f;
1295 Map()->OnModify();
1296 }
1297 else
1298 {
1299 s_Operation = EEnvelopeEditorOp::CONTEXT_MENU;
1300 Map()->SelectTangentInPoint(Index: i, Channel: c);
1301 Ui()->SetActiveItem(pId);
1302 }
1303 }
1304
1305 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
1306 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1307 str_copy(dst&: m_aTooltip, src: "Bezier in-tangent. Left mouse to drag. Hold ctrl to be more precise. Shift+right click to reset.");
1308 m_pUiGotContext = pId;
1309 }
1310 else
1311 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1.0f);
1312
1313 // draw triangle
1314 IGraphics::CFreeformItem FreeformItem(Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w, Final.y + Final.h, Final.x, Final.y + Final.h);
1315 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
1316 }
1317 }
1318 }
1319 }
1320 Graphics()->QuadsEnd();
1321 Ui()->ClipDisable();
1322 }
1323
1324 // handle scaling
1325 static float s_ScaleFactorX = 1.0f;
1326 static float s_ScaleFactorY = 1.0f;
1327 static float s_MidpointX = 0.0f;
1328 static float s_MidpointY = 0.0f;
1329 static std::vector<float> s_vInitialPositionsX;
1330 static std::vector<float> s_vInitialPositionsY;
1331 if(s_Operation == EEnvelopeEditorOp::NONE && !s_NameInput.IsActive() && Input()->KeyIsPressed(Key: KEY_S) && !Input()->ModifierIsPressed() && !Map()->m_vSelectedEnvelopePoints.empty())
1332 {
1333 s_Operation = EEnvelopeEditorOp::SCALE;
1334 s_ScaleFactorX = 1.0f;
1335 s_ScaleFactorY = 1.0f;
1336 auto [FirstPointIndex, FirstPointChannel] = Map()->m_vSelectedEnvelopePoints.front();
1337
1338 float MaximumX = pEnvelope->m_vPoints[FirstPointIndex].m_Time.GetInternal();
1339 float MinimumX = MaximumX;
1340 s_vInitialPositionsX.clear();
1341 for(auto [SelectedIndex, _] : Map()->m_vSelectedEnvelopePoints)
1342 {
1343 float Value = pEnvelope->m_vPoints[SelectedIndex].m_Time.GetInternal();
1344 s_vInitialPositionsX.push_back(x: Value);
1345 MaximumX = std::max(a: MaximumX, b: Value);
1346 MinimumX = std::min(a: MinimumX, b: Value);
1347 }
1348 s_MidpointX = (MaximumX - MinimumX) / 2.0f + MinimumX;
1349
1350 float MaximumY = pEnvelope->m_vPoints[FirstPointIndex].m_aValues[FirstPointChannel];
1351 float MinimumY = MaximumY;
1352 s_vInitialPositionsY.clear();
1353 for(auto [SelectedIndex, SelectedChannel] : Map()->m_vSelectedEnvelopePoints)
1354 {
1355 float Value = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel];
1356 s_vInitialPositionsY.push_back(x: Value);
1357 MaximumY = std::max(a: MaximumY, b: Value);
1358 MinimumY = std::min(a: MinimumY, b: Value);
1359 }
1360 s_MidpointY = (MaximumY - MinimumY) / 2.0f + MinimumY;
1361 }
1362
1363 if(s_Operation == EEnvelopeEditorOp::SCALE)
1364 {
1365 str_copy(dst&: m_aTooltip, src: "Press shift to scale the time. Press alt to scale along midpoint. Press ctrl to be more precise.");
1366
1367 if(Input()->ShiftIsPressed())
1368 {
1369 s_ScaleFactorX += Ui()->MouseDeltaX() / Graphics()->ScreenWidth() * (Input()->ModifierIsPressed() ? 0.5f : 10.0f);
1370 float Midpoint = Input()->AltIsPressed() ? s_MidpointX : 0.0f;
1371 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
1372 {
1373 int SelectedIndex = Map()->m_vSelectedEnvelopePoints[k].first;
1374 CFixedTime BoundLow = CFixedTime::FromSeconds(Seconds: ScreenToEnvelopeX(View, x: View.x));
1375 CFixedTime BoundHigh = CFixedTime::FromSeconds(Seconds: ScreenToEnvelopeX(View, x: View.x + View.w));
1376 for(int j = 0; j < SelectedIndex; j++)
1377 {
1378 if(!Map()->IsEnvPointSelected(Index: j))
1379 BoundLow = std::max(a: pEnvelope->m_vPoints[j].m_Time + CFixedTime(1), b: BoundLow);
1380 }
1381 for(int j = SelectedIndex + 1; j < (int)pEnvelope->m_vPoints.size(); j++)
1382 {
1383 if(!Map()->IsEnvPointSelected(Index: j))
1384 BoundHigh = std::min(a: pEnvelope->m_vPoints[j].m_Time - CFixedTime(1), b: BoundHigh);
1385 }
1386
1387 float Value = s_vInitialPositionsX[k];
1388 float ScaleBoundLow = (BoundLow.GetInternal() - Midpoint) / (Value - Midpoint);
1389 float ScaleBoundHigh = (BoundHigh.GetInternal() - Midpoint) / (Value - Midpoint);
1390 float ScaleBoundMin = std::min(a: ScaleBoundLow, b: ScaleBoundHigh);
1391 float ScaleBoundMax = std::max(a: ScaleBoundLow, b: ScaleBoundHigh);
1392 s_ScaleFactorX = std::clamp(val: s_ScaleFactorX, lo: ScaleBoundMin, hi: ScaleBoundMax);
1393 }
1394
1395 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
1396 {
1397 int SelectedIndex = Map()->m_vSelectedEnvelopePoints[k].first;
1398 float ScaleMinimum = s_vInitialPositionsX[k] - Midpoint > CFixedTime(1).AsSeconds() ? CFixedTime(1).AsSeconds() / (s_vInitialPositionsX[k] - Midpoint) : 0.0f;
1399 float ScaleFactor = std::max(a: ScaleMinimum, b: s_ScaleFactorX);
1400 pEnvelope->m_vPoints[SelectedIndex].m_Time = CFixedTime(std::round(x: (s_vInitialPositionsX[k] - Midpoint) * ScaleFactor + Midpoint));
1401 }
1402 for(size_t k = 1; k < pEnvelope->m_vPoints.size(); k++)
1403 {
1404 if(pEnvelope->m_vPoints[k].m_Time <= pEnvelope->m_vPoints[k - 1].m_Time)
1405 pEnvelope->m_vPoints[k].m_Time = pEnvelope->m_vPoints[k - 1].m_Time + CFixedTime(1);
1406 }
1407 for(auto [SelectedIndex, _] : Map()->m_vSelectedEnvelopePoints)
1408 {
1409 if(SelectedIndex == 0 && pEnvelope->m_vPoints[SelectedIndex].m_Time != CFixedTime(0))
1410 {
1411 float Offset = pEnvelope->m_vPoints[0].m_Time.GetInternal();
1412 RemoveTimeOffsetEnvelope(pEnvelope);
1413 s_MidpointX -= Offset;
1414 for(auto &Value : s_vInitialPositionsX)
1415 Value -= Offset;
1416 break;
1417 }
1418 }
1419 }
1420 else
1421 {
1422 s_ScaleFactorY -= Ui()->MouseDeltaY() / Graphics()->ScreenHeight() * (Input()->ModifierIsPressed() ? 0.5f : 10.0f);
1423 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
1424 {
1425 auto [SelectedIndex, SelectedChannel] = Map()->m_vSelectedEnvelopePoints[k];
1426 if(Input()->AltIsPressed())
1427 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: (s_vInitialPositionsY[k] - s_MidpointY) * s_ScaleFactorY + s_MidpointY);
1428 else
1429 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: s_vInitialPositionsY[k] * s_ScaleFactorY);
1430
1431 if(pEnvelope->GetChannels() == 1 || pEnvelope->GetChannels() == 4)
1432 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::clamp(val: pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel], lo: 0, hi: 1024);
1433 }
1434 }
1435
1436 if(Ui()->MouseButton(Index: 0))
1437 {
1438 s_Operation = EEnvelopeEditorOp::NONE;
1439 Map()->m_EnvOpTracker.Stop(Switch: false);
1440 }
1441 else if(Ui()->MouseButton(Index: 1) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1442 {
1443 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
1444 {
1445 int SelectedIndex = Map()->m_vSelectedEnvelopePoints[k].first;
1446 pEnvelope->m_vPoints[SelectedIndex].m_Time = CFixedTime(std::round(x: s_vInitialPositionsX[k]));
1447 }
1448 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
1449 {
1450 auto [SelectedIndex, SelectedChannel] = Map()->m_vSelectedEnvelopePoints[k];
1451 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: s_vInitialPositionsY[k]);
1452 }
1453 RemoveTimeOffsetEnvelope(pEnvelope);
1454 s_Operation = EEnvelopeEditorOp::NONE;
1455 }
1456 }
1457
1458 // handle box selection
1459 if(s_Operation == EEnvelopeEditorOp::BOX_SELECT)
1460 {
1461 Ui()->ClipEnable(pRect: &View);
1462 CUIRect SelectionRect;
1463 SelectionRect.x = s_MouseXStart;
1464 SelectionRect.y = s_MouseYStart;
1465 SelectionRect.w = Ui()->MouseX() - s_MouseXStart;
1466 SelectionRect.h = Ui()->MouseY() - s_MouseYStart;
1467 SelectionRect.DrawOutline(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
1468 Ui()->ClipDisable();
1469
1470 if(!Ui()->MouseButton(Index: 0))
1471 {
1472 s_Operation = EEnvelopeEditorOp::NONE;
1473 Ui()->SetActiveItem(nullptr);
1474
1475 float TimeStart = ScreenToEnvelopeX(View, x: s_MouseXStart);
1476 float TimeEnd = ScreenToEnvelopeX(View, x: Ui()->MouseX());
1477 float ValueStart = ScreenToEnvelopeY(View, y: s_MouseYStart);
1478 float ValueEnd = ScreenToEnvelopeY(View, y: Ui()->MouseY());
1479
1480 float TimeMin = std::min(a: TimeStart, b: TimeEnd);
1481 float TimeMax = std::max(a: TimeStart, b: TimeEnd);
1482 float ValueMin = std::min(a: ValueStart, b: ValueEnd);
1483 float ValueMax = std::max(a: ValueStart, b: ValueEnd);
1484
1485 if(!Input()->ShiftIsPressed())
1486 Map()->DeselectEnvPoints();
1487
1488 for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++)
1489 {
1490 for(int c = 0; c < CEnvPoint::MAX_CHANNELS; c++)
1491 {
1492 if(!(s_ActiveChannels & (1 << c)))
1493 continue;
1494
1495 float Time = pEnvelope->m_vPoints[i].m_Time.AsSeconds();
1496 float Value = fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]);
1497
1498 if(in_range(a: Time, lower: TimeMin, upper: TimeMax) && in_range(a: Value, lower: ValueMin, upper: ValueMax))
1499 Map()->ToggleEnvPoint(Index: i, Channel: c);
1500 }
1501 }
1502 }
1503 }
1504 }
1505}
1506
1507void CEditor::RenderEnvelopeEditorColorBar(CUIRect ColorBar, const std::shared_ptr<CEnvelope> &pEnvelope)
1508{
1509 if(pEnvelope->m_vPoints.size() < 2)
1510 {
1511 return;
1512 }
1513 const float ViewStartTime = ScreenToEnvelopeX(View: ColorBar, x: ColorBar.x);
1514 const float ViewEndTime = ScreenToEnvelopeX(View: ColorBar, x: ColorBar.x + ColorBar.w);
1515 if(ViewEndTime < 0.0f || ViewStartTime > pEnvelope->EndTime())
1516 {
1517 return;
1518 }
1519 const float StartX = std::max(a: EnvelopeToScreenX(View: ColorBar, x: 0.0f), b: ColorBar.x);
1520 const float TotalWidth = std::min(a: EnvelopeToScreenX(View: ColorBar, x: pEnvelope->EndTime()) - StartX, b: ColorBar.x + ColorBar.w - StartX);
1521
1522 Ui()->ClipEnable(pRect: &ColorBar);
1523 CUIRect ColorBarBackground = CUIRect{.x: StartX, .y: ColorBar.y, .w: TotalWidth, .h: ColorBar.h};
1524 RenderBackground(View: ColorBarBackground, Texture: m_CheckerTexture, Size: ColorBarBackground.h, Brightness: 1.0f);
1525 Graphics()->TextureClear();
1526 Graphics()->QuadsBegin();
1527
1528 int PointBeginIndex = pEnvelope->FindPointIndex(Time: CFixedTime::FromSeconds(Seconds: ViewStartTime));
1529 if(PointBeginIndex == -1)
1530 {
1531 PointBeginIndex = 0;
1532 }
1533 int PointEndIndex = pEnvelope->FindPointIndex(Time: CFixedTime::FromSeconds(Seconds: ViewEndTime));
1534 if(PointEndIndex == -1)
1535 {
1536 PointEndIndex = (int)pEnvelope->m_vPoints.size() - 2;
1537 }
1538 for(int PointIndex = PointBeginIndex; PointIndex <= PointEndIndex; PointIndex++)
1539 {
1540 const auto &PointStart = pEnvelope->m_vPoints[PointIndex];
1541 const auto &PointEnd = pEnvelope->m_vPoints[PointIndex + 1];
1542 const float PointStartTime = PointStart.m_Time.AsSeconds();
1543 const float PointEndTime = PointEnd.m_Time.AsSeconds();
1544
1545 int Steps;
1546 if(PointStart.m_Curvetype == CURVETYPE_LINEAR || PointStart.m_Curvetype == CURVETYPE_STEP)
1547 {
1548 Steps = 1; // let the GPU do the work
1549 }
1550 else
1551 {
1552 const float ClampedPointStartX = std::max(a: EnvelopeToScreenX(View: ColorBar, x: PointStartTime), b: ColorBar.x);
1553 const float ClampedPointEndX = std::min(a: EnvelopeToScreenX(View: ColorBar, x: PointEndTime), b: ColorBar.x + ColorBar.w);
1554 Steps = std::clamp(val: (int)std::sqrt(x: 5.0f * (ClampedPointEndX - ClampedPointStartX)), lo: 1, hi: 250);
1555 }
1556 const float OverallSectionStartTime = Steps == 1 ? PointStartTime : std::max(a: PointStartTime, b: ViewStartTime);
1557 const float OverallSectionEndTime = Steps == 1 ? PointEndTime : std::min(a: PointEndTime, b: ViewEndTime);
1558 float SectionStartTime = OverallSectionStartTime;
1559 float SectionStartX = EnvelopeToScreenX(View: ColorBar, x: SectionStartTime);
1560 for(int Step = 1; Step <= Steps; Step++)
1561 {
1562 const float SectionEndTime = OverallSectionStartTime + (OverallSectionEndTime - OverallSectionStartTime) * (Step / (float)Steps);
1563 const float SectionEndX = EnvelopeToScreenX(View: ColorBar, x: SectionEndTime);
1564
1565 ColorRGBA StartColor;
1566 if(Step == 1 && OverallSectionStartTime == PointStartTime)
1567 {
1568 StartColor = PointStart.ColorValue();
1569 }
1570 else
1571 {
1572 StartColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
1573 pEnvelope->Eval(Time: SectionStartTime, Result&: StartColor, Channels: 4);
1574 }
1575
1576 ColorRGBA EndColor;
1577 if(PointStart.m_Curvetype == CURVETYPE_STEP)
1578 {
1579 EndColor = StartColor;
1580 }
1581 else if(Step == Steps && OverallSectionEndTime == PointEndTime)
1582 {
1583 EndColor = PointEnd.ColorValue();
1584 }
1585 else
1586 {
1587 EndColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
1588 pEnvelope->Eval(Time: SectionEndTime, Result&: EndColor, Channels: 4);
1589 }
1590
1591 Graphics()->SetColor4(TopLeft: StartColor, TopRight: EndColor, BottomLeft: StartColor, BottomRight: EndColor);
1592 const IGraphics::CQuadItem QuadItem(SectionStartX, ColorBar.y, SectionEndX - SectionStartX, ColorBar.h);
1593 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
1594
1595 SectionStartTime = SectionEndTime;
1596 SectionStartX = SectionEndX;
1597 }
1598 }
1599 Graphics()->QuadsEnd();
1600 Ui()->ClipDisable();
1601 ColorBarBackground.h -= Ui()->Screen()->h / Graphics()->ScreenHeight(); // hack to fix alignment of bottom border
1602 ColorBarBackground.DrawOutline(Color: ColorRGBA(0.7f, 0.7f, 0.7f, 1.0f));
1603}
1604
1605void CEditor::UpdateHotEnvelopeObject(const CUIRect &View, const CEnvelope *pEnvelope, int ActiveChannels)
1606{
1607 if(!Ui()->MouseInside(pRect: &View))
1608 return;
1609
1610 const vec2 MousePos = Ui()->MousePos();
1611
1612 float MinDist = 200.0f;
1613 const void *pMinPointId = nullptr;
1614
1615 const auto UpdateMinimum = [&](vec2 Position, const void *pId) {
1616 const float CurrDist = length_squared(a: Position - MousePos);
1617 if(CurrDist < MinDist)
1618 {
1619 MinDist = CurrDist;
1620 pMinPointId = pId;
1621 }
1622 };
1623
1624 for(size_t i = 0; i < pEnvelope->m_vPoints.size(); i++)
1625 {
1626 for(int c = pEnvelope->GetChannels() - 1; c >= 0; c--)
1627 {
1628 if(!(ActiveChannels & (1 << c)))
1629 continue;
1630
1631 if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER)
1632 {
1633 vec2 Position;
1634 Position.x = EnvelopeToScreenX(View, x: (pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]).AsSeconds());
1635 Position.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]));
1636 UpdateMinimum(Position, &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]);
1637 }
1638
1639 if(i < pEnvelope->m_vPoints.size() - 1 && pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER)
1640 {
1641 vec2 Position;
1642 Position.x = EnvelopeToScreenX(View, x: (pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]).AsSeconds());
1643 Position.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]));
1644 UpdateMinimum(Position, &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]);
1645 }
1646
1647 vec2 Position;
1648 Position.x = EnvelopeToScreenX(View, x: pEnvelope->m_vPoints[i].m_Time.AsSeconds());
1649 Position.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]));
1650 UpdateMinimum(Position, &pEnvelope->m_vPoints[i].m_aValues[c]);
1651 }
1652 }
1653
1654 if(pMinPointId != nullptr)
1655 {
1656 Ui()->SetHotItem(pMinPointId);
1657 }
1658 else if(!Map()->m_EnvelopeEvaluator.m_Animate)
1659 {
1660 float Time = Map()->m_EnvelopeEvaluator.m_AnimateTime * Map()->m_EnvelopeEvaluator.m_AnimateSpeed;
1661 float LoopedTime = std::fmod(x: Time, y: pEnvelope->EndTime());
1662 if(absolute(a: EnvelopeToScreenX(View, x: Time) - MousePos.x) < 20.0f || absolute(a: EnvelopeToScreenX(View, x: LoopedTime) - MousePos.x) < 20.0f)
1663 {
1664 Ui()->SetHotItem(&Map()->m_EnvelopeEvaluator.m_AnimateTime);
1665 }
1666 }
1667}
1668
1669void CEditor::ZoomAdaptOffsetX(float ZoomFactor, const CUIRect &View)
1670{
1671 float PosX = g_Config.m_EdZoomTarget ? (Ui()->MouseX() - View.x) / View.w : 0.5f;
1672 m_OffsetEnvelopeX = PosX - (PosX - m_OffsetEnvelopeX) * ZoomFactor;
1673}
1674
1675void CEditor::UpdateZoomEnvelopeX(const CUIRect &View)
1676{
1677 float OldZoom = m_ZoomEnvelopeX.GetValue();
1678 if(m_ZoomEnvelopeX.UpdateValue())
1679 ZoomAdaptOffsetX(ZoomFactor: OldZoom / m_ZoomEnvelopeX.GetValue(), View);
1680}
1681
1682void CEditor::ZoomAdaptOffsetY(float ZoomFactor, const CUIRect &View)
1683{
1684 float PosY = g_Config.m_EdZoomTarget ? 1.0f - (Ui()->MouseY() - View.y) / View.h : 0.5f;
1685 m_OffsetEnvelopeY = PosY - (PosY - m_OffsetEnvelopeY) * ZoomFactor;
1686}
1687
1688void CEditor::UpdateZoomEnvelopeY(const CUIRect &View)
1689{
1690 float OldZoom = m_ZoomEnvelopeY.GetValue();
1691 if(m_ZoomEnvelopeY.UpdateValue())
1692 ZoomAdaptOffsetY(ZoomFactor: OldZoom / m_ZoomEnvelopeY.GetValue(), View);
1693}
1694
1695void CEditor::ResetZoomEnvelope(const std::shared_ptr<CEnvelope> &pEnvelope, int ActiveChannels)
1696{
1697 auto [Bottom, Top] = pEnvelope->GetValueRange(ChannelMask: ActiveChannels);
1698 float EndTime = pEnvelope->EndTime();
1699 float ValueRange = absolute(a: Top - Bottom);
1700
1701 if(ValueRange < m_ZoomEnvelopeY.GetMinValue())
1702 {
1703 // Set view to some sane default if range is too small
1704 m_OffsetEnvelopeY = 0.5f - ValueRange / m_ZoomEnvelopeY.GetMinValue() / 2.0f - Bottom / m_ZoomEnvelopeY.GetMinValue();
1705 m_ZoomEnvelopeY.SetValueInstant(m_ZoomEnvelopeY.GetMinValue());
1706 }
1707 else if(ValueRange > m_ZoomEnvelopeY.GetMaxValue())
1708 {
1709 m_OffsetEnvelopeY = -Bottom / m_ZoomEnvelopeY.GetMaxValue();
1710 m_ZoomEnvelopeY.SetValueInstant(m_ZoomEnvelopeY.GetMaxValue());
1711 }
1712 else
1713 {
1714 // calculate biggest possible spacing
1715 float SpacingFactor = std::min(a: 1.25f, b: m_ZoomEnvelopeY.GetMaxValue() / ValueRange);
1716 m_ZoomEnvelopeY.SetValueInstant(SpacingFactor * ValueRange);
1717 float Space = 1.0f / SpacingFactor;
1718 float Spacing = (1.0f - Space) / 2.0f;
1719
1720 if(Top >= 0 && Bottom >= 0)
1721 m_OffsetEnvelopeY = Spacing - Bottom / m_ZoomEnvelopeY.GetValue();
1722 else if(Top <= 0 && Bottom <= 0)
1723 m_OffsetEnvelopeY = Spacing - Bottom / m_ZoomEnvelopeY.GetValue();
1724 else
1725 m_OffsetEnvelopeY = Spacing + Space * absolute(a: Bottom) / ValueRange;
1726 }
1727
1728 if(EndTime < m_ZoomEnvelopeX.GetMinValue())
1729 {
1730 m_OffsetEnvelopeX = 0.5f - EndTime / m_ZoomEnvelopeX.GetMinValue();
1731 m_ZoomEnvelopeX.SetValueInstant(m_ZoomEnvelopeX.GetMinValue());
1732 }
1733 else if(EndTime > m_ZoomEnvelopeX.GetMaxValue())
1734 {
1735 m_OffsetEnvelopeX = 0.0f;
1736 m_ZoomEnvelopeX.SetValueInstant(m_ZoomEnvelopeX.GetMaxValue());
1737 }
1738 else
1739 {
1740 float SpacingFactor = std::min(a: 1.25f, b: m_ZoomEnvelopeX.GetMaxValue() / EndTime);
1741 m_ZoomEnvelopeX.SetValueInstant(SpacingFactor * EndTime);
1742 float Space = 1.0f / SpacingFactor;
1743 float Spacing = (1.0f - Space) / 2.0f;
1744
1745 m_OffsetEnvelopeX = Spacing;
1746 }
1747}
1748
1749float CEditor::ScreenToEnvelopeX(const CUIRect &View, float x) const
1750{
1751 return (x - View.x - View.w * m_OffsetEnvelopeX) / View.w * m_ZoomEnvelopeX.GetValue();
1752}
1753
1754float CEditor::EnvelopeToScreenX(const CUIRect &View, float x) const
1755{
1756 return View.x + View.w * m_OffsetEnvelopeX + x / m_ZoomEnvelopeX.GetValue() * View.w;
1757}
1758
1759float CEditor::ScreenToEnvelopeY(const CUIRect &View, float y) const
1760{
1761 return (View.h - y + View.y) / View.h * m_ZoomEnvelopeY.GetValue() - m_OffsetEnvelopeY * m_ZoomEnvelopeY.GetValue();
1762}
1763
1764float CEditor::EnvelopeToScreenY(const CUIRect &View, float y) const
1765{
1766 return View.y + View.h - y / m_ZoomEnvelopeY.GetValue() * View.h - m_OffsetEnvelopeY * View.h;
1767}
1768
1769float CEditor::ScreenToEnvelopeDX(const CUIRect &View, float DeltaX)
1770{
1771 return DeltaX / Graphics()->ScreenWidth() * Ui()->Screen()->w / View.w * m_ZoomEnvelopeX.GetValue();
1772}
1773
1774float CEditor::ScreenToEnvelopeDY(const CUIRect &View, float DeltaY)
1775{
1776 return DeltaY / Graphics()->ScreenHeight() * Ui()->Screen()->h / View.h * m_ZoomEnvelopeY.GetValue();
1777}
1778
1779void CEditor::RemoveTimeOffsetEnvelope(const std::shared_ptr<CEnvelope> &pEnvelope)
1780{
1781 CFixedTime TimeOffset = pEnvelope->m_vPoints[0].m_Time;
1782 for(auto &Point : pEnvelope->m_vPoints)
1783 Point.m_Time -= TimeOffset;
1784
1785 m_OffsetEnvelopeX += TimeOffset.AsSeconds() / m_ZoomEnvelopeX.GetValue();
1786}
1787
1788CUi::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect View, bool Active)
1789{
1790 CEditor *pEditor = static_cast<CEditor *>(pContext);
1791 if(pEditor->Map()->m_SelectedEnvelope < 0 || pEditor->Map()->m_SelectedEnvelope >= (int)pEditor->Map()->m_vpEnvelopes.size())
1792 return CUi::POPUP_CLOSE_CURRENT;
1793
1794 const float RowHeight = 12.0f;
1795 CUIRect Row, Label, EditBox;
1796
1797 pEditor->m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
1798
1799 std::shared_ptr<CEnvelope> pEnvelope = pEditor->Map()->m_vpEnvelopes[pEditor->Map()->m_SelectedEnvelope];
1800
1801 if(pEnvelope->GetChannels() == 4 && !pEditor->Map()->IsTangentSelected())
1802 {
1803 View.HSplitTop(Cut: RowHeight, pTop: &Row, pBottom: &View);
1804 View.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &View);
1805 Row.VSplitLeft(Cut: 60.0f, pLeft: &Label, pRight: &Row);
1806 Row.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &EditBox);
1807 pEditor->Ui()->DoLabel(pRect: &Label, pText: "Color:", Size: RowHeight - 2.0f, Align: TEXTALIGN_ML);
1808
1809 const auto SelectedPoint = pEditor->Map()->m_vSelectedEnvelopePoints.front();
1810 const int SelectedIndex = SelectedPoint.first;
1811 auto *pValues = pEnvelope->m_vPoints[SelectedIndex].m_aValues;
1812 const ColorRGBA Color = pEnvelope->m_vPoints[SelectedIndex].ColorValue();
1813 const auto &&SetColor = [&](ColorRGBA NewColor) {
1814 if(Color == NewColor && pEditor->m_ColorPickerPopupContext.m_State == EEditState::EDITING)
1815 return;
1816
1817 static int s_Values[4];
1818
1819 if(pEditor->m_ColorPickerPopupContext.m_State == EEditState::START || pEditor->m_ColorPickerPopupContext.m_State == EEditState::ONE_GO)
1820 {
1821 for(int Channel = 0; Channel < 4; ++Channel)
1822 s_Values[Channel] = pValues[Channel];
1823 }
1824
1825 pEnvelope->m_vPoints[SelectedIndex].SetColorValue(NewColor);
1826
1827 if(pEditor->m_ColorPickerPopupContext.m_State == EEditState::END || pEditor->m_ColorPickerPopupContext.m_State == EEditState::ONE_GO)
1828 {
1829 std::vector<std::shared_ptr<IEditorAction>> vpActions(4);
1830
1831 for(int Channel = 0; Channel < 4; ++Channel)
1832 {
1833 vpActions[Channel] = std::make_shared<CEditorActionEnvelopeEditPoint>(args: pEditor->Map(), args&: pEditor->Map()->m_SelectedEnvelope, args: SelectedIndex, args&: Channel, args: CEditorActionEnvelopeEditPoint::EEditType::VALUE, args&: s_Values[Channel], args: f2fx(v: NewColor[Channel]));
1834 }
1835
1836 char aDisplay[256];
1837 str_format(buffer: aDisplay, buffer_size: sizeof(aDisplay), format: "Edit color of point %d of envelope %d", SelectedIndex, pEditor->Map()->m_SelectedEnvelope);
1838 pEditor->Map()->m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionBulk>(args: pEditor->Map(), args&: vpActions, args&: aDisplay));
1839 }
1840
1841 pEditor->Map()->m_UpdateEnvPointInfo = true;
1842 pEditor->Map()->OnModify();
1843 };
1844 static char s_ColorPickerButton;
1845 pEditor->DoColorPickerButton(pId: &s_ColorPickerButton, pRect: &EditBox, Color, SetColor);
1846 }
1847
1848 static CLineInputNumber s_CurValueInput;
1849 static CLineInputNumber s_CurTimeInput;
1850
1851 static float s_CurrentTime = 0;
1852 static float s_CurrentValue = 0;
1853
1854 if(pEditor->Map()->m_UpdateEnvPointInfo)
1855 {
1856 pEditor->Map()->m_UpdateEnvPointInfo = false;
1857
1858 const auto &[CurrentTime, CurrentValue] = pEditor->Map()->SelectedEnvelopeTimeAndValue();
1859
1860 // update displayed text
1861 s_CurValueInput.SetFloat(fx2f(v: CurrentValue));
1862 s_CurTimeInput.SetFloat(CurrentTime.AsSeconds());
1863
1864 s_CurrentTime = s_CurTimeInput.GetFloat();
1865 s_CurrentValue = s_CurValueInput.GetFloat();
1866 }
1867
1868 View.HSplitTop(Cut: RowHeight, pTop: &Row, pBottom: &View);
1869 Row.VSplitLeft(Cut: 60.0f, pLeft: &Label, pRight: &Row);
1870 Row.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &EditBox);
1871 pEditor->Ui()->DoLabel(pRect: &Label, pText: "Value:", Size: RowHeight - 2.0f, Align: TEXTALIGN_ML);
1872 pEditor->DoEditBox(pLineInput: &s_CurValueInput, pRect: &EditBox, FontSize: RowHeight - 2.0f, Corners: IGraphics::CORNER_ALL, pToolTip: "The value of the selected envelope point.");
1873
1874 View.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &View);
1875 View.HSplitTop(Cut: RowHeight, pTop: &Row, pBottom: &View);
1876 Row.VSplitLeft(Cut: 60.0f, pLeft: &Label, pRight: &Row);
1877 Row.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &EditBox);
1878 pEditor->Ui()->DoLabel(pRect: &Label, pText: "Time (in s):", Size: RowHeight - 2.0f, Align: TEXTALIGN_ML);
1879 pEditor->DoEditBox(pLineInput: &s_CurTimeInput, pRect: &EditBox, FontSize: RowHeight - 2.0f, Corners: IGraphics::CORNER_ALL, pToolTip: "The time of the selected envelope point.");
1880
1881 if(pEditor->Input()->KeyIsPressed(Key: KEY_RETURN) || pEditor->Input()->KeyIsPressed(Key: KEY_KP_ENTER))
1882 {
1883 float CurrentTime = s_CurTimeInput.GetFloat();
1884 float CurrentValue = s_CurValueInput.GetFloat();
1885 if(!(absolute(a: CurrentTime - s_CurrentTime) < 0.0001f && absolute(a: CurrentValue - s_CurrentValue) < 0.0001f))
1886 {
1887 const auto &[OldTime, OldValue] = pEditor->Map()->SelectedEnvelopeTimeAndValue();
1888
1889 if(pEditor->Map()->IsTangentInSelected())
1890 {
1891 auto [SelectedIndex, SelectedChannel] = pEditor->Map()->m_SelectedTangentInPoint;
1892
1893 pEditor->Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionEditEnvelopePointValue>(args: pEditor->Map(), args&: pEditor->Map()->m_SelectedEnvelope, args&: SelectedIndex, args&: SelectedChannel, args: CEditorActionEditEnvelopePointValue::EType::TANGENT_IN, args: OldTime, args: OldValue, args: CFixedTime::FromSeconds(Seconds: CurrentTime), args: f2fx(v: CurrentValue)));
1894 CurrentTime = (pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel]).AsSeconds();
1895 }
1896 else if(pEditor->Map()->IsTangentOutSelected())
1897 {
1898 auto [SelectedIndex, SelectedChannel] = pEditor->Map()->m_SelectedTangentOutPoint;
1899
1900 pEditor->Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionEditEnvelopePointValue>(args: pEditor->Map(), args&: pEditor->Map()->m_SelectedEnvelope, args&: SelectedIndex, args&: SelectedChannel, args: CEditorActionEditEnvelopePointValue::EType::TANGENT_OUT, args: OldTime, args: OldValue, args: CFixedTime::FromSeconds(Seconds: CurrentTime), args: f2fx(v: CurrentValue)));
1901 CurrentTime = (pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel]).AsSeconds();
1902 }
1903 else
1904 {
1905 auto [SelectedIndex, SelectedChannel] = pEditor->Map()->m_vSelectedEnvelopePoints.front();
1906 pEditor->Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionEditEnvelopePointValue>(args: pEditor->Map(), args&: pEditor->Map()->m_SelectedEnvelope, args&: SelectedIndex, args&: SelectedChannel, args: CEditorActionEditEnvelopePointValue::EType::POINT, args: OldTime, args: OldValue, args: CFixedTime::FromSeconds(Seconds: CurrentTime), args: f2fx(v: CurrentValue)));
1907
1908 if(SelectedIndex != 0)
1909 {
1910 CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time.AsSeconds();
1911 }
1912 else
1913 {
1914 CurrentTime = 0.0f;
1915 pEnvelope->m_vPoints[SelectedIndex].m_Time = CFixedTime(0);
1916 }
1917 }
1918
1919 s_CurTimeInput.SetFloat(CFixedTime::FromSeconds(Seconds: CurrentTime).AsSeconds());
1920 s_CurValueInput.SetFloat(fx2f(v: f2fx(v: CurrentValue)));
1921
1922 s_CurrentTime = s_CurTimeInput.GetFloat();
1923 s_CurrentValue = s_CurValueInput.GetFloat();
1924 }
1925 }
1926
1927 View.HSplitTop(Cut: 6.0f, pTop: nullptr, pBottom: &View);
1928 View.HSplitTop(Cut: RowHeight, pTop: &Row, pBottom: &View);
1929 static int s_DeleteButtonId = 0;
1930 const char *pButtonText = pEditor->Map()->IsTangentSelected() ? "Reset" : "Delete";
1931 const char *pTooltip = pEditor->Map()->IsTangentSelected() ? "Reset tangent point to default value." : "Delete current envelope point in all channels.";
1932 if(pEditor->DoButton_Editor(pId: &s_DeleteButtonId, pText: pButtonText, Checked: 0, pRect: &Row, Flags: BUTTONFLAG_LEFT, pToolTip: pTooltip))
1933 {
1934 if(pEditor->Map()->IsTangentInSelected())
1935 {
1936 auto [SelectedIndex, SelectedChannel] = pEditor->Map()->m_SelectedTangentInPoint;
1937 const auto &[OldTime, OldValue] = pEditor->Map()->SelectedEnvelopeTimeAndValue();
1938 pEditor->Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionResetEnvelopePointTangent>(args: pEditor->Map(), args&: pEditor->Map()->m_SelectedEnvelope, args&: SelectedIndex, args&: SelectedChannel, args: true, args: OldTime, args: OldValue));
1939 }
1940 else if(pEditor->Map()->IsTangentOutSelected())
1941 {
1942 auto [SelectedIndex, SelectedChannel] = pEditor->Map()->m_SelectedTangentOutPoint;
1943 const auto &[OldTime, OldValue] = pEditor->Map()->SelectedEnvelopeTimeAndValue();
1944 pEditor->Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionResetEnvelopePointTangent>(args: pEditor->Map(), args&: pEditor->Map()->m_SelectedEnvelope, args&: SelectedIndex, args&: SelectedChannel, args: false, args: OldTime, args: OldValue));
1945 }
1946 else
1947 {
1948 auto [SelectedIndex, SelectedChannel] = pEditor->Map()->m_vSelectedEnvelopePoints.front();
1949 pEditor->Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionDeleteEnvelopePoint>(args: pEditor->Map(), args&: pEditor->Map()->m_SelectedEnvelope, args&: SelectedIndex));
1950 }
1951
1952 return CUi::POPUP_CLOSE_CURRENT;
1953 }
1954
1955 return CUi::POPUP_KEEP_OPEN;
1956}
1957
1958CUi::EPopupMenuFunctionResult CEditor::PopupEnvPointMulti(void *pContext, CUIRect View, bool Active)
1959{
1960 CEditor *pEditor = static_cast<CEditor *>(pContext);
1961 const float RowHeight = 12.0f;
1962
1963 static int s_CurveButtonId = 0;
1964 CUIRect CurveButton;
1965 View.HSplitTop(Cut: RowHeight, pTop: &CurveButton, pBottom: &View);
1966 if(pEditor->DoButton_MenuItem(pId: &s_CurveButtonId, pText: "Project onto", Checked: 0, pRect: &CurveButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Project all selected envelopes onto the curve between the first and last selected envelope."))
1967 {
1968 static SPopupMenuId s_PopupCurveTypeId;
1969 pEditor->Ui()->DoPopupMenu(pId: &s_PopupCurveTypeId, X: pEditor->Ui()->MouseX(), Y: pEditor->Ui()->MouseY(), Width: 80, Height: 80, pContext: pEditor, pfnFunc: PopupEnvPointCurveType);
1970 }
1971
1972 return CUi::POPUP_KEEP_OPEN;
1973}
1974
1975CUi::EPopupMenuFunctionResult CEditor::PopupEnvPointCurveType(void *pContext, CUIRect View, bool Active)
1976{
1977 CEditor *pEditor = static_cast<CEditor *>(pContext);
1978 const float RowHeight = 14.0f;
1979
1980 int CurveType = -1;
1981
1982 static int s_ButtonLinearId;
1983 CUIRect ButtonLinear;
1984 View.HSplitTop(Cut: RowHeight, pTop: &ButtonLinear, pBottom: &View);
1985 if(pEditor->DoButton_MenuItem(pId: &s_ButtonLinearId, pText: CURVE_TYPE_NAMES[CURVETYPE_LINEAR], Checked: 0, pRect: &ButtonLinear))
1986 CurveType = CURVETYPE_LINEAR;
1987
1988 static int s_ButtonSlowId;
1989 CUIRect ButtonSlow;
1990 View.HSplitTop(Cut: RowHeight, pTop: &ButtonSlow, pBottom: &View);
1991 if(pEditor->DoButton_MenuItem(pId: &s_ButtonSlowId, pText: CURVE_TYPE_NAMES[CURVETYPE_SLOW], Checked: 0, pRect: &ButtonSlow))
1992 CurveType = CURVETYPE_SLOW;
1993
1994 static int s_ButtonFastId;
1995 CUIRect ButtonFast;
1996 View.HSplitTop(Cut: RowHeight, pTop: &ButtonFast, pBottom: &View);
1997 if(pEditor->DoButton_MenuItem(pId: &s_ButtonFastId, pText: CURVE_TYPE_NAMES[CURVETYPE_FAST], Checked: 0, pRect: &ButtonFast))
1998 CurveType = CURVETYPE_FAST;
1999
2000 static int s_ButtonStepId;
2001 CUIRect ButtonStep;
2002 View.HSplitTop(Cut: RowHeight, pTop: &ButtonStep, pBottom: &View);
2003 if(pEditor->DoButton_MenuItem(pId: &s_ButtonStepId, pText: CURVE_TYPE_NAMES[CURVETYPE_STEP], Checked: 0, pRect: &ButtonStep))
2004 CurveType = CURVETYPE_STEP;
2005
2006 static int s_ButtonSmoothId;
2007 CUIRect ButtonSmooth;
2008 View.HSplitTop(Cut: RowHeight, pTop: &ButtonSmooth, pBottom: &View);
2009 if(pEditor->DoButton_MenuItem(pId: &s_ButtonSmoothId, pText: CURVE_TYPE_NAMES[CURVETYPE_SMOOTH], Checked: 0, pRect: &ButtonSmooth))
2010 CurveType = CURVETYPE_SMOOTH;
2011
2012 std::vector<std::shared_ptr<IEditorAction>> vpActions;
2013
2014 if(CurveType >= 0)
2015 {
2016 std::shared_ptr<CEnvelope> pEnvelope = pEditor->Map()->m_vpEnvelopes.at(n: pEditor->Map()->m_SelectedEnvelope);
2017
2018 for(int c = 0; c < pEnvelope->GetChannels(); c++)
2019 {
2020 int FirstSelectedIndex = pEnvelope->m_vPoints.size();
2021 int LastSelectedIndex = -1;
2022 for(auto [SelectedIndex, SelectedChannel] : pEditor->Map()->m_vSelectedEnvelopePoints)
2023 {
2024 if(SelectedChannel == c)
2025 {
2026 FirstSelectedIndex = std::min(a: FirstSelectedIndex, b: SelectedIndex);
2027 LastSelectedIndex = std::max(a: LastSelectedIndex, b: SelectedIndex);
2028 }
2029 }
2030
2031 if(FirstSelectedIndex < (int)pEnvelope->m_vPoints.size() && LastSelectedIndex >= 0 && FirstSelectedIndex != LastSelectedIndex)
2032 {
2033 CEnvPoint FirstPoint = pEnvelope->m_vPoints[FirstSelectedIndex];
2034 CEnvPoint LastPoint = pEnvelope->m_vPoints[LastSelectedIndex];
2035
2036 CEnvelope HelperEnvelope(1);
2037 HelperEnvelope.AddPoint(Time: FirstPoint.m_Time, aValues: {FirstPoint.m_aValues[c], 0, 0, 0});
2038 HelperEnvelope.AddPoint(Time: LastPoint.m_Time, aValues: {LastPoint.m_aValues[c], 0, 0, 0});
2039 HelperEnvelope.m_vPoints[0].m_Curvetype = CurveType;
2040
2041 for(auto [SelectedIndex, SelectedChannel] : pEditor->Map()->m_vSelectedEnvelopePoints)
2042 {
2043 if(SelectedChannel == c)
2044 {
2045 if(SelectedIndex != FirstSelectedIndex && SelectedIndex != LastSelectedIndex)
2046 {
2047 CEnvPoint &CurrentPoint = pEnvelope->m_vPoints[SelectedIndex];
2048 ColorRGBA Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
2049 HelperEnvelope.Eval(Time: CurrentPoint.m_Time.AsSeconds(), Result&: Channels, Channels: 1);
2050 int PrevValue = CurrentPoint.m_aValues[c];
2051 CurrentPoint.m_aValues[c] = f2fx(v: Channels.r);
2052 vpActions.push_back(x: std::make_shared<CEditorActionEnvelopeEditPoint>(args: pEditor->Map(), args&: pEditor->Map()->m_SelectedEnvelope, args&: SelectedIndex, args&: SelectedChannel, args: CEditorActionEnvelopeEditPoint::EEditType::VALUE, args&: PrevValue, args&: CurrentPoint.m_aValues[c]));
2053 }
2054 }
2055 }
2056 }
2057 }
2058
2059 if(!vpActions.empty())
2060 {
2061 pEditor->Map()->m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionBulk>(args: pEditor->Map(), args&: vpActions, args: "Project points"));
2062 }
2063
2064 pEditor->Map()->OnModify();
2065 return CUi::POPUP_CLOSE_CURRENT;
2066 }
2067
2068 return CUi::POPUP_KEEP_OPEN;
2069}
2070
2071CUi::EPopupMenuFunctionResult CEditor::PopupEnvelopeCurvetype(void *pContext, CUIRect View, bool Active)
2072{
2073 CEditor *pEditor = static_cast<CEditor *>(pContext);
2074
2075 if(pEditor->Map()->m_SelectedEnvelope < 0 || pEditor->Map()->m_SelectedEnvelope >= (int)pEditor->Map()->m_vpEnvelopes.size())
2076 {
2077 return CUi::POPUP_CLOSE_CURRENT;
2078 }
2079 std::shared_ptr<CEnvelope> pEnvelope = pEditor->Map()->m_vpEnvelopes[pEditor->Map()->m_SelectedEnvelope];
2080
2081 if(pEditor->m_PopupEnvelopeSelectedPoint < 0 || pEditor->m_PopupEnvelopeSelectedPoint >= (int)pEnvelope->m_vPoints.size())
2082 {
2083 return CUi::POPUP_CLOSE_CURRENT;
2084 }
2085 CEnvPoint_runtime &SelectedPoint = pEnvelope->m_vPoints[pEditor->m_PopupEnvelopeSelectedPoint];
2086
2087 static char s_aButtonIds[NUM_CURVETYPES] = {0};
2088
2089 for(int Type = 0; Type < NUM_CURVETYPES; Type++)
2090 {
2091 CUIRect Button;
2092 View.HSplitTop(Cut: 14.0f, pTop: &Button, pBottom: &View);
2093
2094 if(pEditor->DoButton_MenuItem(pId: &s_aButtonIds[Type], pText: CURVE_TYPE_NAMES[Type], Checked: Type == SelectedPoint.m_Curvetype, pRect: &Button))
2095 {
2096 const int PrevCurve = SelectedPoint.m_Curvetype;
2097 if(PrevCurve != Type)
2098 {
2099 SelectedPoint.m_Curvetype = Type;
2100 pEditor->Map()->m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEditPoint>(args: pEditor->Map(),
2101 args&: pEditor->Map()->m_SelectedEnvelope, args&: pEditor->m_PopupEnvelopeSelectedPoint, args: 0, args: CEditorActionEnvelopeEditPoint::EEditType::CURVE_TYPE, args: PrevCurve, args&: SelectedPoint.m_Curvetype));
2102 pEditor->Map()->OnModify();
2103 return CUi::POPUP_CLOSE_CURRENT;
2104 }
2105 }
2106 }
2107
2108 return CUi::POPUP_KEEP_OPEN;
2109}
2110