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