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