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 <algorithm>
5
6#include <base/color.h>
7#include <base/system.h>
8
9#if defined(CONF_FAMILY_UNIX)
10#include <pthread.h>
11#endif
12
13#include <engine/client.h>
14#include <engine/console.h>
15#include <engine/gfx/image_loader.h>
16#include <engine/gfx/image_manipulation.h>
17#include <engine/graphics.h>
18#include <engine/input.h>
19#include <engine/keys.h>
20#include <engine/shared/config.h>
21#include <engine/shared/filecollection.h>
22#include <engine/storage.h>
23#include <engine/textrender.h>
24
25#include <game/client/components/camera.h>
26#include <game/client/gameclient.h>
27#include <game/client/lineinput.h>
28#include <game/client/render.h>
29#include <game/client/ui.h>
30#include <game/client/ui_listbox.h>
31#include <game/client/ui_scrollregion.h>
32#include <game/generated/client_data.h>
33#include <game/localization.h>
34
35#include <game/editor/editor_history.h>
36#include <game/editor/mapitems/image.h>
37#include <game/editor/mapitems/sound.h>
38
39#include "auto_map.h"
40#include "editor.h"
41#include "editor_actions.h"
42
43#include <chrono>
44#include <iterator>
45#include <limits>
46#include <type_traits>
47
48using namespace FontIcons;
49
50static const char *VANILLA_IMAGES[] = {
51 "bg_cloud1",
52 "bg_cloud2",
53 "bg_cloud3",
54 "desert_doodads",
55 "desert_main",
56 "desert_mountains",
57 "desert_mountains2",
58 "desert_sun",
59 "generic_deathtiles",
60 "generic_unhookable",
61 "grass_doodads",
62 "grass_main",
63 "jungle_background",
64 "jungle_deathtiles",
65 "jungle_doodads",
66 "jungle_main",
67 "jungle_midground",
68 "jungle_unhookables",
69 "moon",
70 "mountains",
71 "snow",
72 "stars",
73 "sun",
74 "winter_doodads",
75 "winter_main",
76 "winter_mountains",
77 "winter_mountains2",
78 "winter_mountains3"};
79
80bool CEditor::IsVanillaImage(const char *pImage)
81{
82 return std::any_of(first: std::begin(arr&: VANILLA_IMAGES), last: std::end(arr&: VANILLA_IMAGES), pred: [pImage](const char *pVanillaImage) { return str_comp(a: pImage, b: pVanillaImage) == 0; });
83}
84
85static const char *FILETYPE_EXTENSIONS[CEditor::NUM_FILETYPES] = {
86 ".map",
87 ".png",
88 ".opus"};
89
90const void *CEditor::ms_pUiGotContext;
91
92enum
93{
94 BUTTON_CONTEXT = 1,
95};
96
97void CEditor::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Result, size_t Channels, void *pUser)
98{
99 CEditor *pThis = (CEditor *)pUser;
100 if(Env < 0 || Env >= (int)pThis->m_Map.m_vpEnvelopes.size())
101 return;
102
103 std::shared_ptr<CEnvelope> pEnv = pThis->m_Map.m_vpEnvelopes[Env];
104 float Time = pThis->m_AnimateTime;
105 Time *= pThis->m_AnimateSpeed;
106 Time += (TimeOffsetMillis / 1000.0f);
107 pEnv->Eval(Time, Result, Channels);
108}
109
110/********************************************************
111 OTHER
112*********************************************************/
113
114bool CEditor::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const char *pToolTip, const std::vector<STextColorSplit> &vColorSplits)
115{
116 UpdateTooltip(pId: pLineInput, pRect, pToolTip);
117 return Ui()->DoEditBox(pLineInput, pRect, FontSize, Corners, vColorSplits);
118}
119
120bool CEditor::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const char *pToolTip, const std::vector<STextColorSplit> &vColorSplits)
121{
122 UpdateTooltip(pId: pLineInput, pRect, pToolTip);
123 return Ui()->DoClearableEditBox(pLineInput, pRect, FontSize, Corners, vColorSplits);
124}
125
126ColorRGBA CEditor::GetButtonColor(const void *pId, int Checked)
127{
128 if(Checked < 0)
129 return ColorRGBA(0, 0, 0, 0.5f);
130
131 switch(Checked)
132 {
133 case 8: // invisible
134 return ColorRGBA(0, 0, 0, 0);
135 case 7: // selected + game layers
136 if(Ui()->HotItem() == pId)
137 return ColorRGBA(1, 0, 0, 0.4f);
138 return ColorRGBA(1, 0, 0, 0.2f);
139
140 case 6: // game layers
141 if(Ui()->HotItem() == pId)
142 return ColorRGBA(1, 1, 1, 0.4f);
143 return ColorRGBA(1, 1, 1, 0.2f);
144
145 case 5: // selected + image/sound should be embedded
146 if(Ui()->HotItem() == pId)
147 return ColorRGBA(1, 0, 0, 0.75f);
148 return ColorRGBA(1, 0, 0, 0.5f);
149
150 case 4: // image/sound should be embedded
151 if(Ui()->HotItem() == pId)
152 return ColorRGBA(1, 0, 0, 1.0f);
153 return ColorRGBA(1, 0, 0, 0.875f);
154
155 case 3: // selected + unused image/sound
156 if(Ui()->HotItem() == pId)
157 return ColorRGBA(1, 0, 1, 0.75f);
158 return ColorRGBA(1, 0, 1, 0.5f);
159
160 case 2: // unused image/sound
161 if(Ui()->HotItem() == pId)
162 return ColorRGBA(0, 0, 1, 0.75f);
163 return ColorRGBA(0, 0, 1, 0.5f);
164
165 case 1: // selected
166 if(Ui()->HotItem() == pId)
167 return ColorRGBA(1, 0, 0, 0.75f);
168 return ColorRGBA(1, 0, 0, 0.5f);
169
170 default: // regular
171 if(Ui()->HotItem() == pId)
172 return ColorRGBA(1, 1, 1, 0.75f);
173 return ColorRGBA(1, 1, 1, 0.5f);
174 }
175}
176
177void CEditor::UpdateTooltip(const void *pId, const CUIRect *pRect, const char *pToolTip)
178{
179 if(Ui()->MouseInside(pRect) && !pToolTip)
180 str_copy(dst&: m_aTooltip, src: "");
181 else if(Ui()->HotItem() == pId && pToolTip)
182 str_copy(dst&: m_aTooltip, src: pToolTip);
183}
184
185int CEditor::DoButton_Editor_Common(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip)
186{
187 if(Ui()->MouseInside(pRect))
188 {
189 if(Flags & BUTTON_CONTEXT)
190 ms_pUiGotContext = pId;
191 }
192
193 UpdateTooltip(pId, pRect, pToolTip);
194 return Ui()->DoButtonLogic(pId, Checked, pRect);
195}
196
197int CEditor::DoButton_Editor(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip)
198{
199 pRect->Draw(Color: GetButtonColor(pId, Checked), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
200 CUIRect NewRect = *pRect;
201 Ui()->DoLabel(pRect: &NewRect, pText, Size: 10.0f, Align: TEXTALIGN_MC);
202 Checked %= 2;
203 return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags, pToolTip);
204}
205
206int CEditor::DoButton_Env(const void *pId, const char *pText, int Checked, const CUIRect *pRect, const char *pToolTip, ColorRGBA BaseColor, int Corners)
207{
208 float Bright = Checked ? 1.0f : 0.5f;
209 float Alpha = Ui()->HotItem() == pId ? 1.0f : 0.75f;
210 ColorRGBA Color = ColorRGBA(BaseColor.r * Bright, BaseColor.g * Bright, BaseColor.b * Bright, Alpha);
211
212 pRect->Draw(Color, Corners, Rounding: 3.0f);
213 Ui()->DoLabel(pRect, pText, Size: 10.0f, Align: TEXTALIGN_MC);
214 Checked %= 2;
215 return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags: 0, pToolTip);
216}
217
218int CEditor::DoButton_MenuItem(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip)
219{
220 if(Ui()->HotItem() == pId || Checked)
221 pRect->Draw(Color: GetButtonColor(pId, Checked), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
222
223 CUIRect Rect;
224 pRect->VMargin(Cut: 5.0f, pOtherRect: &Rect);
225
226 SLabelProperties Props;
227 Props.m_MaxWidth = Rect.w;
228 Props.m_EllipsisAtEnd = true;
229 Ui()->DoLabel(pRect: &Rect, pText, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
230
231 return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags, pToolTip);
232}
233
234int CEditor::DoButton_Ex(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip, int Corners, float FontSize, int Align)
235{
236 pRect->Draw(Color: GetButtonColor(pId, Checked), Corners, Rounding: 3.0f);
237
238 CUIRect Rect;
239 pRect->VMargin(Cut: ((Align & TEXTALIGN_MASK_HORIZONTAL) == TEXTALIGN_CENTER) ? 1.0f : 5.0f, pOtherRect: &Rect);
240
241 SLabelProperties Props;
242 Props.m_MaxWidth = Rect.w;
243 Props.m_EllipsisAtEnd = true;
244 Ui()->DoLabel(pRect: &Rect, pText, Size: FontSize, Align, LabelProps: Props);
245
246 return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags, pToolTip);
247}
248
249int CEditor::DoButton_FontIcon(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip, int Corners, float FontSize)
250{
251 pRect->Draw(Color: GetButtonColor(pId, Checked), Corners, Rounding: 3.0f);
252
253 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
254 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
255 Ui()->DoLabel(pRect, pText, Size: FontSize, Align: TEXTALIGN_MC);
256 TextRender()->SetRenderFlags(0);
257 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
258
259 return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags, pToolTip);
260}
261
262int CEditor::DoButton_DraggableEx(const void *pId, const char *pText, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted, int Flags, const char *pToolTip, int Corners, float FontSize)
263{
264 pRect->Draw(Color: GetButtonColor(pId, Checked), Corners, Rounding: 3.0f);
265
266 CUIRect Rect;
267 pRect->VMargin(Cut: pRect->w > 20.0f ? 5.0f : 0.0f, pOtherRect: &Rect);
268
269 SLabelProperties Props;
270 Props.m_MaxWidth = Rect.w;
271 Props.m_EllipsisAtEnd = true;
272 Ui()->DoLabel(pRect: &Rect, pText, Size: FontSize, Align: TEXTALIGN_MC, LabelProps: Props);
273
274 if(Ui()->MouseInside(pRect))
275 {
276 if(Flags & BUTTON_CONTEXT)
277 ms_pUiGotContext = pId;
278 }
279
280 UpdateTooltip(pId, pRect, pToolTip);
281 return Ui()->DoDraggableButtonLogic(pId, Checked, pRect, pClicked, pAbrupted);
282}
283
284void CEditor::RenderBackground(CUIRect View, IGraphics::CTextureHandle Texture, float Size, float Brightness) const
285{
286 Graphics()->TextureSet(Texture);
287 Graphics()->BlendNormal();
288 Graphics()->QuadsBegin();
289 Graphics()->SetColor(r: Brightness, g: Brightness, b: Brightness, a: 1.0f);
290 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: View.w / Size, BottomRightV: View.h / Size);
291 IGraphics::CQuadItem QuadItem(View.x, View.y, View.w, View.h);
292 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
293 Graphics()->QuadsEnd();
294}
295
296SEditResult<int> CEditor::UiDoValueSelector(void *pId, CUIRect *pRect, const char *pLabel, int Current, int Min, int Max, int Step, float Scale, const char *pToolTip, bool IsDegree, bool IsHex, int Corners, const ColorRGBA *pColor, bool ShowValue)
297{
298 // logic
299 static float s_Value;
300 static CLineInputNumber s_NumberInput;
301 static bool s_TextMode = false;
302 static void *s_pLastTextId = pId;
303 const bool Inside = Ui()->MouseInside(pRect);
304 const int Base = IsHex ? 16 : 10;
305 static bool s_Editing = false;
306 EEditState State = EEditState::EDITING;
307
308 if(Ui()->MouseButton(Index: 1) && Ui()->HotItem() == pId)
309 {
310 s_pLastTextId = pId;
311 s_TextMode = true;
312 Ui()->DisableMouseLock();
313 s_NumberInput.SetInteger(Number: Current, Base);
314 }
315
316 if(Ui()->CheckActiveItem(pId))
317 {
318 if(!Ui()->MouseButton(Index: 0))
319 {
320 Ui()->DisableMouseLock();
321 Ui()->SetActiveItem(nullptr);
322 s_TextMode = false;
323 }
324 }
325
326 if(s_TextMode && s_pLastTextId == pId)
327 {
328 str_copy(dst&: m_aTooltip, src: "Type your number");
329
330 DoEditBox(pLineInput: &s_NumberInput, pRect, FontSize: 10.0f, Corners);
331
332 Ui()->SetActiveItem(&s_NumberInput);
333
334 if(Input()->KeyIsPressed(Key: KEY_RETURN) || Input()->KeyIsPressed(Key: KEY_KP_ENTER) ||
335 ((Ui()->MouseButton(Index: 1) || Ui()->MouseButton(Index: 0)) && !Inside))
336 {
337 Current = clamp(val: s_NumberInput.GetInteger(Base), lo: Min, hi: Max);
338 Ui()->DisableMouseLock();
339 Ui()->SetActiveItem(nullptr);
340 s_TextMode = false;
341 }
342
343 if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
344 {
345 Ui()->DisableMouseLock();
346 Ui()->SetActiveItem(nullptr);
347 s_TextMode = false;
348 }
349 }
350 else
351 {
352 if(Ui()->CheckActiveItem(pId))
353 {
354 if(Ui()->MouseButton(Index: 0))
355 {
356 if(Input()->ShiftIsPressed())
357 s_Value += m_MouseDeltaX * 0.05f;
358 else
359 s_Value += m_MouseDeltaX;
360
361 if(absolute(a: s_Value) >= Scale)
362 {
363 int Count = (int)(s_Value / Scale);
364 s_Value = std::fmod(x: s_Value, y: Scale);
365 Current += Step * Count;
366 Current = clamp(val: Current, lo: Min, hi: Max);
367
368 // Constrain to discrete steps
369 if(Count > 0)
370 Current = Current / Step * Step;
371 else
372 Current = std::ceil(x: Current / (float)Step) * Step;
373 }
374 }
375 if(pToolTip && !s_TextMode)
376 str_copy(dst&: m_aTooltip, src: pToolTip);
377 }
378 else if(Ui()->HotItem() == pId)
379 {
380 if(Ui()->MouseButton(Index: 0))
381 {
382 Ui()->SetActiveItem(pId);
383 Ui()->EnableMouseLock(pId);
384 s_Value = 0;
385 }
386 if(pToolTip && !s_TextMode)
387 str_copy(dst&: m_aTooltip, src: pToolTip);
388 }
389
390 if(Inside)
391 Ui()->SetHotItem(pId);
392
393 // render
394 char aBuf[128];
395 if(pLabel[0] != '\0')
396 {
397 if(ShowValue)
398 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %d", pLabel, Current);
399 else
400 str_copy(dst&: aBuf, src: pLabel);
401 }
402 else if(IsDegree)
403 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d°", Current);
404 else if(IsHex)
405 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "#%06X", Current);
406 else
407 str_from_int(value: Current, dst&: aBuf);
408 pRect->Draw(Color: pColor ? *pColor : GetButtonColor(pId, Checked: 0), Corners, Rounding: 3.0f);
409 Ui()->DoLabel(pRect, pText: aBuf, Size: 10, Align: TEXTALIGN_MC);
410 }
411
412 if(!s_TextMode)
413 s_NumberInput.Clear();
414
415 bool MouseLocked = Ui()->CheckMouseLock();
416 if((MouseLocked || s_TextMode) && !s_Editing)
417 {
418 State = EEditState::START;
419 s_Editing = true;
420 }
421
422 if(!MouseLocked && !s_TextMode && s_Editing)
423 {
424 State = EEditState::END;
425 s_Editing = false;
426 }
427
428 return SEditResult<int>{.m_State: State, .m_Value: Current};
429}
430
431std::shared_ptr<CLayerGroup> CEditor::GetSelectedGroup() const
432{
433 if(m_SelectedGroup >= 0 && m_SelectedGroup < (int)m_Map.m_vpGroups.size())
434 return m_Map.m_vpGroups[m_SelectedGroup];
435 return nullptr;
436}
437
438std::shared_ptr<CLayer> CEditor::GetSelectedLayer(int Index) const
439{
440 std::shared_ptr<CLayerGroup> pGroup = GetSelectedGroup();
441 if(!pGroup)
442 return nullptr;
443
444 if(Index < 0 || Index >= (int)m_vSelectedLayers.size())
445 return nullptr;
446
447 int LayerIndex = m_vSelectedLayers[Index];
448
449 if(LayerIndex >= 0 && LayerIndex < (int)m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size())
450 return pGroup->m_vpLayers[LayerIndex];
451 return nullptr;
452}
453
454std::shared_ptr<CLayer> CEditor::GetSelectedLayerType(int Index, int Type) const
455{
456 std::shared_ptr<CLayer> pLayer = GetSelectedLayer(Index);
457 if(pLayer && pLayer->m_Type == Type)
458 return pLayer;
459 return nullptr;
460}
461
462std::vector<CQuad *> CEditor::GetSelectedQuads()
463{
464 std::shared_ptr<CLayerQuads> pQuadLayer = std::static_pointer_cast<CLayerQuads>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_QUADS));
465 std::vector<CQuad *> vpQuads;
466 if(!pQuadLayer)
467 return vpQuads;
468 vpQuads.resize(new_size: m_vSelectedQuads.size());
469 for(int i = 0; i < (int)m_vSelectedQuads.size(); ++i)
470 vpQuads[i] = &pQuadLayer->m_vQuads[m_vSelectedQuads[i]];
471 return vpQuads;
472}
473
474CSoundSource *CEditor::GetSelectedSource() const
475{
476 std::shared_ptr<CLayerSounds> pSounds = std::static_pointer_cast<CLayerSounds>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_SOUNDS));
477 if(!pSounds)
478 return nullptr;
479 if(m_SelectedSource >= 0 && m_SelectedSource < (int)pSounds->m_vSources.size())
480 return &pSounds->m_vSources[m_SelectedSource];
481 return nullptr;
482}
483
484void CEditor::SelectLayer(int LayerIndex, int GroupIndex)
485{
486 if(GroupIndex != -1)
487 m_SelectedGroup = GroupIndex;
488
489 m_vSelectedLayers.clear();
490 DeselectQuads();
491 DeselectQuadPoints();
492 AddSelectedLayer(LayerIndex);
493}
494
495void CEditor::AddSelectedLayer(int LayerIndex)
496{
497 m_vSelectedLayers.push_back(x: LayerIndex);
498
499 m_QuadKnifeActive = false;
500}
501
502void CEditor::SelectQuad(int Index)
503{
504 m_vSelectedQuads.clear();
505 m_vSelectedQuads.push_back(x: Index);
506}
507
508void CEditor::ToggleSelectQuad(int Index)
509{
510 int ListIndex = FindSelectedQuadIndex(Index);
511 if(ListIndex < 0)
512 m_vSelectedQuads.push_back(x: Index);
513 else
514 m_vSelectedQuads.erase(position: m_vSelectedQuads.begin() + ListIndex);
515}
516
517void CEditor::DeselectQuads()
518{
519 m_vSelectedQuads.clear();
520}
521
522void CEditor::DeselectQuadPoints()
523{
524 m_SelectedQuadPoints = 0;
525}
526
527void CEditor::SelectQuadPoint(int QuadIndex, int Index)
528{
529 SelectQuad(Index: QuadIndex);
530 m_SelectedQuadPoints = 1 << Index;
531}
532
533void CEditor::ToggleSelectQuadPoint(int QuadIndex, int Index)
534{
535 if(IsQuadPointSelected(QuadIndex, Index))
536 {
537 m_SelectedQuadPoints ^= 1 << Index;
538 }
539 else
540 {
541 if(!IsQuadSelected(Index: QuadIndex))
542 {
543 ToggleSelectQuad(Index: QuadIndex);
544 }
545
546 if(!(m_SelectedQuadPoints & 1 << Index))
547 {
548 m_SelectedQuadPoints ^= 1 << Index;
549 }
550 }
551}
552
553void CEditor::DeleteSelectedQuads()
554{
555 std::shared_ptr<CLayerQuads> pLayer = std::static_pointer_cast<CLayerQuads>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_QUADS));
556 if(!pLayer)
557 return;
558
559 std::vector<int> vSelectedQuads(m_vSelectedQuads);
560 std::vector<CQuad> vDeletedQuads;
561
562 for(int i = 0; i < (int)m_vSelectedQuads.size(); ++i)
563 {
564 auto const &Quad = pLayer->m_vQuads[m_vSelectedQuads[i]];
565 vDeletedQuads.push_back(x: Quad);
566
567 pLayer->m_vQuads.erase(position: pLayer->m_vQuads.begin() + m_vSelectedQuads[i]);
568 for(int j = i + 1; j < (int)m_vSelectedQuads.size(); ++j)
569 if(m_vSelectedQuads[j] > m_vSelectedQuads[i])
570 m_vSelectedQuads[j]--;
571
572 m_vSelectedQuads.erase(position: m_vSelectedQuads.begin() + i);
573 i--;
574 }
575
576 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionDeleteQuad>(args: this, args&: m_SelectedGroup, args&: m_vSelectedLayers[0], args&: vSelectedQuads, args&: vDeletedQuads));
577}
578
579bool CEditor::IsQuadSelected(int Index) const
580{
581 return FindSelectedQuadIndex(Index) >= 0;
582}
583
584bool CEditor::IsQuadCornerSelected(int Index) const
585{
586 return m_SelectedQuadPoints & (1 << Index);
587}
588
589bool CEditor::IsQuadPointSelected(int QuadIndex, int Index) const
590{
591 return IsQuadSelected(Index: QuadIndex) && IsQuadCornerSelected(Index);
592}
593
594int CEditor::FindSelectedQuadIndex(int Index) const
595{
596 for(size_t i = 0; i < m_vSelectedQuads.size(); ++i)
597 if(m_vSelectedQuads[i] == Index)
598 return i;
599 return -1;
600}
601
602int CEditor::FindEnvPointIndex(int Index, int Channel) const
603{
604 auto Iter = std::find(
605 first: m_vSelectedEnvelopePoints.begin(),
606 last: m_vSelectedEnvelopePoints.end(),
607 val: std::pair(Index, Channel));
608
609 if(Iter != m_vSelectedEnvelopePoints.end())
610 return Iter - m_vSelectedEnvelopePoints.begin();
611 else
612 return -1;
613}
614
615void CEditor::SelectEnvPoint(int Index)
616{
617 m_vSelectedEnvelopePoints.clear();
618
619 for(int c = 0; c < CEnvPoint::MAX_CHANNELS; c++)
620 m_vSelectedEnvelopePoints.emplace_back(args&: Index, args&: c);
621}
622
623void CEditor::SelectEnvPoint(int Index, int Channel)
624{
625 DeselectEnvPoints();
626 m_vSelectedEnvelopePoints.emplace_back(args&: Index, args&: Channel);
627}
628
629void CEditor::ToggleEnvPoint(int Index, int Channel)
630{
631 if(IsTangentSelected())
632 DeselectEnvPoints();
633
634 int ListIndex = FindEnvPointIndex(Index, Channel);
635
636 if(ListIndex >= 0)
637 {
638 m_vSelectedEnvelopePoints.erase(position: m_vSelectedEnvelopePoints.begin() + ListIndex);
639 }
640 else
641 m_vSelectedEnvelopePoints.emplace_back(args&: Index, args&: Channel);
642}
643
644bool CEditor::IsEnvPointSelected(int Index, int Channel) const
645{
646 int ListIndex = FindEnvPointIndex(Index, Channel);
647
648 return ListIndex >= 0;
649}
650
651bool CEditor::IsEnvPointSelected(int Index) const
652{
653 auto Iter = std::find_if(
654 first: m_vSelectedEnvelopePoints.begin(),
655 last: m_vSelectedEnvelopePoints.end(),
656 pred: [&](auto pair) { return pair.first == Index; });
657
658 return Iter != m_vSelectedEnvelopePoints.end();
659}
660
661void CEditor::DeselectEnvPoints()
662{
663 m_vSelectedEnvelopePoints.clear();
664 m_SelectedTangentInPoint = std::pair(-1, -1);
665 m_SelectedTangentOutPoint = std::pair(-1, -1);
666}
667
668void CEditor::SelectTangentOutPoint(int Index, int Channel)
669{
670 DeselectEnvPoints();
671 m_SelectedTangentOutPoint = std::pair(Index, Channel);
672}
673
674bool CEditor::IsTangentOutPointSelected(int Index, int Channel) const
675{
676 return m_SelectedTangentOutPoint == std::pair(Index, Channel);
677}
678
679void CEditor::SelectTangentInPoint(int Index, int Channel)
680{
681 DeselectEnvPoints();
682 m_SelectedTangentInPoint = std::pair(Index, Channel);
683}
684
685bool CEditor::IsTangentInPointSelected(int Index, int Channel) const
686{
687 return m_SelectedTangentInPoint == std::pair(Index, Channel);
688}
689
690bool CEditor::IsTangentInSelected() const
691{
692 return m_SelectedTangentInPoint != std::pair(-1, -1);
693}
694
695bool CEditor::IsTangentOutSelected() const
696{
697 return m_SelectedTangentOutPoint != std::pair(-1, -1);
698}
699
700bool CEditor::IsTangentSelected() const
701{
702 return IsTangentInSelected() || IsTangentOutSelected();
703}
704
705std::pair<int, int> CEditor::EnvGetSelectedTimeAndValue() const
706{
707 if(m_SelectedEnvelope < 0 || m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size())
708 return {};
709
710 std::shared_ptr<CEnvelope> pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope];
711 int CurrentTime;
712 int CurrentValue;
713 if(IsTangentInSelected())
714 {
715 auto [SelectedIndex, SelectedChannel] = m_SelectedTangentInPoint;
716
717 CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel];
718 CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaY[SelectedChannel];
719 }
720 else if(IsTangentOutSelected())
721 {
722 auto [SelectedIndex, SelectedChannel] = m_SelectedTangentOutPoint;
723
724 CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel];
725 CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaY[SelectedChannel];
726 }
727 else
728 {
729 auto [SelectedIndex, SelectedChannel] = m_vSelectedEnvelopePoints.front();
730
731 CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time;
732 CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel];
733 }
734
735 return std::pair<int, int>{CurrentTime, CurrentValue};
736}
737
738bool CEditor::CallbackOpenMap(const char *pFileName, int StorageType, void *pUser)
739{
740 CEditor *pEditor = (CEditor *)pUser;
741 if(pEditor->Load(pFilename: pFileName, StorageType))
742 {
743 pEditor->m_ValidSaveFilename = StorageType == IStorage::TYPE_SAVE && (pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentFolder || (pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentLink && str_comp(a: pEditor->m_aFileDialogCurrentLink, b: "themes") == 0));
744 if(pEditor->m_Dialog == DIALOG_FILE)
745 pEditor->m_Dialog = DIALOG_NONE;
746 return true;
747 }
748 else
749 {
750 pEditor->ShowFileDialogError(pFormat: "Failed to load map from file '%s'.", pFileName);
751 return false;
752 }
753}
754
755bool CEditor::CallbackAppendMap(const char *pFileName, int StorageType, void *pUser)
756{
757 CEditor *pEditor = (CEditor *)pUser;
758 if(pEditor->Append(pFilename: pFileName, StorageType))
759 {
760 pEditor->m_Dialog = DIALOG_NONE;
761 return true;
762 }
763 else
764 {
765 pEditor->m_aFileName[0] = 0;
766 pEditor->ShowFileDialogError(pFormat: "Failed to load map from file '%s'.", pFileName);
767 return false;
768 }
769}
770
771bool CEditor::CallbackSaveMap(const char *pFileName, int StorageType, void *pUser)
772{
773 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
774
775 CEditor *pEditor = static_cast<CEditor *>(pUser);
776 char aBuf[IO_MAX_PATH_LENGTH];
777 // add map extension
778 if(!str_endswith(str: pFileName, suffix: ".map"))
779 {
780 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s.map", pFileName);
781 pFileName = aBuf;
782 }
783
784 // Save map to specified file
785 if(pEditor->Save(pFilename: pFileName))
786 {
787 str_copy(dst&: pEditor->m_aFileName, src: pFileName);
788 pEditor->m_ValidSaveFilename = true;
789 pEditor->m_Map.m_Modified = false;
790 }
791 else
792 {
793 pEditor->ShowFileDialogError(pFormat: "Failed to save map to file '%s'.", pFileName);
794 return false;
795 }
796
797 // Also update autosave if it's older than half the configured autosave interval, so we also have periodic backups.
798 const float Time = pEditor->Client()->GlobalTime();
799 if(g_Config.m_EdAutosaveInterval > 0 && pEditor->m_Map.m_LastSaveTime < Time && Time - pEditor->m_Map.m_LastSaveTime > 30 * g_Config.m_EdAutosaveInterval)
800 {
801 if(!pEditor->PerformAutosave())
802 return false;
803 }
804
805 pEditor->m_Dialog = DIALOG_NONE;
806 return true;
807}
808
809bool CEditor::CallbackSaveCopyMap(const char *pFileName, int StorageType, void *pUser)
810{
811 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
812
813 CEditor *pEditor = static_cast<CEditor *>(pUser);
814 char aBuf[IO_MAX_PATH_LENGTH];
815 // add map extension
816 if(!str_endswith(str: pFileName, suffix: ".map"))
817 {
818 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s.map", pFileName);
819 pFileName = aBuf;
820 }
821
822 if(pEditor->Save(pFilename: pFileName))
823 {
824 pEditor->m_Dialog = DIALOG_NONE;
825 return true;
826 }
827 else
828 {
829 pEditor->ShowFileDialogError(pFormat: "Failed to save map to file '%s'.", pFileName);
830 return false;
831 }
832}
833
834bool CEditor::CallbackSaveImage(const char *pFileName, int StorageType, void *pUser)
835{
836 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
837
838 CEditor *pEditor = static_cast<CEditor *>(pUser);
839 char aBuf[IO_MAX_PATH_LENGTH];
840
841 // add file extension
842 if(!str_endswith(str: pFileName, suffix: ".png"))
843 {
844 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s.png", pFileName);
845 pFileName = aBuf;
846 }
847
848 std::shared_ptr<CEditorImage> pImg = pEditor->m_Map.m_vpImages[pEditor->m_SelectedImage];
849
850 EImageFormat OutputFormat;
851 switch(pImg->m_Format)
852 {
853 case CImageInfo::FORMAT_RGB:
854 OutputFormat = IMAGE_FORMAT_RGB;
855 break;
856 case CImageInfo::FORMAT_RGBA:
857 OutputFormat = IMAGE_FORMAT_RGBA;
858 break;
859 case CImageInfo::FORMAT_SINGLE_COMPONENT:
860 OutputFormat = IMAGE_FORMAT_R;
861 break;
862 default:
863 dbg_assert(false, "Image has invalid format.");
864 return false;
865 };
866
867 TImageByteBuffer ByteBuffer;
868 SImageByteBuffer ImageByteBuffer(&ByteBuffer);
869 if(SavePng(ImageFormat: OutputFormat, pRawBuffer: pImg->m_pData, WrittenBytes&: ImageByteBuffer, Width: pImg->m_Width, Height: pImg->m_Height))
870 {
871 IOHANDLE File = pEditor->Storage()->OpenFile(pFilename: pFileName, Flags: IOFLAG_WRITE, Type: StorageType);
872 if(File)
873 {
874 io_write(io: File, buffer: &ByteBuffer.front(), size: ByteBuffer.size());
875 io_close(io: File);
876 pEditor->m_Dialog = DIALOG_NONE;
877 return true;
878 }
879 pEditor->ShowFileDialogError(pFormat: "Failed to open file '%s'.", pFileName);
880 return false;
881 }
882 else
883 {
884 pEditor->ShowFileDialogError(pFormat: "Failed to write image to file.");
885 return false;
886 }
887}
888
889bool CEditor::CallbackSaveSound(const char *pFileName, int StorageType, void *pUser)
890{
891 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
892
893 CEditor *pEditor = static_cast<CEditor *>(pUser);
894 char aBuf[IO_MAX_PATH_LENGTH];
895
896 // add file extension
897 if(!str_endswith(str: pFileName, suffix: ".opus"))
898 {
899 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s.opus", pFileName);
900 pFileName = aBuf;
901 }
902 std::shared_ptr<CEditorSound> pSound = pEditor->m_Map.m_vpSounds[pEditor->m_SelectedSound];
903 IOHANDLE File = pEditor->Storage()->OpenFile(pFilename: pFileName, Flags: IOFLAG_WRITE, Type: StorageType);
904
905 if(File)
906 {
907 io_write(io: File, buffer: pSound->m_pData, size: pSound->m_DataSize);
908 io_close(io: File);
909 pEditor->OnDialogClose();
910 pEditor->m_Dialog = DIALOG_NONE;
911 return true;
912 }
913 pEditor->ShowFileDialogError(pFormat: "Failed to open file '%s'.", pFileName);
914 return false;
915}
916
917void CEditor::DoAudioPreview(CUIRect View, const void *pPlayPauseButtonId, const void *pStopButtonId, const void *pSeekBarId, const int SampleId)
918{
919 CUIRect Button, SeekBar;
920 // play/pause button
921 {
922 View.VSplitLeft(Cut: View.h, pLeft: &Button, pRight: &View);
923 if(DoButton_FontIcon(pId: pPlayPauseButtonId, pText: Sound()->IsPlaying(SampleId) ? FONT_ICON_PAUSE : FONT_ICON_PLAY, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Play/pause audio preview", Corners: IGraphics::CORNER_ALL) ||
924 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_SPACE)))
925 {
926 if(Sound()->IsPlaying(SampleId))
927 {
928 Sound()->Pause(SampleId);
929 }
930 else
931 {
932 if(SampleId != m_ToolbarPreviewSound && m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
933 Sound()->Pause(SampleId: m_ToolbarPreviewSound);
934
935 Sound()->Play(ChannelId: CSounds::CHN_GUI, SampleId, Flags: ISound::FLAG_PREVIEW);
936 }
937 }
938 }
939 // stop button
940 {
941 View.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &View);
942 View.VSplitLeft(Cut: View.h, pLeft: &Button, pRight: &View);
943 if(DoButton_FontIcon(pId: pStopButtonId, pText: FONT_ICON_STOP, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Stop audio preview", Corners: IGraphics::CORNER_ALL))
944 {
945 Sound()->Stop(SampleId);
946 }
947 }
948 // do seekbar
949 {
950 View.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &View);
951 const float Cut = std::min(a: View.w, b: 200.0f);
952 View.VSplitLeft(Cut, pLeft: &SeekBar, pRight: &View);
953 SeekBar.HMargin(Cut: 2.5f, pOtherRect: &SeekBar);
954
955 const float Rounding = 5.0f;
956
957 char aBuffer[64];
958 const float CurrentTime = Sound()->GetSampleCurrentTime(SampleId);
959 const float TotalTime = Sound()->GetSampleTotalTime(SampleId);
960
961 // draw seek bar
962 SeekBar.Draw(Color: ColorRGBA(0, 0, 0, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
963
964 // draw filled bar
965 const float Amount = CurrentTime / TotalTime;
966 CUIRect FilledBar = SeekBar;
967 FilledBar.w = 2 * Rounding + (FilledBar.w - 2 * Rounding) * Amount;
968 FilledBar.Draw(Color: ColorRGBA(1, 1, 1, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
969
970 // draw time
971 char aCurrentTime[32];
972 str_time_float(secs: CurrentTime, format: TIME_HOURS, buffer: aCurrentTime, buffer_size: sizeof(aCurrentTime));
973 char aTotalTime[32];
974 str_time_float(secs: TotalTime, format: TIME_HOURS, buffer: aTotalTime, buffer_size: sizeof(aTotalTime));
975 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s / %s", aCurrentTime, aTotalTime);
976 Ui()->DoLabel(pRect: &SeekBar, pText: aBuffer, Size: SeekBar.h * 0.70f, Align: TEXTALIGN_MC);
977
978 // do the logic
979 const bool Inside = Ui()->MouseInside(pRect: &SeekBar);
980
981 if(Ui()->CheckActiveItem(pId: pSeekBarId))
982 {
983 if(!Ui()->MouseButton(Index: 0))
984 {
985 Ui()->SetActiveItem(nullptr);
986 }
987 else
988 {
989 const float AmountSeek = clamp(val: (Ui()->MouseX() - SeekBar.x - Rounding) / (float)(SeekBar.w - 2 * Rounding), lo: 0.0f, hi: 1.0f);
990 Sound()->SetSampleCurrentTime(SampleId, Time: AmountSeek);
991 }
992 }
993 else if(Ui()->HotItem() == pSeekBarId)
994 {
995 if(Ui()->MouseButton(Index: 0))
996 Ui()->SetActiveItem(pSeekBarId);
997 }
998
999 if(Inside)
1000 Ui()->SetHotItem(pSeekBarId);
1001 }
1002}
1003
1004void CEditor::DoToolbarLayers(CUIRect ToolBar)
1005{
1006 const bool ModPressed = Input()->ModifierIsPressed();
1007 const bool ShiftPressed = Input()->ShiftIsPressed();
1008
1009 // handle shortcut for info button
1010 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_I) && ModPressed && !ShiftPressed)
1011 {
1012 if(m_ShowTileInfo == SHOW_TILE_HEXADECIMAL)
1013 m_ShowTileInfo = SHOW_TILE_DECIMAL;
1014 else if(m_ShowTileInfo != SHOW_TILE_OFF)
1015 m_ShowTileInfo = SHOW_TILE_OFF;
1016 else
1017 m_ShowTileInfo = SHOW_TILE_DECIMAL;
1018 m_ShowEnvelopePreview = SHOWENV_NONE;
1019 }
1020
1021 // handle shortcut for hex button
1022 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_I) && ModPressed && ShiftPressed)
1023 {
1024 m_ShowTileInfo = m_ShowTileInfo == SHOW_TILE_HEXADECIMAL ? SHOW_TILE_OFF : SHOW_TILE_HEXADECIMAL;
1025 m_ShowEnvelopePreview = SHOWENV_NONE;
1026 }
1027
1028 // handle shortcut for unused button
1029 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_U) && ModPressed)
1030 m_AllowPlaceUnusedTiles = !m_AllowPlaceUnusedTiles;
1031
1032 CUIRect TB_Top, TB_Bottom;
1033 CUIRect Button;
1034
1035 ToolBar.HSplitMid(pTop: &TB_Top, pBottom: &TB_Bottom, Spacing: 5.0f);
1036
1037 // top line buttons
1038 {
1039 // detail button
1040 TB_Top.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &TB_Top);
1041 static int s_HqButton = 0;
1042 if(DoButton_Editor(pId: &s_HqButton, pText: "HD", Checked: m_ShowDetail, pRect: &Button, Flags: 0, pToolTip: "[ctrl+h] Toggle High Detail") ||
1043 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_H) && ModPressed))
1044 {
1045 m_ShowDetail = !m_ShowDetail;
1046 }
1047
1048 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1049
1050 // animation button
1051 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1052 static char s_AnimateButton;
1053 if(DoButton_FontIcon(pId: &s_AnimateButton, pText: FONT_ICON_CIRCLE_PLAY, Checked: m_Animate, pRect: &Button, Flags: 0, pToolTip: "[ctrl+m] Toggle animation", Corners: IGraphics::CORNER_L) ||
1054 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_M) && ModPressed))
1055 {
1056 m_AnimateStart = time_get();
1057 m_Animate = !m_Animate;
1058 }
1059
1060 // animation settings button
1061 TB_Top.VSplitLeft(Cut: 14.0f, pLeft: &Button, pRight: &TB_Top);
1062 static char s_AnimateSettingsButton;
1063 if(DoButton_FontIcon(pId: &s_AnimateSettingsButton, pText: FONT_ICON_CIRCLE_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Change animation settings.", Corners: IGraphics::CORNER_R, FontSize: 8.0f))
1064 {
1065 m_AnimateUpdatePopup = true;
1066 static SPopupMenuId s_PopupAnimateSettingsId;
1067 Ui()->DoPopupMenu(pId: &s_PopupAnimateSettingsId, X: Button.x, Y: Button.y + Button.h, Width: 150.0f, Height: 37.0f, pContext: this, pfnFunc: PopupAnimateSettings);
1068 }
1069
1070 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1071
1072 // proof button
1073 TB_Top.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &TB_Top);
1074 static int s_ProofButton = 0;
1075 if(DoButton_Ex(pId: &s_ProofButton, pText: "Proof", Checked: MapView()->ProofMode()->IsEnabled(), pRect: &Button, Flags: 0, pToolTip: "[ctrl+p] Toggles proof borders. These borders represent what a player maximum can see.", Corners: IGraphics::CORNER_L) ||
1076 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_P) && ModPressed))
1077 {
1078 MapView()->ProofMode()->Toggle();
1079 }
1080
1081 TB_Top.VSplitLeft(Cut: 14.0f, pLeft: &Button, pRight: &TB_Top);
1082 static int s_ProofModeButton = 0;
1083 if(DoButton_FontIcon(pId: &s_ProofModeButton, pText: FONT_ICON_CIRCLE_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Select proof mode.", Corners: IGraphics::CORNER_R, FontSize: 8.0f))
1084 {
1085 static SPopupMenuId s_PopupProofModeId;
1086 Ui()->DoPopupMenu(pId: &s_PopupProofModeId, X: Button.x, Y: Button.y + Button.h, Width: 60.0f, Height: 36.0f, pContext: this, pfnFunc: PopupProofMode);
1087 }
1088
1089 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1090
1091 // zoom button
1092 TB_Top.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &TB_Top);
1093 static int s_ZoomButton = 0;
1094 if(DoButton_Editor(pId: &s_ZoomButton, pText: "Zoom", Checked: m_PreviewZoom, pRect: &Button, Flags: 0, pToolTip: "Toggles preview of how layers will be zoomed in-game"))
1095 {
1096 m_PreviewZoom = !m_PreviewZoom;
1097 }
1098
1099 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1100
1101 // grid button
1102 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1103 static int s_GridButton = 0;
1104 if(DoButton_FontIcon(pId: &s_GridButton, pText: FONT_ICON_BORDER_ALL, Checked: MapView()->MapGrid()->IsEnabled(), pRect: &Button, Flags: 0, pToolTip: "[ctrl+g] Toggle Grid", Corners: IGraphics::CORNER_L) ||
1105 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_G) && ModPressed && !ShiftPressed))
1106 {
1107 MapView()->MapGrid()->Toggle();
1108 }
1109
1110 // grid settings button
1111 TB_Top.VSplitLeft(Cut: 14.0f, pLeft: &Button, pRight: &TB_Top);
1112 static char s_GridSettingsButton;
1113 if(DoButton_FontIcon(pId: &s_GridSettingsButton, pText: FONT_ICON_CIRCLE_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Change grid settings.", Corners: IGraphics::CORNER_R, FontSize: 8.0f))
1114 {
1115 MapView()->MapGrid()->DoSettingsPopup(Position: vec2(Button.x, Button.y + Button.h));
1116 }
1117
1118 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1119
1120 // zoom group
1121 TB_Top.VSplitLeft(Cut: 20.0f, pLeft: &Button, pRight: &TB_Top);
1122 static int s_ZoomOutButton = 0;
1123 if(DoButton_FontIcon(pId: &s_ZoomOutButton, pText: FONT_ICON_MINUS, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[NumPad-] Zoom out", Corners: IGraphics::CORNER_L))
1124 {
1125 MapView()->Zoom()->ChangeValue(Amount: 50.0f);
1126 }
1127
1128 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1129 static int s_ZoomNormalButton = 0;
1130 if(DoButton_FontIcon(pId: &s_ZoomNormalButton, pText: FONT_ICON_MAGNIFYING_GLASS, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[NumPad*] Zoom to normal and remove editor offset", Corners: IGraphics::CORNER_NONE))
1131 {
1132 MapView()->ResetZoom();
1133 }
1134
1135 TB_Top.VSplitLeft(Cut: 20.0f, pLeft: &Button, pRight: &TB_Top);
1136 static int s_ZoomInButton = 0;
1137 if(DoButton_FontIcon(pId: &s_ZoomInButton, pText: FONT_ICON_PLUS, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[NumPad+] Zoom in", Corners: IGraphics::CORNER_R))
1138 {
1139 MapView()->Zoom()->ChangeValue(Amount: -50.0f);
1140 }
1141
1142 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1143
1144 // undo/redo group
1145 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1146 static int s_UndoButton = 0;
1147 if(DoButton_FontIcon(pId: &s_UndoButton, pText: FONT_ICON_UNDO, Checked: m_EditorHistory.CanUndo() - 1, pRect: &Button, Flags: 0, pToolTip: "[ctrl+z] Undo last action", Corners: IGraphics::CORNER_L))
1148 {
1149 m_EditorHistory.Undo();
1150 }
1151
1152 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1153 static int s_RedoButton = 0;
1154 if(DoButton_FontIcon(pId: &s_RedoButton, pText: FONT_ICON_REDO, Checked: m_EditorHistory.CanRedo() - 1, pRect: &Button, Flags: 0, pToolTip: "[ctrl+y] Redo last action", Corners: IGraphics::CORNER_R))
1155 {
1156 m_EditorHistory.Redo();
1157 }
1158
1159 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1160
1161 // brush manipulation
1162 {
1163 int Enabled = m_pBrush->IsEmpty() ? -1 : 0;
1164
1165 // flip buttons
1166 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1167 static int s_FlipXButton = 0;
1168 if(DoButton_FontIcon(pId: &s_FlipXButton, pText: FONT_ICON_ARROWS_LEFT_RIGHT, Checked: Enabled, pRect: &Button, Flags: 0, pToolTip: "[N] Flip brush horizontal", Corners: IGraphics::CORNER_L) || (Input()->KeyPress(Key: KEY_N) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr))
1169 {
1170 for(auto &pLayer : m_pBrush->m_vpLayers)
1171 pLayer->BrushFlipX();
1172 }
1173
1174 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1175 static int s_FlipyButton = 0;
1176 if(DoButton_FontIcon(pId: &s_FlipyButton, pText: FONT_ICON_ARROWS_UP_DOWN, Checked: Enabled, pRect: &Button, Flags: 0, pToolTip: "[M] Flip brush vertical", Corners: IGraphics::CORNER_R) || (Input()->KeyPress(Key: KEY_M) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr))
1177 {
1178 for(auto &pLayer : m_pBrush->m_vpLayers)
1179 pLayer->BrushFlipY();
1180 }
1181 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1182
1183 // rotate buttons
1184 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1185 static int s_RotationAmount = 90;
1186 bool TileLayer = false;
1187 // check for tile layers in brush selection
1188 for(auto &pLayer : m_pBrush->m_vpLayers)
1189 if(pLayer->m_Type == LAYERTYPE_TILES)
1190 {
1191 TileLayer = true;
1192 s_RotationAmount = maximum(a: 90, b: (s_RotationAmount / 90) * 90);
1193 break;
1194 }
1195
1196 static int s_CcwButton = 0;
1197 if(DoButton_FontIcon(pId: &s_CcwButton, pText: FONT_ICON_ARROW_ROTATE_LEFT, Checked: Enabled, pRect: &Button, Flags: 0, pToolTip: "[R] Rotates the brush counter clockwise", Corners: IGraphics::CORNER_L) || (Input()->KeyPress(Key: KEY_R) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr))
1198 {
1199 for(auto &pLayer : m_pBrush->m_vpLayers)
1200 pLayer->BrushRotate(Amount: -s_RotationAmount / 360.0f * pi * 2);
1201 }
1202
1203 TB_Top.VSplitLeft(Cut: 30.0f, pLeft: &Button, pRight: &TB_Top);
1204 auto RotationAmountRes = UiDoValueSelector(pId: &s_RotationAmount, pRect: &Button, pLabel: "", Current: s_RotationAmount, Min: TileLayer ? 90 : 1, Max: 359, Step: TileLayer ? 90 : 1, Scale: TileLayer ? 10.0f : 2.0f, pToolTip: "Rotation of the brush in degrees. Use left mouse button to drag and change the value. Hold shift to be more precise.", IsDegree: true, IsHex: false, Corners: IGraphics::CORNER_NONE);
1205 s_RotationAmount = RotationAmountRes.m_Value;
1206
1207 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1208 static int s_CwButton = 0;
1209 if(DoButton_FontIcon(pId: &s_CwButton, pText: FONT_ICON_ARROW_ROTATE_RIGHT, Checked: Enabled, pRect: &Button, Flags: 0, pToolTip: "[T] Rotates the brush clockwise", Corners: IGraphics::CORNER_R) || (Input()->KeyPress(Key: KEY_T) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr))
1210 {
1211 for(auto &pLayer : m_pBrush->m_vpLayers)
1212 pLayer->BrushRotate(Amount: s_RotationAmount / 360.0f * pi * 2);
1213 }
1214 }
1215
1216 // Color pipette and palette
1217 {
1218 const float PipetteButtonWidth = 30.0f;
1219 const float ColorPickerButtonWidth = 20.0f;
1220 const float Spacing = 2.0f;
1221 const size_t NumColorsShown = clamp<int>(val: round_to_int(f: (TB_Top.w - PipetteButtonWidth - 40.0f) / (ColorPickerButtonWidth + Spacing)), lo: 1, hi: std::size(m_aSavedColors));
1222
1223 CUIRect ColorPalette;
1224 TB_Top.VSplitRight(Cut: NumColorsShown * (ColorPickerButtonWidth + Spacing) + PipetteButtonWidth, pLeft: &TB_Top, pRight: &ColorPalette);
1225
1226 // Pipette button
1227 static char s_PipetteButton;
1228 ColorPalette.VSplitLeft(Cut: PipetteButtonWidth, pLeft: &Button, pRight: &ColorPalette);
1229 ColorPalette.VSplitLeft(Cut: Spacing, pLeft: nullptr, pRight: &ColorPalette);
1230 if(DoButton_FontIcon(pId: &s_PipetteButton, pText: FONT_ICON_EYE_DROPPER, Checked: m_ColorPipetteActive ? 1 : 0, pRect: &Button, Flags: 0, pToolTip: "[Ctrl+Shift+C] Color pipette. Pick a color from the screen by clicking on it.", Corners: IGraphics::CORNER_ALL) ||
1231 (CLineInput::GetActiveInput() == nullptr && ModPressed && ShiftPressed && Input()->KeyPress(Key: KEY_C)))
1232 {
1233 m_ColorPipetteActive = !m_ColorPipetteActive;
1234 }
1235
1236 // Palette color pickers
1237 for(size_t i = 0; i < NumColorsShown; ++i)
1238 {
1239 ColorPalette.VSplitLeft(Cut: ColorPickerButtonWidth, pLeft: &Button, pRight: &ColorPalette);
1240 ColorPalette.VSplitLeft(Cut: Spacing, pLeft: nullptr, pRight: &ColorPalette);
1241 const auto &&SetColor = [&](ColorRGBA NewColor) {
1242 m_aSavedColors[i] = NewColor;
1243 };
1244 DoColorPickerButton(pId: &m_aSavedColors[i], pRect: &Button, Color: m_aSavedColors[i], SetColor);
1245 }
1246 }
1247 }
1248
1249 // Bottom line buttons
1250 {
1251 // refocus button
1252 {
1253 TB_Bottom.VSplitLeft(Cut: 50.0f, pLeft: &Button, pRight: &TB_Bottom);
1254 static int s_RefocusButton = 0;
1255 int FocusButtonChecked = MapView()->IsFocused() ? -1 : 1;
1256 if(DoButton_Editor(pId: &s_RefocusButton, pText: "Refocus", Checked: FocusButtonChecked, pRect: &Button, Flags: 0, pToolTip: "[HOME] Restore map focus") || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_HOME)))
1257 MapView()->Focus();
1258 TB_Bottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Bottom);
1259 }
1260
1261 // tile manipulation
1262 {
1263 // do tele/tune/switch/speedup button
1264 {
1265 std::shared_ptr<CLayerTiles> pS = std::static_pointer_cast<CLayerTiles>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_TILES));
1266 if(pS)
1267 {
1268 const char *pButtonName = nullptr;
1269 CUi::FPopupMenuFunction pfnPopupFunc = nullptr;
1270 int Rows = 0;
1271 if(pS == m_Map.m_pSwitchLayer)
1272 {
1273 pButtonName = "Switch";
1274 pfnPopupFunc = PopupSwitch;
1275 Rows = 3;
1276 }
1277 else if(pS == m_Map.m_pSpeedupLayer)
1278 {
1279 pButtonName = "Speedup";
1280 pfnPopupFunc = PopupSpeedup;
1281 Rows = 3;
1282 }
1283 else if(pS == m_Map.m_pTuneLayer)
1284 {
1285 pButtonName = "Tune";
1286 pfnPopupFunc = PopupTune;
1287 Rows = 1;
1288 }
1289 else if(pS == m_Map.m_pTeleLayer)
1290 {
1291 pButtonName = "Tele";
1292 pfnPopupFunc = PopupTele;
1293 Rows = 3;
1294 }
1295
1296 if(pButtonName != nullptr)
1297 {
1298 static char s_aButtonTooltip[64];
1299 str_format(buffer: s_aButtonTooltip, buffer_size: sizeof(s_aButtonTooltip), format: "[ctrl+t] %s", pButtonName);
1300
1301 TB_Bottom.VSplitLeft(Cut: 60.0f, pLeft: &Button, pRight: &TB_Bottom);
1302 static int s_ModifierButton = 0;
1303 if(DoButton_Ex(pId: &s_ModifierButton, pText: pButtonName, Checked: 0, pRect: &Button, Flags: 0, pToolTip: s_aButtonTooltip, Corners: IGraphics::CORNER_ALL) || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && ModPressed && Input()->KeyPress(Key: KEY_T)))
1304 {
1305 static SPopupMenuId s_PopupModifierId;
1306 if(!Ui()->IsPopupOpen(pId: &s_PopupModifierId))
1307 {
1308 Ui()->DoPopupMenu(pId: &s_PopupModifierId, X: Button.x, Y: Button.y + Button.h, Width: 120, Height: 10.0f + Rows * 13.0f, pContext: this, pfnFunc: pfnPopupFunc);
1309 }
1310 }
1311 TB_Bottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Bottom);
1312 }
1313 }
1314 }
1315 }
1316
1317 // do add quad/sound button
1318 std::shared_ptr<CLayer> pLayer = GetSelectedLayer(Index: 0);
1319 if(pLayer && (pLayer->m_Type == LAYERTYPE_QUADS || pLayer->m_Type == LAYERTYPE_SOUNDS))
1320 {
1321 TB_Bottom.VSplitLeft(Cut: 60.0f, pLeft: &Button, pRight: &TB_Bottom);
1322
1323 bool Invoked = false;
1324 static int s_AddItemButton = 0;
1325
1326 if(pLayer->m_Type == LAYERTYPE_QUADS)
1327 {
1328 Invoked = DoButton_Editor(pId: &s_AddItemButton, pText: "Add Quad", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[ctrl+q] Add a new quad") ||
1329 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_Q) && ModPressed);
1330 }
1331 else if(pLayer->m_Type == LAYERTYPE_SOUNDS)
1332 {
1333 Invoked = DoButton_Editor(pId: &s_AddItemButton, pText: "Add Sound", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[ctrl+q] Add a new sound source") ||
1334 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_Q) && ModPressed);
1335 }
1336
1337 if(Invoked)
1338 {
1339 std::shared_ptr<CLayerGroup> pGroup = GetSelectedGroup();
1340
1341 float aMapping[4];
1342 pGroup->Mapping(pPoints: aMapping);
1343 int x = aMapping[0] + (aMapping[2] - aMapping[0]) / 2;
1344 int y = aMapping[1] + (aMapping[3] - aMapping[1]) / 2;
1345 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_Q) && ModPressed)
1346 {
1347 x += Ui()->MouseWorldX() - (MapView()->GetWorldOffset().x * pGroup->m_ParallaxX / 100) - pGroup->m_OffsetX;
1348 y += Ui()->MouseWorldY() - (MapView()->GetWorldOffset().y * pGroup->m_ParallaxY / 100) - pGroup->m_OffsetY;
1349 }
1350
1351 if(pLayer->m_Type == LAYERTYPE_QUADS)
1352 m_EditorHistory.Execute(pAction: std::make_shared<CEditorActionNewEmptyQuad>(args: this, args&: m_SelectedGroup, args&: m_vSelectedLayers[0], args&: x, args&: y));
1353 else if(pLayer->m_Type == LAYERTYPE_SOUNDS)
1354 m_EditorHistory.Execute(pAction: std::make_shared<CEditorActionNewEmptySound>(args: this, args&: m_SelectedGroup, args&: m_vSelectedLayers[0], args&: x, args&: y));
1355 }
1356 TB_Bottom.VSplitLeft(Cut: 5.0f, pLeft: &Button, pRight: &TB_Bottom);
1357 }
1358
1359 // Brush draw mode button
1360 {
1361 TB_Bottom.VSplitLeft(Cut: 65.0f, pLeft: &Button, pRight: &TB_Bottom);
1362 static int s_BrushDrawModeButton = 0;
1363 if(DoButton_Editor(pId: &s_BrushDrawModeButton, pText: "Destructive", Checked: m_BrushDrawDestructive, pRect: &Button, Flags: 0, pToolTip: "[ctrl+d] Toggle brush draw mode") ||
1364 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_D) && ModPressed && !ShiftPressed))
1365 m_BrushDrawDestructive = !m_BrushDrawDestructive;
1366 TB_Bottom.VSplitLeft(Cut: 5.0f, pLeft: &Button, pRight: &TB_Bottom);
1367 }
1368 }
1369}
1370
1371void CEditor::DoToolbarSounds(CUIRect ToolBar)
1372{
1373 CUIRect ToolBarTop, ToolBarBottom;
1374 ToolBar.HSplitMid(pTop: &ToolBarTop, pBottom: &ToolBarBottom, Spacing: 5.0f);
1375
1376 if(m_SelectedSound >= 0 && (size_t)m_SelectedSound < m_Map.m_vpSounds.size())
1377 {
1378 const std::shared_ptr<CEditorSound> pSelectedSound = m_Map.m_vpSounds[m_SelectedSound];
1379 if(pSelectedSound->m_SoundId != m_ToolbarPreviewSound && m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
1380 Sound()->Stop(SampleId: m_ToolbarPreviewSound);
1381 m_ToolbarPreviewSound = pSelectedSound->m_SoundId;
1382
1383 static int s_PlayPauseButton, s_StopButton, s_SeekBar = 0;
1384 DoAudioPreview(View: ToolBarBottom, pPlayPauseButtonId: &s_PlayPauseButton, pStopButtonId: &s_StopButton, pSeekBarId: &s_SeekBar, SampleId: m_ToolbarPreviewSound);
1385 }
1386 else
1387 m_ToolbarPreviewSound = -1;
1388}
1389
1390static void Rotate(const CPoint *pCenter, CPoint *pPoint, float Rotation)
1391{
1392 int x = pPoint->x - pCenter->x;
1393 int y = pPoint->y - pCenter->y;
1394 pPoint->x = (int)(x * std::cos(x: Rotation) - y * std::sin(x: Rotation) + pCenter->x);
1395 pPoint->y = (int)(x * std::sin(x: Rotation) + y * std::cos(x: Rotation) + pCenter->y);
1396}
1397
1398void CEditor::DoSoundSource(int LayerIndex, CSoundSource *pSource, int Index)
1399{
1400 void *pId = &pSource->m_Position;
1401
1402 static ESoundSourceOp s_Operation = ESoundSourceOp::OP_NONE;
1403
1404 float wx = Ui()->MouseWorldX();
1405 float wy = Ui()->MouseWorldY();
1406
1407 float CenterX = fx2f(v: pSource->m_Position.x);
1408 float CenterY = fx2f(v: pSource->m_Position.y);
1409
1410 float dx = (CenterX - wx) / m_MouseWScale;
1411 float dy = (CenterY - wy) / m_MouseWScale;
1412 if(dx * dx + dy * dy < 50)
1413 Ui()->SetHotItem(pId);
1414
1415 const bool IgnoreGrid = Input()->AltIsPressed();
1416 static CSoundSourceOperationTracker s_Tracker(this);
1417
1418 if(s_Operation == ESoundSourceOp::OP_NONE)
1419 {
1420 if(!Ui()->MouseButton(Index: 0))
1421 s_Tracker.End();
1422 }
1423
1424 if(Ui()->CheckActiveItem(pId))
1425 {
1426 if(s_Operation != ESoundSourceOp::OP_NONE)
1427 {
1428 s_Tracker.Begin(pSource, Operation: s_Operation, LayerIndex);
1429 }
1430
1431 if(m_MouseDeltaWx * m_MouseDeltaWx + m_MouseDeltaWy * m_MouseDeltaWy > 0.0f)
1432 {
1433 if(s_Operation == ESoundSourceOp::OP_MOVE)
1434 {
1435 float x = wx;
1436 float y = wy;
1437 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
1438 MapView()->MapGrid()->SnapToGrid(x, y);
1439 pSource->m_Position.x = f2fx(v: x);
1440 pSource->m_Position.y = f2fx(v: y);
1441 }
1442 }
1443
1444 if(s_Operation == ESoundSourceOp::OP_CONTEXT_MENU)
1445 {
1446 if(!Ui()->MouseButton(Index: 1))
1447 {
1448 if(m_vSelectedLayers.size() == 1)
1449 {
1450 static SPopupMenuId s_PopupSourceId;
1451 Ui()->DoPopupMenu(pId: &s_PopupSourceId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 200, pContext: this, pfnFunc: PopupSource);
1452 Ui()->DisableMouseLock();
1453 }
1454 s_Operation = ESoundSourceOp::OP_NONE;
1455 Ui()->SetActiveItem(nullptr);
1456 }
1457 }
1458 else
1459 {
1460 if(!Ui()->MouseButton(Index: 0))
1461 {
1462 Ui()->DisableMouseLock();
1463 s_Operation = ESoundSourceOp::OP_NONE;
1464 Ui()->SetActiveItem(nullptr);
1465 }
1466 }
1467
1468 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1469 }
1470 else if(Ui()->HotItem() == pId)
1471 {
1472 ms_pUiGotContext = pId;
1473
1474 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1475 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold alt to ignore grid.");
1476
1477 if(Ui()->MouseButton(Index: 0))
1478 {
1479 s_Operation = ESoundSourceOp::OP_MOVE;
1480
1481 Ui()->SetActiveItem(pId);
1482 m_SelectedSource = Index;
1483 }
1484
1485 if(Ui()->MouseButton(Index: 1))
1486 {
1487 m_SelectedSource = Index;
1488 s_Operation = ESoundSourceOp::OP_CONTEXT_MENU;
1489 Ui()->SetActiveItem(pId);
1490 }
1491 }
1492 else
1493 {
1494 Graphics()->SetColor(r: 0, g: 1, b: 0, a: 1);
1495 }
1496
1497 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * m_MouseWScale, 5.0f * m_MouseWScale);
1498 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1499}
1500
1501void CEditor::PreparePointDrag(const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex)
1502{
1503 m_QuadDragOriginalPoints[QuadIndex][PointIndex] = pQuad->m_aPoints[PointIndex];
1504}
1505
1506void CEditor::DoPointDrag(const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, int OffsetX, int OffsetY)
1507{
1508 pQuad->m_aPoints[PointIndex].x = m_QuadDragOriginalPoints[QuadIndex][PointIndex].x + OffsetX;
1509 pQuad->m_aPoints[PointIndex].y = m_QuadDragOriginalPoints[QuadIndex][PointIndex].y + OffsetY;
1510}
1511
1512CEditor::EAxis CEditor::GetDragAxis(int OffsetX, int OffsetY) const
1513{
1514 if(Input()->ShiftIsPressed())
1515 if(absolute(a: OffsetX) < absolute(a: OffsetY))
1516 return EAxis::AXIS_Y;
1517 else
1518 return EAxis::AXIS_X;
1519 else
1520 return EAxis::AXIS_NONE;
1521}
1522
1523void CEditor::DrawAxis(EAxis Axis, CPoint &OriginalPoint, CPoint &Point) const
1524{
1525 if(Axis == EAxis::AXIS_NONE)
1526 return;
1527
1528 Graphics()->SetColor(r: 1, g: 0, b: 0.1f, a: 1);
1529 if(Axis == EAxis::AXIS_X)
1530 {
1531 IGraphics::CQuadItem Line(fx2f(v: OriginalPoint.x + Point.x) / 2.0f, fx2f(v: OriginalPoint.y), fx2f(v: Point.x - OriginalPoint.x), 1.0f * m_MouseWScale);
1532 Graphics()->QuadsDraw(pArray: &Line, Num: 1);
1533 }
1534 else if(Axis == EAxis::AXIS_Y)
1535 {
1536 IGraphics::CQuadItem Line(fx2f(v: OriginalPoint.x), fx2f(v: OriginalPoint.y + Point.y) / 2.0f, 1.0f * m_MouseWScale, fx2f(v: Point.y - OriginalPoint.y));
1537 Graphics()->QuadsDraw(pArray: &Line, Num: 1);
1538 }
1539
1540 // Draw ghost of original point
1541 IGraphics::CQuadItem QuadItem(fx2f(v: OriginalPoint.x), fx2f(v: OriginalPoint.y), 5.0f * m_MouseWScale, 5.0f * m_MouseWScale);
1542 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1543}
1544
1545void CEditor::ComputePointAlignments(const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, int OffsetX, int OffsetY, std::vector<SAlignmentInfo> &vAlignments, bool Append) const
1546{
1547 if(!Append)
1548 vAlignments.clear();
1549 if(!g_Config.m_EdAlignQuads)
1550 return;
1551
1552 bool GridEnabled = MapView()->MapGrid()->IsEnabled() && !Input()->AltIsPressed();
1553
1554 // Perform computation from the original position of this point
1555 int Threshold = f2fx(v: maximum(a: 5.0f, b: 10.0f * m_MouseWScale));
1556 CPoint OrigPoint = m_QuadDragOriginalPoints.at(k: QuadIndex)[PointIndex];
1557 // Get the "current" point by applying the offset
1558 CPoint Point = OrigPoint + ivec2(OffsetX, OffsetY);
1559
1560 // Save smallest diff on both axis to only keep closest alignments
1561 int SmallestDiffX = Threshold + 1, SmallestDiffY = Threshold + 1;
1562 // Store both axis alignments in separate vectors
1563 std::vector<SAlignmentInfo> vAlignmentsX, vAlignmentsY;
1564
1565 // Check if we can align/snap to a specific point
1566 auto &&CheckAlignment = [&](CPoint *pQuadPoint) {
1567 int DX = pQuadPoint->x - Point.x;
1568 int DY = pQuadPoint->y - Point.y;
1569 int DiffX = absolute(a: DX);
1570 int DiffY = absolute(a: DY);
1571
1572 if(DiffX <= Threshold && (!GridEnabled || DiffX == 0))
1573 {
1574 // Only store alignments that have the smallest difference
1575 if(DiffX < SmallestDiffX)
1576 {
1577 vAlignmentsX.clear();
1578 SmallestDiffX = DiffX;
1579 }
1580
1581 // We can have multiple alignments having the same difference/distance
1582 if(DiffX == SmallestDiffX)
1583 {
1584 vAlignmentsX.push_back(x: SAlignmentInfo{
1585 .m_AlignedPoint: *pQuadPoint, // Aligned point
1586 {.m_X: OrigPoint.y}, // Value that can change (which is not snapped), original position
1587 .m_Axis: EAxis::AXIS_Y, // The alignment axis
1588 .m_PointIndex: PointIndex, // The index of the point
1589 .m_Diff: DX,
1590 });
1591 }
1592 }
1593
1594 if(DiffY <= Threshold && (!GridEnabled || DiffY == 0))
1595 {
1596 // Only store alignments that have the smallest difference
1597 if(DiffY < SmallestDiffY)
1598 {
1599 vAlignmentsY.clear();
1600 SmallestDiffY = DiffY;
1601 }
1602
1603 if(DiffY == SmallestDiffY)
1604 {
1605 vAlignmentsY.push_back(x: SAlignmentInfo{
1606 .m_AlignedPoint: *pQuadPoint,
1607 {.m_X: OrigPoint.x},
1608 .m_Axis: EAxis::AXIS_X,
1609 .m_PointIndex: PointIndex,
1610 .m_Diff: DY,
1611 });
1612 }
1613 }
1614 };
1615
1616 // Iterate through all the quads of the current layer
1617 // Check alignment with each point of the quad (corners & pivot)
1618 // Compute an AABB (Axis Aligned Bounding Box) to get the center of the quad
1619 // Check alignment with the center of the quad
1620 for(size_t i = 0; i < pLayer->m_vQuads.size(); i++)
1621 {
1622 auto *pCurrentQuad = &pLayer->m_vQuads[i];
1623 CPoint Min = pCurrentQuad->m_aPoints[0];
1624 CPoint Max = pCurrentQuad->m_aPoints[0];
1625
1626 for(int v = 0; v < 5; v++)
1627 {
1628 CPoint *pQuadPoint = &pCurrentQuad->m_aPoints[v];
1629
1630 if(v != 4)
1631 { // Don't use pivot to compute AABB
1632 if(pQuadPoint->x < Min.x)
1633 Min.x = pQuadPoint->x;
1634 if(pQuadPoint->y < Min.y)
1635 Min.y = pQuadPoint->y;
1636 if(pQuadPoint->x > Max.x)
1637 Max.x = pQuadPoint->x;
1638 if(pQuadPoint->y > Max.y)
1639 Max.y = pQuadPoint->y;
1640 }
1641
1642 // Don't check alignment with current point
1643 if(pQuadPoint == &pQuad->m_aPoints[PointIndex])
1644 continue;
1645
1646 // Don't check alignment with other selected points
1647 bool IsCurrentPointSelected = IsQuadSelected(Index: i) && (IsQuadCornerSelected(Index: v) || (v == PointIndex && PointIndex == 4));
1648 if(IsCurrentPointSelected)
1649 continue;
1650
1651 CheckAlignment(pQuadPoint);
1652 }
1653
1654 // Don't check alignment with center of selected quads
1655 if(!IsQuadSelected(Index: i))
1656 {
1657 CPoint Center = (Min + Max) / 2.0f;
1658 CheckAlignment(&Center);
1659 }
1660 }
1661
1662 // Finally concatenate both alignment vectors into the output
1663 vAlignments.reserve(n: vAlignmentsX.size() + vAlignmentsY.size());
1664 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsX.begin(), last: vAlignmentsX.end());
1665 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsY.begin(), last: vAlignmentsY.end());
1666}
1667
1668void CEditor::ComputePointsAlignments(const std::shared_ptr<CLayerQuads> &pLayer, bool Pivot, int OffsetX, int OffsetY, std::vector<SAlignmentInfo> &vAlignments) const
1669{
1670 // This method is used to compute alignments from selected points
1671 // and only apply the closest alignment on X and Y to the offset.
1672
1673 vAlignments.clear();
1674 std::vector<SAlignmentInfo> vAllAlignments;
1675
1676 for(int Selected : m_vSelectedQuads)
1677 {
1678 CQuad *pQuad = &pLayer->m_vQuads[Selected];
1679
1680 if(!Pivot)
1681 {
1682 for(int m = 0; m < 4; m++)
1683 {
1684 if(IsQuadPointSelected(QuadIndex: Selected, Index: m))
1685 {
1686 ComputePointAlignments(pLayer, pQuad, QuadIndex: Selected, PointIndex: m, OffsetX, OffsetY, vAlignments&: vAllAlignments, Append: true);
1687 }
1688 }
1689 }
1690 else
1691 {
1692 ComputePointAlignments(pLayer, pQuad, QuadIndex: Selected, PointIndex: 4, OffsetX, OffsetY, vAlignments&: vAllAlignments, Append: true);
1693 }
1694 }
1695
1696 int SmallestDiffX, SmallestDiffY;
1697 SmallestDiffX = SmallestDiffY = std::numeric_limits<int>::max();
1698
1699 std::vector<SAlignmentInfo> vAlignmentsX, vAlignmentsY;
1700
1701 for(const auto &Alignment : vAllAlignments)
1702 {
1703 int AbsDiff = absolute(a: Alignment.m_Diff);
1704 if(Alignment.m_Axis == EAxis::AXIS_X)
1705 {
1706 if(AbsDiff < SmallestDiffY)
1707 {
1708 SmallestDiffY = AbsDiff;
1709 vAlignmentsY.clear();
1710 }
1711 if(AbsDiff == SmallestDiffY)
1712 vAlignmentsY.emplace_back(args: Alignment);
1713 }
1714 else if(Alignment.m_Axis == EAxis::AXIS_Y)
1715 {
1716 if(AbsDiff < SmallestDiffX)
1717 {
1718 SmallestDiffX = AbsDiff;
1719 vAlignmentsX.clear();
1720 }
1721 if(AbsDiff == SmallestDiffX)
1722 vAlignmentsX.emplace_back(args: Alignment);
1723 }
1724 }
1725
1726 vAlignments.reserve(n: vAlignmentsX.size() + vAlignmentsY.size());
1727 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsX.begin(), last: vAlignmentsX.end());
1728 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsY.begin(), last: vAlignmentsY.end());
1729}
1730
1731void CEditor::ComputeAABBAlignments(const std::shared_ptr<CLayerQuads> &pLayer, const SAxisAlignedBoundingBox &AABB, int OffsetX, int OffsetY, std::vector<SAlignmentInfo> &vAlignments) const
1732{
1733 vAlignments.clear();
1734 if(!g_Config.m_EdAlignQuads)
1735 return;
1736
1737 // This method is a bit different than the point alignment in the way where instead of trying to aling 1 point to all quads,
1738 // we try to align 5 points to all quads, these 5 points being 5 points of an AABB.
1739 // Otherwise, the concept is the same, we use the original position of the AABB to make the computations.
1740 int Threshold = f2fx(v: maximum(a: 5.0f, b: 10.0f * m_MouseWScale));
1741 int SmallestDiffX = Threshold + 1, SmallestDiffY = Threshold + 1;
1742 std::vector<SAlignmentInfo> vAlignmentsX, vAlignmentsY;
1743
1744 bool GridEnabled = MapView()->MapGrid()->IsEnabled() && !Input()->AltIsPressed();
1745
1746 auto &&CheckAlignment = [&](CPoint &Aligned, int Point) {
1747 CPoint ToCheck = AABB.m_aPoints[Point] + ivec2(OffsetX, OffsetY);
1748 int DX = Aligned.x - ToCheck.x;
1749 int DY = Aligned.y - ToCheck.y;
1750 int DiffX = absolute(a: DX);
1751 int DiffY = absolute(a: DY);
1752
1753 if(DiffX <= Threshold && (!GridEnabled || DiffX == 0))
1754 {
1755 if(DiffX < SmallestDiffX)
1756 {
1757 SmallestDiffX = DiffX;
1758 vAlignmentsX.clear();
1759 }
1760
1761 if(DiffX == SmallestDiffX)
1762 {
1763 vAlignmentsX.push_back(x: SAlignmentInfo{
1764 .m_AlignedPoint: Aligned,
1765 {.m_X: AABB.m_aPoints[Point].y},
1766 .m_Axis: EAxis::AXIS_Y,
1767 .m_PointIndex: Point,
1768 .m_Diff: DX,
1769 });
1770 }
1771 }
1772
1773 if(DiffY <= Threshold && (!GridEnabled || DiffY == 0))
1774 {
1775 if(DiffY < SmallestDiffY)
1776 {
1777 SmallestDiffY = DiffY;
1778 vAlignmentsY.clear();
1779 }
1780
1781 if(DiffY == SmallestDiffY)
1782 {
1783 vAlignmentsY.push_back(x: SAlignmentInfo{
1784 .m_AlignedPoint: Aligned,
1785 {.m_X: AABB.m_aPoints[Point].x},
1786 .m_Axis: EAxis::AXIS_X,
1787 .m_PointIndex: Point,
1788 .m_Diff: DY,
1789 });
1790 }
1791 }
1792 };
1793
1794 auto &&CheckAABBAlignment = [&](CPoint &QuadMin, CPoint &QuadMax) {
1795 CPoint QuadCenter = (QuadMin + QuadMax) / 2.0f;
1796 CPoint aQuadPoints[5] = {
1797 QuadMin, // Top left
1798 {QuadMax.x, QuadMin.y}, // Top right
1799 {QuadMin.x, QuadMax.y}, // Bottom left
1800 QuadMax, // Bottom right
1801 QuadCenter,
1802 };
1803
1804 // Check all points with all the other points
1805 for(auto &QuadPoint : aQuadPoints)
1806 {
1807 // i is the quad point which is "aligned" and that we want to compare with
1808 for(int j = 0; j < 5; j++)
1809 {
1810 // j is the point we try to align
1811 CheckAlignment(QuadPoint, j);
1812 }
1813 }
1814 };
1815
1816 // Iterate through all quads of the current layer
1817 // Compute AABB of all quads and check if the dragged AABB can be aligned to this AABB.
1818 for(size_t i = 0; i < pLayer->m_vQuads.size(); i++)
1819 {
1820 auto *pCurrentQuad = &pLayer->m_vQuads[i];
1821 if(IsQuadSelected(Index: i)) // Don't check with other selected quads
1822 continue;
1823
1824 // Get AABB of this quad
1825 CPoint QuadMin = pCurrentQuad->m_aPoints[0], QuadMax = pCurrentQuad->m_aPoints[0];
1826 for(int v = 1; v < 4; v++)
1827 {
1828 QuadMin.x = minimum(a: QuadMin.x, b: pCurrentQuad->m_aPoints[v].x);
1829 QuadMin.y = minimum(a: QuadMin.y, b: pCurrentQuad->m_aPoints[v].y);
1830 QuadMax.x = maximum(a: QuadMax.x, b: pCurrentQuad->m_aPoints[v].x);
1831 QuadMax.y = maximum(a: QuadMax.y, b: pCurrentQuad->m_aPoints[v].y);
1832 }
1833
1834 CheckAABBAlignment(QuadMin, QuadMax);
1835 }
1836
1837 // Finally, concatenate both alignment vectors into the output
1838 vAlignments.reserve(n: vAlignmentsX.size() + vAlignmentsY.size());
1839 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsX.begin(), last: vAlignmentsX.end());
1840 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsY.begin(), last: vAlignmentsY.end());
1841}
1842
1843void CEditor::DrawPointAlignments(const std::vector<SAlignmentInfo> &vAlignments, int OffsetX, int OffsetY) const
1844{
1845 if(!g_Config.m_EdAlignQuads)
1846 return;
1847
1848 // Drawing an alignment is easy, we convert fixed to float for the aligned point coords
1849 // and we also convert the "changing" value after applying the offset (which might be edited to actually align the value with the alignment).
1850 Graphics()->SetColor(r: 1, g: 0, b: 0.1f, a: 1);
1851 for(const SAlignmentInfo &Alignment : vAlignments)
1852 {
1853 // We don't use IGraphics::CLineItem to draw because we don't want to stop QuadsBegin(), quads work just fine.
1854 if(Alignment.m_Axis == EAxis::AXIS_X)
1855 { // Alignment on X axis is same Y values but different X values
1856 IGraphics::CQuadItem Line(fx2f(v: Alignment.m_AlignedPoint.x), fx2f(v: Alignment.m_AlignedPoint.y), fx2f(v: Alignment.m_X + OffsetX - Alignment.m_AlignedPoint.x), 1.0f * m_MouseWScale);
1857 Graphics()->QuadsDrawTL(pArray: &Line, Num: 1);
1858 }
1859 else if(Alignment.m_Axis == EAxis::AXIS_Y)
1860 { // Alignment on Y axis is same X values but different Y values
1861 IGraphics::CQuadItem Line(fx2f(v: Alignment.m_AlignedPoint.x), fx2f(v: Alignment.m_AlignedPoint.y), 1.0f * m_MouseWScale, fx2f(v: Alignment.m_Y + OffsetY - Alignment.m_AlignedPoint.y));
1862 Graphics()->QuadsDrawTL(pArray: &Line, Num: 1);
1863 }
1864 }
1865}
1866
1867void CEditor::DrawAABB(const SAxisAlignedBoundingBox &AABB, int OffsetX, int OffsetY) const
1868{
1869 // Drawing an AABB is simply converting the points from fixed to float
1870 // Then making lines out of quads and drawing them
1871 vec2 TL = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TL].x + OffsetX), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TL].y + OffsetY)};
1872 vec2 TR = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TR].x + OffsetX), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TR].y + OffsetY)};
1873 vec2 BL = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BL].x + OffsetX), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BL].y + OffsetY)};
1874 vec2 BR = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BR].x + OffsetX), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BR].y + OffsetY)};
1875 vec2 Center = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_CENTER].x + OffsetX), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_CENTER].y + OffsetY)};
1876
1877 // We don't use IGraphics::CLineItem to draw because we don't want to stop QuadsBegin(), quads work just fine.
1878 IGraphics::CQuadItem Lines[4] = {
1879 {TL.x, TL.y, TR.x - TL.x, 1.0f * m_MouseWScale},
1880 {TL.x, TL.y, 1.0f * m_MouseWScale, BL.y - TL.y},
1881 {TR.x, TR.y, 1.0f * m_MouseWScale, BR.y - TR.y},
1882 {BL.x, BL.y, BR.x - BL.x, 1.0f * m_MouseWScale},
1883 };
1884 Graphics()->SetColor(r: 1, g: 0, b: 1, a: 1);
1885 Graphics()->QuadsDrawTL(pArray: Lines, Num: 4);
1886
1887 IGraphics::CQuadItem CenterQuad(Center.x, Center.y, 5.0f * m_MouseWScale, 5.0f * m_MouseWScale);
1888 Graphics()->QuadsDraw(pArray: &CenterQuad, Num: 1);
1889}
1890
1891void CEditor::QuadSelectionAABB(const std::shared_ptr<CLayerQuads> &pLayer, SAxisAlignedBoundingBox &OutAABB)
1892{
1893 // Compute an englobing AABB of the current selection of quads
1894 CPoint Min{
1895 std::numeric_limits<int>::max(),
1896 std::numeric_limits<int>::max(),
1897 };
1898 CPoint Max{
1899 std::numeric_limits<int>::min(),
1900 std::numeric_limits<int>::min(),
1901 };
1902 for(int Selected : m_vSelectedQuads)
1903 {
1904 CQuad *pQuad = &pLayer->m_vQuads[Selected];
1905 for(int i = 0; i < 4; i++)
1906 {
1907 auto *pPoint = &pQuad->m_aPoints[i];
1908 Min.x = minimum(a: Min.x, b: pPoint->x);
1909 Min.y = minimum(a: Min.y, b: pPoint->y);
1910 Max.x = maximum(a: Max.x, b: pPoint->x);
1911 Max.y = maximum(a: Max.y, b: pPoint->y);
1912 }
1913 }
1914 CPoint Center = (Min + Max) / 2.0f;
1915 CPoint aPoints[SAxisAlignedBoundingBox::NUM_POINTS] = {
1916 Min, // Top left
1917 {Max.x, Min.y}, // Top right
1918 {Min.x, Max.y}, // Bottom left
1919 Max, // Bottom right
1920 Center,
1921 };
1922 mem_copy(dest: OutAABB.m_aPoints, source: aPoints, size: sizeof(CPoint) * SAxisAlignedBoundingBox::NUM_POINTS);
1923}
1924
1925void CEditor::ApplyAlignments(const std::vector<SAlignmentInfo> &vAlignments, int &OffsetX, int &OffsetY)
1926{
1927 if(vAlignments.empty())
1928 return;
1929
1930 // Find X and Y aligment
1931 const int *pAlignedX = nullptr;
1932 const int *pAlignedY = nullptr;
1933
1934 // To Find the alignments we simply iterate through the vector of alignments and find the first
1935 // X and Y alignments.
1936 // Then, we use the saved m_Diff to adjust the offset
1937 int AdjustX = 0, AdjustY = 0;
1938 for(const SAlignmentInfo &Alignment : vAlignments)
1939 {
1940 if(Alignment.m_Axis == EAxis::AXIS_X && !pAlignedY)
1941 {
1942 pAlignedY = &Alignment.m_AlignedPoint.y;
1943 AdjustY = Alignment.m_Diff;
1944 }
1945 else if(Alignment.m_Axis == EAxis::AXIS_Y && !pAlignedX)
1946 {
1947 pAlignedX = &Alignment.m_AlignedPoint.x;
1948 AdjustX = Alignment.m_Diff;
1949 }
1950 }
1951
1952 // Adjust offset
1953 OffsetX += AdjustX;
1954 OffsetY += AdjustY;
1955}
1956
1957void CEditor::ApplyAxisAlignment(int &OffsetX, int &OffsetY) const
1958{
1959 // This is used to preserve axis alignment when pressing `Shift`
1960 // Should be called before any other computation
1961 EAxis Axis = GetDragAxis(OffsetX, OffsetY);
1962 OffsetX = ((Axis == EAxis::AXIS_NONE || Axis == EAxis::AXIS_X) ? OffsetX : 0);
1963 OffsetY = ((Axis == EAxis::AXIS_NONE || Axis == EAxis::AXIS_Y) ? OffsetY : 0);
1964}
1965
1966void CEditor::DoQuad(int LayerIndex, const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int Index)
1967{
1968 enum
1969 {
1970 OP_NONE = 0,
1971 OP_SELECT,
1972 OP_MOVE_ALL,
1973 OP_MOVE_PIVOT,
1974 OP_ROTATE,
1975 OP_CONTEXT_MENU,
1976 OP_DELETE,
1977 };
1978
1979 // some basic values
1980 void *pId = &pQuad->m_aPoints[4]; // use pivot addr as id
1981 static std::vector<std::vector<CPoint>> s_vvRotatePoints;
1982 static int s_Operation = OP_NONE;
1983 static float s_MouseXStart = 0.0f;
1984 static float s_MouseYStart = 0.0f;
1985 static float s_RotateAngle = 0;
1986 float wx = Ui()->MouseWorldX();
1987 float wy = Ui()->MouseWorldY();
1988 static CPoint s_OriginalPosition;
1989 static std::vector<SAlignmentInfo> s_PivotAlignments; // Alignments per pivot per quad
1990 static std::vector<SAlignmentInfo> s_vAABBAlignments; // Alignments for one AABB (single quad or selection of multiple quads)
1991 static SAxisAlignedBoundingBox s_SelectionAABB; // Selection AABB
1992 static ivec2 s_LastOffset; // Last offset, stored as static so we can use it to draw every frame
1993
1994 // get pivot
1995 float CenterX = fx2f(v: pQuad->m_aPoints[4].x);
1996 float CenterY = fx2f(v: pQuad->m_aPoints[4].y);
1997
1998 const bool IgnoreGrid = Input()->AltIsPressed();
1999
2000 auto &&GetDragOffset = [&]() -> ivec2 {
2001 float x = wx;
2002 float y = wy;
2003 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
2004 MapView()->MapGrid()->SnapToGrid(x, y);
2005
2006 int OffsetX = f2fx(v: x) - s_OriginalPosition.x;
2007 int OffsetY = f2fx(v: y) - s_OriginalPosition.y;
2008
2009 return {OffsetX, OffsetY};
2010 };
2011
2012 // draw selection background
2013 if(IsQuadSelected(Index))
2014 {
2015 Graphics()->SetColor(r: 0, g: 0, b: 0, a: 1);
2016 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 7.0f * m_MouseWScale, 7.0f * m_MouseWScale);
2017 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
2018 }
2019
2020 if(Ui()->CheckActiveItem(pId))
2021 {
2022 if(m_MouseDeltaWx * m_MouseDeltaWx + m_MouseDeltaWy * m_MouseDeltaWy > 0.0f)
2023 {
2024 if(s_Operation == OP_SELECT)
2025 {
2026 float x = s_MouseXStart - Ui()->MouseX();
2027 float y = s_MouseYStart - Ui()->MouseY();
2028
2029 if(x * x + y * y > 20.0f)
2030 {
2031 if(!IsQuadSelected(Index))
2032 SelectQuad(Index);
2033
2034 s_OriginalPosition = pQuad->m_aPoints[4];
2035
2036 if(Input()->ShiftIsPressed())
2037 {
2038 s_Operation = OP_MOVE_PIVOT;
2039 // When moving, we need to save the original position of all selected pivots
2040 for(int Selected : m_vSelectedQuads)
2041 {
2042 CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
2043 PreparePointDrag(pLayer, pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: 4);
2044 }
2045 }
2046 else
2047 {
2048 s_Operation = OP_MOVE_ALL;
2049 // When moving, we need to save the original position of all selected quads points
2050 for(int Selected : m_vSelectedQuads)
2051 {
2052 CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
2053 for(size_t v = 0; v < 5; v++)
2054 PreparePointDrag(pLayer, pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: v);
2055 }
2056 // And precompute AABB of selection since it will not change during drag
2057 if(g_Config.m_EdAlignQuads)
2058 QuadSelectionAABB(pLayer, OutAABB&: s_SelectionAABB);
2059 }
2060 }
2061 }
2062
2063 // check if we only should move pivot
2064 if(s_Operation == OP_MOVE_PIVOT)
2065 {
2066 m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: m_vSelectedQuads, GroupIndex: -1, LayerIndex);
2067
2068 s_LastOffset = GetDragOffset(); // Update offset
2069 ApplyAxisAlignment(OffsetX&: s_LastOffset.x, OffsetY&: s_LastOffset.y); // Apply axis alignment to the offset
2070
2071 ComputePointsAlignments(pLayer, Pivot: true, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y, vAlignments&: s_PivotAlignments);
2072 ApplyAlignments(vAlignments: s_PivotAlignments, OffsetX&: s_LastOffset.x, OffsetY&: s_LastOffset.y);
2073
2074 for(auto &Selected : m_vSelectedQuads)
2075 {
2076 CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
2077 DoPointDrag(pLayer, pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: 4, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2078 }
2079 }
2080 else if(s_Operation == OP_MOVE_ALL)
2081 {
2082 m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: m_vSelectedQuads, GroupIndex: -1, LayerIndex);
2083
2084 // Compute drag offset
2085 s_LastOffset = GetDragOffset();
2086 ApplyAxisAlignment(OffsetX&: s_LastOffset.x, OffsetY&: s_LastOffset.y);
2087
2088 // Then compute possible alignments with the selection AABB
2089 ComputeAABBAlignments(pLayer, AABB: s_SelectionAABB, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y, vAlignments&: s_vAABBAlignments);
2090 // Apply alignments before drag
2091 ApplyAlignments(vAlignments: s_vAABBAlignments, OffsetX&: s_LastOffset.x, OffsetY&: s_LastOffset.y);
2092 // Then do the drag
2093 for(int Selected : m_vSelectedQuads)
2094 {
2095 CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
2096 for(int v = 0; v < 5; v++)
2097 DoPointDrag(pLayer, pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: v, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2098 }
2099 }
2100 else if(s_Operation == OP_ROTATE)
2101 {
2102 m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: m_vSelectedQuads, GroupIndex: -1, LayerIndex);
2103
2104 for(size_t i = 0; i < m_vSelectedQuads.size(); ++i)
2105 {
2106 CQuad *pCurrentQuad = &pLayer->m_vQuads[m_vSelectedQuads[i]];
2107 for(int v = 0; v < 4; v++)
2108 {
2109 pCurrentQuad->m_aPoints[v] = s_vvRotatePoints[i][v];
2110 Rotate(pCenter: &pCurrentQuad->m_aPoints[4], pPoint: &pCurrentQuad->m_aPoints[v], Rotation: s_RotateAngle);
2111 }
2112 }
2113
2114 if(Input()->ShiftIsPressed())
2115 s_RotateAngle += (m_MouseDeltaX)*0.0001f;
2116 else
2117 s_RotateAngle += (m_MouseDeltaX)*0.002f;
2118 }
2119 }
2120
2121 // Draw axis and aligments when moving
2122 if(s_Operation == OP_MOVE_PIVOT || s_Operation == OP_MOVE_ALL)
2123 {
2124 EAxis Axis = GetDragAxis(OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2125 DrawAxis(Axis, OriginalPoint&: s_OriginalPosition, Point&: pQuad->m_aPoints[4]);
2126
2127 str_copy(dst&: m_aTooltip, src: "Hold shift to keep alignment on one axis.");
2128 }
2129
2130 if(s_Operation == OP_MOVE_PIVOT)
2131 DrawPointAlignments(vAlignments: s_PivotAlignments, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2132
2133 if(s_Operation == OP_MOVE_ALL)
2134 {
2135 DrawPointAlignments(vAlignments: s_vAABBAlignments, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2136
2137 if(g_Config.m_EdShowQuadsRect)
2138 DrawAABB(AABB: s_SelectionAABB, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2139 }
2140
2141 if(s_Operation == OP_CONTEXT_MENU)
2142 {
2143 if(!Ui()->MouseButton(Index: 1))
2144 {
2145 if(m_vSelectedLayers.size() == 1)
2146 {
2147 m_SelectedQuadIndex = FindSelectedQuadIndex(Index);
2148
2149 static SPopupMenuId s_PopupQuadId;
2150 Ui()->DoPopupMenu(pId: &s_PopupQuadId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 198, pContext: this, pfnFunc: PopupQuad);
2151 Ui()->DisableMouseLock();
2152 }
2153 s_Operation = OP_NONE;
2154 Ui()->SetActiveItem(nullptr);
2155 }
2156 }
2157 else if(s_Operation == OP_DELETE)
2158 {
2159 if(!Ui()->MouseButton(Index: 1))
2160 {
2161 if(m_vSelectedLayers.size() == 1)
2162 {
2163 Ui()->DisableMouseLock();
2164 m_Map.OnModify();
2165 DeleteSelectedQuads();
2166 }
2167 s_Operation = OP_NONE;
2168 Ui()->SetActiveItem(nullptr);
2169 }
2170 }
2171 else if(s_Operation == OP_ROTATE)
2172 {
2173 if(Ui()->MouseButton(Index: 0))
2174 {
2175 Ui()->DisableMouseLock();
2176 s_Operation = OP_NONE;
2177 Ui()->SetActiveItem(nullptr);
2178 m_QuadTracker.EndQuadTrack();
2179 }
2180 else if(Ui()->MouseButton(Index: 1))
2181 {
2182 Ui()->DisableMouseLock();
2183 s_Operation = OP_NONE;
2184 Ui()->SetActiveItem(nullptr);
2185
2186 // Reset points to old position
2187 for(size_t i = 0; i < m_vSelectedQuads.size(); ++i)
2188 {
2189 CQuad *pCurrentQuad = &pLayer->m_vQuads[m_vSelectedQuads[i]];
2190 for(int v = 0; v < 4; v++)
2191 pCurrentQuad->m_aPoints[v] = s_vvRotatePoints[i][v];
2192 }
2193 }
2194 }
2195 else
2196 {
2197 if(!Ui()->MouseButton(Index: 0))
2198 {
2199 if(s_Operation == OP_SELECT)
2200 {
2201 if(Input()->ShiftIsPressed())
2202 ToggleSelectQuad(Index);
2203 else
2204 SelectQuad(Index);
2205 }
2206 else if(s_Operation == OP_MOVE_PIVOT || s_Operation == OP_MOVE_ALL)
2207 {
2208 m_QuadTracker.EndQuadTrack();
2209 }
2210
2211 Ui()->DisableMouseLock();
2212 s_Operation = OP_NONE;
2213 Ui()->SetActiveItem(nullptr);
2214
2215 s_LastOffset = ivec2();
2216 s_OriginalPosition = ivec2();
2217 s_vAABBAlignments.clear();
2218 s_PivotAlignments.clear();
2219 }
2220 }
2221
2222 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
2223 }
2224 else if(Input()->KeyPress(Key: KEY_R) && !m_vSelectedQuads.empty() && m_Dialog == DIALOG_NONE)
2225 {
2226 Ui()->EnableMouseLock(pId);
2227 Ui()->SetActiveItem(pId);
2228 s_Operation = OP_ROTATE;
2229 s_RotateAngle = 0;
2230
2231 s_vvRotatePoints.clear();
2232 s_vvRotatePoints.resize(new_size: m_vSelectedQuads.size());
2233 for(size_t i = 0; i < m_vSelectedQuads.size(); ++i)
2234 {
2235 CQuad *pCurrentQuad = &pLayer->m_vQuads[m_vSelectedQuads[i]];
2236
2237 s_vvRotatePoints[i].resize(new_size: 4);
2238 s_vvRotatePoints[i][0] = pCurrentQuad->m_aPoints[0];
2239 s_vvRotatePoints[i][1] = pCurrentQuad->m_aPoints[1];
2240 s_vvRotatePoints[i][2] = pCurrentQuad->m_aPoints[2];
2241 s_vvRotatePoints[i][3] = pCurrentQuad->m_aPoints[3];
2242 }
2243 }
2244 else if(Ui()->HotItem() == pId)
2245 {
2246 ms_pUiGotContext = pId;
2247
2248 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
2249 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold shift to move pivot. Hold alt to ignore grid. Hold shift and right click to delete.");
2250
2251 if(Ui()->MouseButton(Index: 0))
2252 {
2253 Ui()->SetActiveItem(pId);
2254
2255 s_MouseXStart = Ui()->MouseX();
2256 s_MouseYStart = Ui()->MouseY();
2257
2258 s_Operation = OP_SELECT;
2259 }
2260 else if(Ui()->MouseButton(Index: 1))
2261 {
2262 if(Input()->ShiftIsPressed())
2263 {
2264 s_Operation = OP_DELETE;
2265
2266 if(!IsQuadSelected(Index))
2267 SelectQuad(Index);
2268
2269 Ui()->SetActiveItem(pId);
2270 }
2271 else
2272 {
2273 s_Operation = OP_CONTEXT_MENU;
2274
2275 if(!IsQuadSelected(Index))
2276 SelectQuad(Index);
2277
2278 Ui()->SetActiveItem(pId);
2279 }
2280 }
2281 }
2282 else
2283 Graphics()->SetColor(r: 0, g: 1, b: 0, a: 1);
2284
2285 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * m_MouseWScale, 5.0f * m_MouseWScale);
2286 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
2287}
2288
2289void CEditor::DoQuadPoint(int LayerIndex, const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int QuadIndex, int V)
2290{
2291 void *pId = &pQuad->m_aPoints[V];
2292
2293 float wx = Ui()->MouseWorldX();
2294 float wy = Ui()->MouseWorldY();
2295
2296 float px = fx2f(v: pQuad->m_aPoints[V].x);
2297 float py = fx2f(v: pQuad->m_aPoints[V].y);
2298
2299 // draw selection background
2300 if(IsQuadPointSelected(QuadIndex, Index: V))
2301 {
2302 Graphics()->SetColor(r: 0, g: 0, b: 0, a: 1);
2303 IGraphics::CQuadItem QuadItem(px, py, 7.0f * m_MouseWScale, 7.0f * m_MouseWScale);
2304 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
2305 }
2306
2307 enum
2308 {
2309 OP_NONE = 0,
2310 OP_SELECT,
2311 OP_MOVEPOINT,
2312 OP_MOVEUV,
2313 OP_CONTEXT_MENU
2314 };
2315
2316 static int s_Operation = OP_NONE;
2317 static float s_MouseXStart = 0.0f;
2318 static float s_MouseYStart = 0.0f;
2319 static CPoint s_OriginalPoint;
2320 static std::vector<SAlignmentInfo> s_Alignments; // Alignments
2321 static ivec2 s_LastOffset;
2322
2323 const bool IgnoreGrid = Input()->AltIsPressed();
2324
2325 auto &&GetDragOffset = [&]() -> ivec2 {
2326 float x = wx;
2327 float y = wy;
2328 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
2329 MapView()->MapGrid()->SnapToGrid(x, y);
2330
2331 int OffsetX = f2fx(v: x) - s_OriginalPoint.x;
2332 int OffsetY = f2fx(v: y) - s_OriginalPoint.y;
2333
2334 return {OffsetX, OffsetY};
2335 };
2336
2337 if(Ui()->CheckActiveItem(pId))
2338 {
2339 if(m_MouseDeltaWx * m_MouseDeltaWx + m_MouseDeltaWy * m_MouseDeltaWy > 0.0f)
2340 {
2341 if(s_Operation == OP_SELECT)
2342 {
2343 float x = s_MouseXStart - Ui()->MouseX();
2344 float y = s_MouseYStart - Ui()->MouseY();
2345
2346 if(x * x + y * y > 20.0f)
2347 {
2348 if(!IsQuadPointSelected(QuadIndex, Index: V))
2349 SelectQuadPoint(QuadIndex, Index: V);
2350
2351 if(Input()->ShiftIsPressed())
2352 {
2353 s_Operation = OP_MOVEUV;
2354 Ui()->EnableMouseLock(pId);
2355 }
2356 else
2357 {
2358 s_Operation = OP_MOVEPOINT;
2359 // Save original positions before moving
2360 s_OriginalPoint = pQuad->m_aPoints[V];
2361 for(int Selected : m_vSelectedQuads)
2362 {
2363 for(int m = 0; m < 4; m++)
2364 if(IsQuadPointSelected(QuadIndex: Selected, Index: m))
2365 PreparePointDrag(pLayer, pQuad: &pLayer->m_vQuads[Selected], QuadIndex: Selected, PointIndex: m);
2366 }
2367 }
2368 }
2369 }
2370
2371 if(s_Operation == OP_MOVEPOINT)
2372 {
2373 m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: m_vSelectedQuads, GroupIndex: -1, LayerIndex);
2374
2375 s_LastOffset = GetDragOffset(); // Update offset
2376 ApplyAxisAlignment(OffsetX&: s_LastOffset.x, OffsetY&: s_LastOffset.y); // Apply axis alignment to offset
2377
2378 ComputePointsAlignments(pLayer, Pivot: false, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y, vAlignments&: s_Alignments);
2379 ApplyAlignments(vAlignments: s_Alignments, OffsetX&: s_LastOffset.x, OffsetY&: s_LastOffset.y);
2380
2381 for(int Selected : m_vSelectedQuads)
2382 {
2383 for(int m = 0; m < 4; m++)
2384 {
2385 if(IsQuadPointSelected(QuadIndex: Selected, Index: m))
2386 {
2387 DoPointDrag(pLayer, pQuad: &pLayer->m_vQuads[Selected], QuadIndex: Selected, PointIndex: m, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2388 }
2389 }
2390 }
2391 }
2392 else if(s_Operation == OP_MOVEUV)
2393 {
2394 int SelectedPoints = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3);
2395
2396 m_QuadTracker.BeginQuadPointPropTrack(pLayer, vSelectedQuads: m_vSelectedQuads, SelectedQuadPoints: SelectedPoints, GroupIndex: -1, LayerIndex);
2397 m_QuadTracker.AddQuadPointPropTrack(Prop: EQuadPointProp::PROP_TEX_U);
2398 m_QuadTracker.AddQuadPointPropTrack(Prop: EQuadPointProp::PROP_TEX_V);
2399
2400 for(int Selected : m_vSelectedQuads)
2401 {
2402 CQuad *pSelectedQuad = &pLayer->m_vQuads[Selected];
2403 for(int m = 0; m < 4; m++)
2404 {
2405 if(IsQuadPointSelected(QuadIndex: Selected, Index: m))
2406 {
2407 // 0,2;1,3 - line x
2408 // 0,1;2,3 - line y
2409
2410 pSelectedQuad->m_aTexcoords[m].x += f2fx(v: m_MouseDeltaWx * 0.001f);
2411 pSelectedQuad->m_aTexcoords[(m + 2) % 4].x += f2fx(v: m_MouseDeltaWx * 0.001f);
2412
2413 pSelectedQuad->m_aTexcoords[m].y += f2fx(v: m_MouseDeltaWy * 0.001f);
2414 pSelectedQuad->m_aTexcoords[m ^ 1].y += f2fx(v: m_MouseDeltaWy * 0.001f);
2415 }
2416 }
2417 }
2418 }
2419 }
2420
2421 // Draw axis and alignments when dragging
2422 if(s_Operation == OP_MOVEPOINT)
2423 {
2424 Graphics()->SetColor(r: 1, g: 0, b: 0.1f, a: 1);
2425
2426 // Axis
2427 EAxis Axis = GetDragAxis(OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2428 DrawAxis(Axis, OriginalPoint&: s_OriginalPoint, Point&: pQuad->m_aPoints[V]);
2429
2430 // Alignments
2431 DrawPointAlignments(vAlignments: s_Alignments, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2432
2433 str_copy(dst&: m_aTooltip, src: "Hold shift to keep alignment on one axis.");
2434 }
2435
2436 if(s_Operation == OP_CONTEXT_MENU)
2437 {
2438 if(!Ui()->MouseButton(Index: 1))
2439 {
2440 if(m_vSelectedLayers.size() == 1)
2441 {
2442 if(!IsQuadSelected(Index: QuadIndex))
2443 SelectQuad(Index: QuadIndex);
2444
2445 m_SelectedQuadPoint = V;
2446 m_SelectedQuadIndex = FindSelectedQuadIndex(Index: QuadIndex);
2447
2448 static SPopupMenuId s_PopupPointId;
2449 Ui()->DoPopupMenu(pId: &s_PopupPointId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 75, pContext: this, pfnFunc: PopupPoint);
2450 }
2451 Ui()->SetActiveItem(nullptr);
2452 }
2453 }
2454 else
2455 {
2456 if(!Ui()->MouseButton(Index: 0))
2457 {
2458 if(s_Operation == OP_SELECT)
2459 {
2460 if(Input()->ShiftIsPressed())
2461 ToggleSelectQuadPoint(QuadIndex, Index: V);
2462 else
2463 SelectQuadPoint(QuadIndex, Index: V);
2464 }
2465
2466 if(s_Operation == OP_MOVEPOINT)
2467 {
2468 m_QuadTracker.EndQuadTrack();
2469 }
2470 else if(s_Operation == OP_MOVEUV)
2471 {
2472 m_QuadTracker.EndQuadPointPropTrackAll();
2473 }
2474
2475 Ui()->DisableMouseLock();
2476 Ui()->SetActiveItem(nullptr);
2477 }
2478 }
2479
2480 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
2481 }
2482 else if(Ui()->HotItem() == pId)
2483 {
2484 ms_pUiGotContext = pId;
2485
2486 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
2487 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold shift to move the texture. Hold alt to ignore grid.");
2488
2489 if(Ui()->MouseButton(Index: 0))
2490 {
2491 Ui()->SetActiveItem(pId);
2492
2493 s_MouseXStart = Ui()->MouseX();
2494 s_MouseYStart = Ui()->MouseY();
2495
2496 s_Operation = OP_SELECT;
2497 }
2498 else if(Ui()->MouseButton(Index: 1))
2499 {
2500 s_Operation = OP_CONTEXT_MENU;
2501
2502 Ui()->SetActiveItem(pId);
2503
2504 if(!IsQuadPointSelected(QuadIndex, Index: V))
2505 SelectQuadPoint(QuadIndex, Index: V);
2506 }
2507 }
2508 else
2509 Graphics()->SetColor(r: 1, g: 0, b: 0, a: 1);
2510
2511 IGraphics::CQuadItem QuadItem(px, py, 5.0f * m_MouseWScale, 5.0f * m_MouseWScale);
2512 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
2513}
2514
2515float CEditor::TriangleArea(vec2 A, vec2 B, vec2 C)
2516{
2517 return absolute(a: ((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) * 0.5f);
2518}
2519
2520bool CEditor::IsInTriangle(vec2 Point, vec2 A, vec2 B, vec2 C)
2521{
2522 // Normalize to increase precision
2523 vec2 Min(minimum(a: A.x, b: B.x, c: C.x), minimum(a: A.y, b: B.y, c: C.y));
2524 vec2 Max(maximum(a: A.x, b: B.x, c: C.x), maximum(a: A.y, b: B.y, c: C.y));
2525 vec2 Size(Max.x - Min.x, Max.y - Min.y);
2526
2527 if(Size.x < 0.0000001f || Size.y < 0.0000001f)
2528 return false;
2529
2530 vec2 Normal(1.f / Size.x, 1.f / Size.y);
2531
2532 A = (A - Min) * Normal;
2533 B = (B - Min) * Normal;
2534 C = (C - Min) * Normal;
2535 Point = (Point - Min) * Normal;
2536
2537 float Area = TriangleArea(A, B, C);
2538 return Area > 0.f && absolute(a: TriangleArea(A: Point, B: A, C: B) + TriangleArea(A: Point, B, C) + TriangleArea(A: Point, B: C, C: A) - Area) < 0.000001f;
2539}
2540
2541void CEditor::DoQuadKnife(int QuadIndex)
2542{
2543 std::shared_ptr<CLayerQuads> pLayer = std::static_pointer_cast<CLayerQuads>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_QUADS));
2544 CQuad *pQuad = &pLayer->m_vQuads[QuadIndex];
2545
2546 const bool IgnoreGrid = Input()->AltIsPressed();
2547 float SnapRadius = 4.f * m_MouseWScale;
2548
2549 vec2 Mouse = vec2(Ui()->MouseWorldX(), Ui()->MouseWorldY());
2550 vec2 Point = Mouse;
2551
2552 vec2 v[4] = {
2553 vec2(fx2f(v: pQuad->m_aPoints[0].x), fx2f(v: pQuad->m_aPoints[0].y)),
2554 vec2(fx2f(v: pQuad->m_aPoints[1].x), fx2f(v: pQuad->m_aPoints[1].y)),
2555 vec2(fx2f(v: pQuad->m_aPoints[3].x), fx2f(v: pQuad->m_aPoints[3].y)),
2556 vec2(fx2f(v: pQuad->m_aPoints[2].x), fx2f(v: pQuad->m_aPoints[2].y))};
2557
2558 str_copy(dst&: m_aTooltip, src: "Left click inside the quad to select an area to slice. Hold alt to ignore grid. Right click to leave knife mode");
2559
2560 if(Ui()->MouseButtonClicked(Index: 1))
2561 {
2562 m_QuadKnifeActive = false;
2563 return;
2564 }
2565
2566 // Handle snapping
2567 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
2568 {
2569 float CellSize = MapView()->MapGrid()->GridLineDistance();
2570 vec2 OnGrid(Mouse.x, Mouse.y);
2571 MapView()->MapGrid()->SnapToGrid(x&: OnGrid.x, y&: OnGrid.y);
2572
2573 if(IsInTriangle(Point: OnGrid, A: v[0], B: v[1], C: v[2]) || IsInTriangle(Point: OnGrid, A: v[0], B: v[3], C: v[2]))
2574 Point = OnGrid;
2575 else
2576 {
2577 float MinDistance = -1.f;
2578
2579 for(int i = 0; i < 4; i++)
2580 {
2581 int j = (i + 1) % 4;
2582 vec2 Min(minimum(a: v[i].x, b: v[j].x), minimum(a: v[i].y, b: v[j].y));
2583 vec2 Max(maximum(a: v[i].x, b: v[j].x), maximum(a: v[i].y, b: v[j].y));
2584
2585 if(in_range(a: OnGrid.y, lower: Min.y, upper: Max.y) && Max.y - Min.y > 0.0000001f)
2586 {
2587 vec2 OnEdge(v[i].x + (OnGrid.y - v[i].y) / (v[j].y - v[i].y) * (v[j].x - v[i].x), OnGrid.y);
2588 float Distance = absolute(a: OnGrid.x - OnEdge.x);
2589
2590 if(Distance < CellSize && (Distance < MinDistance || MinDistance < 0.f))
2591 {
2592 MinDistance = Distance;
2593 Point = OnEdge;
2594 }
2595 }
2596
2597 if(in_range(a: OnGrid.x, lower: Min.x, upper: Max.x) && Max.x - Min.x > 0.0000001f)
2598 {
2599 vec2 OnEdge(OnGrid.x, v[i].y + (OnGrid.x - v[i].x) / (v[j].x - v[i].x) * (v[j].y - v[i].y));
2600 float Distance = absolute(a: OnGrid.y - OnEdge.y);
2601
2602 if(Distance < CellSize && (Distance < MinDistance || MinDistance < 0.f))
2603 {
2604 MinDistance = Distance;
2605 Point = OnEdge;
2606 }
2607 }
2608 }
2609 }
2610 }
2611 else
2612 {
2613 float MinDistance = -1.f;
2614
2615 // Try snapping to corners
2616 for(const auto &x : v)
2617 {
2618 float Distance = distance(a: Mouse, b: x);
2619
2620 if(Distance <= SnapRadius && (Distance < MinDistance || MinDistance < 0.f))
2621 {
2622 MinDistance = Distance;
2623 Point = x;
2624 }
2625 }
2626
2627 if(MinDistance < 0.f)
2628 {
2629 // Try snapping to edges
2630 for(int i = 0; i < 4; i++)
2631 {
2632 int j = (i + 1) % 4;
2633 vec2 s(v[j] - v[i]);
2634
2635 float t = ((Mouse.x - v[i].x) * s.x + (Mouse.y - v[i].y) * s.y) / (s.x * s.x + s.y * s.y);
2636
2637 if(in_range(a: t, lower: 0.f, upper: 1.f))
2638 {
2639 vec2 OnEdge = vec2((v[i].x + t * s.x), (v[i].y + t * s.y));
2640 float Distance = distance(a: Mouse, b: OnEdge);
2641
2642 if(Distance <= SnapRadius && (Distance < MinDistance || MinDistance < 0.f))
2643 {
2644 MinDistance = Distance;
2645 Point = OnEdge;
2646 }
2647 }
2648 }
2649 }
2650 }
2651
2652 bool ValidPosition = IsInTriangle(Point, A: v[0], B: v[1], C: v[2]) || IsInTriangle(Point, A: v[0], B: v[3], C: v[2]);
2653
2654 if(Ui()->MouseButtonClicked(Index: 0) && ValidPosition)
2655 {
2656 m_aQuadKnifePoints[m_QuadKnifeCount] = Point;
2657 m_QuadKnifeCount++;
2658 }
2659
2660 if(m_QuadKnifeCount == 4)
2661 {
2662 if(IsInTriangle(Point: m_aQuadKnifePoints[3], A: m_aQuadKnifePoints[0], B: m_aQuadKnifePoints[1], C: m_aQuadKnifePoints[2]) ||
2663 IsInTriangle(Point: m_aQuadKnifePoints[1], A: m_aQuadKnifePoints[0], B: m_aQuadKnifePoints[2], C: m_aQuadKnifePoints[3]))
2664 {
2665 // Fix concave order
2666 std::swap(a&: m_aQuadKnifePoints[0], b&: m_aQuadKnifePoints[3]);
2667 std::swap(a&: m_aQuadKnifePoints[1], b&: m_aQuadKnifePoints[2]);
2668 }
2669
2670 std::swap(a&: m_aQuadKnifePoints[2], b&: m_aQuadKnifePoints[3]);
2671
2672 CQuad *pResult = pLayer->NewQuad(x: 64, y: 64, Width: 64, Height: 64);
2673 pQuad = &pLayer->m_vQuads[QuadIndex];
2674
2675 for(int i = 0; i < 4; i++)
2676 {
2677 int t = IsInTriangle(Point: m_aQuadKnifePoints[i], A: v[0], B: v[3], C: v[2]) ? 2 : 1;
2678
2679 vec2 A = vec2(fx2f(v: pQuad->m_aPoints[0].x), fx2f(v: pQuad->m_aPoints[0].y));
2680 vec2 B = vec2(fx2f(v: pQuad->m_aPoints[3].x), fx2f(v: pQuad->m_aPoints[3].y));
2681 vec2 C = vec2(fx2f(v: pQuad->m_aPoints[t].x), fx2f(v: pQuad->m_aPoints[t].y));
2682
2683 float TriArea = TriangleArea(A, B, C);
2684 float WeightA = TriangleArea(A: m_aQuadKnifePoints[i], B, C) / TriArea;
2685 float WeightB = TriangleArea(A: m_aQuadKnifePoints[i], B: C, C: A) / TriArea;
2686 float WeightC = TriangleArea(A: m_aQuadKnifePoints[i], B: A, C: B) / TriArea;
2687
2688 pResult->m_aColors[i].r = (int)std::round(x: pQuad->m_aColors[0].r * WeightA + pQuad->m_aColors[3].r * WeightB + pQuad->m_aColors[t].r * WeightC);
2689 pResult->m_aColors[i].g = (int)std::round(x: pQuad->m_aColors[0].g * WeightA + pQuad->m_aColors[3].g * WeightB + pQuad->m_aColors[t].g * WeightC);
2690 pResult->m_aColors[i].b = (int)std::round(x: pQuad->m_aColors[0].b * WeightA + pQuad->m_aColors[3].b * WeightB + pQuad->m_aColors[t].b * WeightC);
2691 pResult->m_aColors[i].a = (int)std::round(x: pQuad->m_aColors[0].a * WeightA + pQuad->m_aColors[3].a * WeightB + pQuad->m_aColors[t].a * WeightC);
2692
2693 pResult->m_aTexcoords[i].x = (int)std::round(x: pQuad->m_aTexcoords[0].x * WeightA + pQuad->m_aTexcoords[3].x * WeightB + pQuad->m_aTexcoords[t].x * WeightC);
2694 pResult->m_aTexcoords[i].y = (int)std::round(x: pQuad->m_aTexcoords[0].y * WeightA + pQuad->m_aTexcoords[3].y * WeightB + pQuad->m_aTexcoords[t].y * WeightC);
2695
2696 pResult->m_aPoints[i].x = f2fx(v: m_aQuadKnifePoints[i].x);
2697 pResult->m_aPoints[i].y = f2fx(v: m_aQuadKnifePoints[i].y);
2698 }
2699
2700 pResult->m_aPoints[4].x = ((pResult->m_aPoints[0].x + pResult->m_aPoints[3].x) / 2 + (pResult->m_aPoints[1].x + pResult->m_aPoints[2].x) / 2) / 2;
2701 pResult->m_aPoints[4].y = ((pResult->m_aPoints[0].y + pResult->m_aPoints[3].y) / 2 + (pResult->m_aPoints[1].y + pResult->m_aPoints[2].y) / 2) / 2;
2702
2703 m_QuadKnifeCount = 0;
2704 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionNewQuad>(args: this, args&: m_SelectedGroup, args&: m_vSelectedLayers[0]));
2705 }
2706
2707 // Render
2708 Graphics()->TextureClear();
2709 Graphics()->LinesBegin();
2710
2711 IGraphics::CLineItem aEdges[4] = {
2712 IGraphics::CLineItem(v[0].x, v[0].y, v[1].x, v[1].y),
2713 IGraphics::CLineItem(v[1].x, v[1].y, v[2].x, v[2].y),
2714 IGraphics::CLineItem(v[2].x, v[2].y, v[3].x, v[3].y),
2715 IGraphics::CLineItem(v[3].x, v[3].y, v[0].x, v[0].y)};
2716
2717 Graphics()->SetColor(r: 1.f, g: 0.5f, b: 0.f, a: 1.f);
2718 Graphics()->LinesDraw(pArray: aEdges, Num: 4);
2719
2720 IGraphics::CLineItem aLines[4];
2721 int LineCount = maximum(a: m_QuadKnifeCount - 1, b: 0);
2722
2723 for(int i = 0; i < LineCount; i++)
2724 aLines[i] = IGraphics::CLineItem(m_aQuadKnifePoints[i].x, m_aQuadKnifePoints[i].y, m_aQuadKnifePoints[i + 1].x, m_aQuadKnifePoints[i + 1].y);
2725
2726 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
2727 Graphics()->LinesDraw(pArray: aLines, Num: LineCount);
2728
2729 if(ValidPosition)
2730 {
2731 if(m_QuadKnifeCount > 0)
2732 {
2733 IGraphics::CLineItem LineCurrent(Point.x, Point.y, m_aQuadKnifePoints[m_QuadKnifeCount - 1].x, m_aQuadKnifePoints[m_QuadKnifeCount - 1].y);
2734 Graphics()->LinesDraw(pArray: &LineCurrent, Num: 1);
2735 }
2736
2737 if(m_QuadKnifeCount == 3)
2738 {
2739 IGraphics::CLineItem LineClose(Point.x, Point.y, m_aQuadKnifePoints[0].x, m_aQuadKnifePoints[0].y);
2740 Graphics()->LinesDraw(pArray: &LineClose, Num: 1);
2741 }
2742 }
2743
2744 Graphics()->LinesEnd();
2745 Graphics()->QuadsBegin();
2746
2747 IGraphics::CQuadItem aMarkers[4];
2748
2749 for(int i = 0; i < m_QuadKnifeCount; i++)
2750 aMarkers[i] = IGraphics::CQuadItem(m_aQuadKnifePoints[i].x, m_aQuadKnifePoints[i].y, 5.f * m_MouseWScale, 5.f * m_MouseWScale);
2751
2752 Graphics()->SetColor(r: 0.f, g: 0.f, b: 1.f, a: 1.f);
2753 Graphics()->QuadsDraw(pArray: aMarkers, Num: m_QuadKnifeCount);
2754
2755 if(ValidPosition)
2756 {
2757 IGraphics::CQuadItem MarkerCurrent(Point.x, Point.y, 5.f * m_MouseWScale, 5.f * m_MouseWScale);
2758 Graphics()->QuadsDraw(pArray: &MarkerCurrent, Num: 1);
2759 }
2760
2761 Graphics()->QuadsEnd();
2762}
2763
2764void CEditor::DoQuadEnvelopes(const std::vector<CQuad> &vQuads, IGraphics::CTextureHandle Texture)
2765{
2766 size_t Num = vQuads.size();
2767 std::shared_ptr<CEnvelope> *apEnvelope = new std::shared_ptr<CEnvelope>[Num];
2768 for(size_t i = 0; i < Num; i++)
2769 apEnvelope[i] = nullptr;
2770
2771 for(size_t i = 0; i < Num; i++)
2772 {
2773 if((m_ShowEnvelopePreview == SHOWENV_SELECTED && vQuads[i].m_PosEnv == m_SelectedEnvelope) || m_ShowEnvelopePreview == SHOWENV_ALL)
2774 if(vQuads[i].m_PosEnv >= 0 && vQuads[i].m_PosEnv < (int)m_Map.m_vpEnvelopes.size())
2775 apEnvelope[i] = m_Map.m_vpEnvelopes[vQuads[i].m_PosEnv];
2776 }
2777
2778 // Draw Lines
2779 Graphics()->TextureClear();
2780 Graphics()->LinesBegin();
2781 Graphics()->SetColor(r: 80.0f / 255, g: 150.0f / 255, b: 230.f / 255, a: 0.5f);
2782 for(size_t j = 0; j < Num; j++)
2783 {
2784 if(!apEnvelope[j])
2785 continue;
2786
2787 // QuadParams
2788 const CPoint *pPivotPoint = &vQuads[j].m_aPoints[4];
2789 for(size_t i = 0; i < apEnvelope[j]->m_vPoints.size() - 1; i++)
2790 {
2791 ColorRGBA Result = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
2792 apEnvelope[j]->Eval(Time: apEnvelope[j]->m_vPoints[i].m_Time / 1000.0f + 0.000001f, Result, Channels: 2);
2793 vec2 Pos0 = vec2(fx2f(v: pPivotPoint->x) + Result.r, fx2f(v: pPivotPoint->y) + Result.g);
2794
2795 const int Steps = 15;
2796 for(int n = 1; n <= Steps; n++)
2797 {
2798 const float Time = mix(a: apEnvelope[j]->m_vPoints[i].m_Time, b: apEnvelope[j]->m_vPoints[i + 1].m_Time, amount: (float)n / Steps);
2799 Result = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
2800 apEnvelope[j]->Eval(Time: Time / 1000.0f - 0.000001f, Result, Channels: 2);
2801
2802 vec2 Pos1 = vec2(fx2f(v: pPivotPoint->x) + Result.r, fx2f(v: pPivotPoint->y) + Result.g);
2803
2804 IGraphics::CLineItem Line = IGraphics::CLineItem(Pos0.x, Pos0.y, Pos1.x, Pos1.y);
2805 Graphics()->LinesDraw(pArray: &Line, Num: 1);
2806
2807 Pos0 = Pos1;
2808 }
2809 }
2810 }
2811 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2812 Graphics()->LinesEnd();
2813
2814 // Draw Quads
2815 Graphics()->TextureSet(Texture);
2816 Graphics()->QuadsBegin();
2817
2818 for(size_t j = 0; j < Num; j++)
2819 {
2820 if(!apEnvelope[j])
2821 continue;
2822
2823 // QuadParams
2824 const CPoint *pPoints = vQuads[j].m_aPoints;
2825
2826 for(size_t i = 0; i < apEnvelope[j]->m_vPoints.size(); i++)
2827 {
2828 // Calc Env Position
2829 float OffsetX = fx2f(v: apEnvelope[j]->m_vPoints[i].m_aValues[0]);
2830 float OffsetY = fx2f(v: apEnvelope[j]->m_vPoints[i].m_aValues[1]);
2831 float Rot = fx2f(v: apEnvelope[j]->m_vPoints[i].m_aValues[2]) / 360.0f * pi * 2;
2832
2833 // Set Colours
2834 float Alpha = (m_SelectedQuadEnvelope == vQuads[j].m_PosEnv && IsEnvPointSelected(Index: i)) ? 0.65f : 0.35f;
2835 IGraphics::CColorVertex aArray[4] = {
2836 IGraphics::CColorVertex(0, vQuads[j].m_aColors[0].r, vQuads[j].m_aColors[0].g, vQuads[j].m_aColors[0].b, Alpha),
2837 IGraphics::CColorVertex(1, vQuads[j].m_aColors[1].r, vQuads[j].m_aColors[1].g, vQuads[j].m_aColors[1].b, Alpha),
2838 IGraphics::CColorVertex(2, vQuads[j].m_aColors[2].r, vQuads[j].m_aColors[2].g, vQuads[j].m_aColors[2].b, Alpha),
2839 IGraphics::CColorVertex(3, vQuads[j].m_aColors[3].r, vQuads[j].m_aColors[3].g, vQuads[j].m_aColors[3].b, Alpha)};
2840 Graphics()->SetColorVertex(pArray: aArray, Num: 4);
2841
2842 // Rotation
2843 if(Rot != 0)
2844 {
2845 static CPoint aRotated[4];
2846 aRotated[0] = vQuads[j].m_aPoints[0];
2847 aRotated[1] = vQuads[j].m_aPoints[1];
2848 aRotated[2] = vQuads[j].m_aPoints[2];
2849 aRotated[3] = vQuads[j].m_aPoints[3];
2850 pPoints = aRotated;
2851
2852 Rotate(pCenter: &vQuads[j].m_aPoints[4], pPoint: &aRotated[0], Rotation: Rot);
2853 Rotate(pCenter: &vQuads[j].m_aPoints[4], pPoint: &aRotated[1], Rotation: Rot);
2854 Rotate(pCenter: &vQuads[j].m_aPoints[4], pPoint: &aRotated[2], Rotation: Rot);
2855 Rotate(pCenter: &vQuads[j].m_aPoints[4], pPoint: &aRotated[3], Rotation: Rot);
2856 }
2857
2858 // Set Texture Coords
2859 Graphics()->QuadsSetSubsetFree(
2860 x0: fx2f(v: vQuads[j].m_aTexcoords[0].x), y0: fx2f(v: vQuads[j].m_aTexcoords[0].y),
2861 x1: fx2f(v: vQuads[j].m_aTexcoords[1].x), y1: fx2f(v: vQuads[j].m_aTexcoords[1].y),
2862 x2: fx2f(v: vQuads[j].m_aTexcoords[2].x), y2: fx2f(v: vQuads[j].m_aTexcoords[2].y),
2863 x3: fx2f(v: vQuads[j].m_aTexcoords[3].x), y3: fx2f(v: vQuads[j].m_aTexcoords[3].y));
2864
2865 // Set Quad Coords & Draw
2866 IGraphics::CFreeformItem Freeform(
2867 fx2f(v: pPoints[0].x) + OffsetX, fx2f(v: pPoints[0].y) + OffsetY,
2868 fx2f(v: pPoints[1].x) + OffsetX, fx2f(v: pPoints[1].y) + OffsetY,
2869 fx2f(v: pPoints[2].x) + OffsetX, fx2f(v: pPoints[2].y) + OffsetY,
2870 fx2f(v: pPoints[3].x) + OffsetX, fx2f(v: pPoints[3].y) + OffsetY);
2871 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2872 }
2873 }
2874 Graphics()->QuadsEnd();
2875 Graphics()->TextureClear();
2876 Graphics()->QuadsBegin();
2877
2878 // Draw QuadPoints
2879 for(size_t j = 0; j < Num; j++)
2880 {
2881 if(!apEnvelope[j])
2882 continue;
2883
2884 for(size_t i = 0; i < apEnvelope[j]->m_vPoints.size(); i++)
2885 DoQuadEnvPoint(pQuad: &vQuads[j], QIndex: j, pIndex: i);
2886 }
2887 Graphics()->QuadsEnd();
2888 delete[] apEnvelope;
2889}
2890
2891void CEditor::DoQuadEnvPoint(const CQuad *pQuad, int QIndex, int PIndex)
2892{
2893 enum
2894 {
2895 OP_NONE = 0,
2896 OP_MOVE,
2897 OP_ROTATE,
2898 };
2899
2900 // some basic values
2901 static float s_LastWx = 0;
2902 static float s_LastWy = 0;
2903 static int s_Operation = OP_NONE;
2904 float wx = Ui()->MouseWorldX();
2905 float wy = Ui()->MouseWorldY();
2906 std::shared_ptr<CEnvelope> pEnvelope = m_Map.m_vpEnvelopes[pQuad->m_PosEnv];
2907 void *pId = &pEnvelope->m_vPoints[PIndex];
2908
2909 // get pivot
2910 float CenterX = fx2f(v: pQuad->m_aPoints[4].x) + fx2f(v: pEnvelope->m_vPoints[PIndex].m_aValues[0]);
2911 float CenterY = fx2f(v: pQuad->m_aPoints[4].y) + fx2f(v: pEnvelope->m_vPoints[PIndex].m_aValues[1]);
2912
2913 const bool IgnoreGrid = Input()->AltIsPressed();
2914
2915 if(Ui()->CheckActiveItem(pId) && m_CurrentQuadIndex == QIndex)
2916 {
2917 if(s_Operation == OP_MOVE)
2918 {
2919 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
2920 {
2921 float x = wx;
2922 float y = wy;
2923 MapView()->MapGrid()->SnapToGrid(x, y);
2924 pEnvelope->m_vPoints[PIndex].m_aValues[0] = f2fx(v: x) - pQuad->m_aPoints[4].x;
2925 pEnvelope->m_vPoints[PIndex].m_aValues[1] = f2fx(v: y) - pQuad->m_aPoints[4].y;
2926 }
2927 else
2928 {
2929 pEnvelope->m_vPoints[PIndex].m_aValues[0] += f2fx(v: wx - s_LastWx);
2930 pEnvelope->m_vPoints[PIndex].m_aValues[1] += f2fx(v: wy - s_LastWy);
2931 }
2932 }
2933 else if(s_Operation == OP_ROTATE)
2934 pEnvelope->m_vPoints[PIndex].m_aValues[2] += 10 * m_MouseDeltaX;
2935
2936 s_LastWx = wx;
2937 s_LastWy = wy;
2938
2939 if(!Ui()->MouseButton(Index: 0))
2940 {
2941 Ui()->DisableMouseLock();
2942 s_Operation = OP_NONE;
2943 Ui()->SetActiveItem(nullptr);
2944 }
2945
2946 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2947 }
2948 else if(Ui()->HotItem() == pId && m_CurrentQuadIndex == QIndex)
2949 {
2950 ms_pUiGotContext = pId;
2951
2952 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2953 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold ctrl to rotate. Hold alt to ignore grid.");
2954
2955 if(Ui()->MouseButton(Index: 0))
2956 {
2957 if(Input()->ModifierIsPressed())
2958 {
2959 Ui()->EnableMouseLock(pId);
2960 s_Operation = OP_ROTATE;
2961
2962 SelectQuad(Index: QIndex);
2963 }
2964 else
2965 {
2966 s_Operation = OP_MOVE;
2967
2968 SelectQuad(Index: QIndex);
2969 }
2970
2971 SelectEnvPoint(Index: PIndex);
2972 m_SelectedQuadEnvelope = pQuad->m_PosEnv;
2973
2974 Ui()->SetActiveItem(pId);
2975
2976 s_LastWx = wx;
2977 s_LastWy = wy;
2978 }
2979 else
2980 {
2981 DeselectEnvPoints();
2982 m_SelectedQuadEnvelope = -1;
2983 }
2984 }
2985 else
2986 Graphics()->SetColor(r: 0.0f, g: 1.0f, b: 0.0f, a: 1.0f);
2987
2988 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * m_MouseWScale, 5.0f * m_MouseWScale);
2989 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
2990}
2991
2992void CEditor::DoMapEditor(CUIRect View)
2993{
2994 // render all good stuff
2995 if(!m_ShowPicker)
2996 {
2997 MapView()->RenderMap();
2998 }
2999 else
3000 {
3001 // fix aspect ratio of the image in the picker
3002 float Max = minimum(a: View.w, b: View.h);
3003 View.w = View.h = Max;
3004 }
3005
3006 const bool Inside = Ui()->MouseInside(pRect: &View);
3007
3008 // fetch mouse position
3009 float wx = Ui()->MouseWorldX();
3010 float wy = Ui()->MouseWorldY();
3011 float mx = Ui()->MouseX();
3012 float my = Ui()->MouseY();
3013
3014 static float s_StartWx = 0;
3015 static float s_StartWy = 0;
3016
3017 enum
3018 {
3019 OP_NONE = 0,
3020 OP_BRUSH_GRAB,
3021 OP_BRUSH_DRAW,
3022 OP_BRUSH_PAINT,
3023 OP_PAN_WORLD,
3024 OP_PAN_EDITOR,
3025 };
3026
3027 // remap the screen so it can display the whole tileset
3028 if(m_ShowPicker)
3029 {
3030 CUIRect Screen = *Ui()->Screen();
3031 float Size = 32.0f * 16.0f;
3032 float w = Size * (Screen.w / View.w);
3033 float h = Size * (Screen.h / View.h);
3034 float x = -(View.x / Screen.w) * w;
3035 float y = -(View.y / Screen.h) * h;
3036 wx = x + w * mx / Screen.w;
3037 wy = y + h * my / Screen.h;
3038 std::shared_ptr<CLayerTiles> pTileLayer = std::static_pointer_cast<CLayerTiles>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_TILES));
3039 if(pTileLayer)
3040 {
3041 Graphics()->MapScreen(TopLeftX: x, TopLeftY: y, BottomRightX: x + w, BottomRightY: y + h);
3042 m_pTilesetPicker->m_Image = pTileLayer->m_Image;
3043 if(m_BrushColorEnabled)
3044 {
3045 m_pTilesetPicker->m_Color = pTileLayer->m_Color;
3046 m_pTilesetPicker->m_Color.a = 255;
3047 }
3048 else
3049 {
3050 m_pTilesetPicker->m_Color = {255, 255, 255, 255};
3051 }
3052
3053 m_pTilesetPicker->m_Game = pTileLayer->m_Game;
3054 m_pTilesetPicker->m_Tele = pTileLayer->m_Tele;
3055 m_pTilesetPicker->m_Speedup = pTileLayer->m_Speedup;
3056 m_pTilesetPicker->m_Front = pTileLayer->m_Front;
3057 m_pTilesetPicker->m_Switch = pTileLayer->m_Switch;
3058 m_pTilesetPicker->m_Tune = pTileLayer->m_Tune;
3059
3060 m_pTilesetPicker->Render(Tileset: true);
3061
3062 if(m_ShowTileInfo != SHOW_TILE_OFF)
3063 m_pTilesetPicker->ShowInfo();
3064 }
3065 else
3066 {
3067 std::shared_ptr<CLayerQuads> pQuadLayer = std::static_pointer_cast<CLayerQuads>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_QUADS));
3068 if(pQuadLayer)
3069 {
3070 m_pQuadsetPicker->m_Image = pQuadLayer->m_Image;
3071 m_pQuadsetPicker->m_vQuads[0].m_aPoints[0].x = f2fx(v: View.x);
3072 m_pQuadsetPicker->m_vQuads[0].m_aPoints[0].y = f2fx(v: View.y);
3073 m_pQuadsetPicker->m_vQuads[0].m_aPoints[1].x = f2fx(v: (View.x + View.w));
3074 m_pQuadsetPicker->m_vQuads[0].m_aPoints[1].y = f2fx(v: View.y);
3075 m_pQuadsetPicker->m_vQuads[0].m_aPoints[2].x = f2fx(v: View.x);
3076 m_pQuadsetPicker->m_vQuads[0].m_aPoints[2].y = f2fx(v: (View.y + View.h));
3077 m_pQuadsetPicker->m_vQuads[0].m_aPoints[3].x = f2fx(v: (View.x + View.w));
3078 m_pQuadsetPicker->m_vQuads[0].m_aPoints[3].y = f2fx(v: (View.y + View.h));
3079 m_pQuadsetPicker->m_vQuads[0].m_aPoints[4].x = f2fx(v: (View.x + View.w / 2));
3080 m_pQuadsetPicker->m_vQuads[0].m_aPoints[4].y = f2fx(v: (View.y + View.h / 2));
3081 m_pQuadsetPicker->Render();
3082 }
3083 }
3084 }
3085
3086 static int s_Operation = OP_NONE;
3087
3088 // draw layer borders
3089 std::pair<int, std::shared_ptr<CLayer>> apEditLayers[128];
3090 size_t NumEditLayers = 0;
3091
3092 if(m_ShowPicker && GetSelectedLayer(Index: 0) && GetSelectedLayer(Index: 0)->m_Type == LAYERTYPE_TILES)
3093 {
3094 apEditLayers[0] = {0, m_pTilesetPicker};
3095 NumEditLayers++;
3096 }
3097 else if(m_ShowPicker)
3098 {
3099 apEditLayers[0] = {0, m_pQuadsetPicker};
3100 NumEditLayers++;
3101 }
3102 else
3103 {
3104 // pick a type of layers to edit, preferring Tiles layers.
3105 int EditingType = -1;
3106 for(size_t i = 0; i < m_vSelectedLayers.size(); i++)
3107 {
3108 std::shared_ptr<CLayer> pLayer = GetSelectedLayer(Index: i);
3109 if(pLayer && (EditingType == -1 || pLayer->m_Type == LAYERTYPE_TILES))
3110 {
3111 EditingType = pLayer->m_Type;
3112 if(EditingType == LAYERTYPE_TILES)
3113 break;
3114 }
3115 }
3116 for(size_t i = 0; i < m_vSelectedLayers.size() && NumEditLayers < 128; i++)
3117 {
3118 apEditLayers[NumEditLayers] = {m_vSelectedLayers[i], GetSelectedLayerType(Index: i, Type: EditingType)};
3119 if(apEditLayers[NumEditLayers].second)
3120 {
3121 NumEditLayers++;
3122 }
3123 }
3124
3125 MapView()->RenderGroupBorder();
3126 MapView()->MapGrid()->OnRender(View);
3127 }
3128
3129 const bool ShouldPan = (Input()->ModifierIsPressed() && Ui()->MouseButton(Index: 0)) || Ui()->MouseButton(Index: 2);
3130 if(m_pContainerPanned == &m_MapEditorId)
3131 {
3132 // do panning
3133 if(ShouldPan)
3134 {
3135 if(Input()->ShiftIsPressed())
3136 s_Operation = OP_PAN_EDITOR;
3137 else
3138 s_Operation = OP_PAN_WORLD;
3139 }
3140 else
3141 s_Operation = OP_NONE;
3142
3143 if(s_Operation == OP_PAN_WORLD)
3144 MapView()->OffsetWorld(Offset: -vec2(m_MouseDeltaX, m_MouseDeltaY) * m_MouseWScale);
3145 else if(s_Operation == OP_PAN_EDITOR)
3146 MapView()->OffsetEditor(Offset: -vec2(m_MouseDeltaX, m_MouseDeltaY) * m_MouseWScale);
3147
3148 if(s_Operation == OP_NONE)
3149 m_pContainerPanned = nullptr;
3150 }
3151
3152 if(Inside)
3153 {
3154 Ui()->SetHotItem(&m_MapEditorId);
3155
3156 // do global operations like pan and zoom
3157 if(Ui()->CheckActiveItem(pId: nullptr) && (Ui()->MouseButton(Index: 0) || Ui()->MouseButton(Index: 2)))
3158 {
3159 s_StartWx = wx;
3160 s_StartWy = wy;
3161
3162 if(ShouldPan && m_pContainerPanned == nullptr)
3163 m_pContainerPanned = &m_MapEditorId;
3164 }
3165
3166 // brush editing
3167 if(Ui()->HotItem() == &m_MapEditorId)
3168 {
3169 if(m_ShowPicker)
3170 {
3171 std::shared_ptr<CLayer> pLayer = GetSelectedLayer(Index: 0);
3172 int Layer;
3173 if(pLayer == m_Map.m_pGameLayer)
3174 Layer = LAYER_GAME;
3175 else if(pLayer == m_Map.m_pFrontLayer)
3176 Layer = LAYER_FRONT;
3177 else if(pLayer == m_Map.m_pSwitchLayer)
3178 Layer = LAYER_SWITCH;
3179 else if(pLayer == m_Map.m_pTeleLayer)
3180 Layer = LAYER_TELE;
3181 else if(pLayer == m_Map.m_pSpeedupLayer)
3182 Layer = LAYER_SPEEDUP;
3183 else if(pLayer == m_Map.m_pTuneLayer)
3184 Layer = LAYER_TUNE;
3185 else
3186 Layer = NUM_LAYERS;
3187
3188 EExplanation Explanation;
3189 if(m_SelectEntitiesImage == "DDNet")
3190 Explanation = EExplanation::DDNET;
3191 else if(m_SelectEntitiesImage == "FNG")
3192 Explanation = EExplanation::FNG;
3193 else if(m_SelectEntitiesImage == "Race")
3194 Explanation = EExplanation::RACE;
3195 else if(m_SelectEntitiesImage == "Vanilla")
3196 Explanation = EExplanation::VANILLA;
3197 else if(m_SelectEntitiesImage == "blockworlds")
3198 Explanation = EExplanation::BLOCKWORLDS;
3199 else
3200 Explanation = EExplanation::NONE;
3201
3202 if(Layer != NUM_LAYERS)
3203 {
3204 const char *pExplanation = Explain(Explanation, Tile: (int)wx / 32 + (int)wy / 32 * 16, Layer);
3205 if(pExplanation)
3206 str_copy(dst&: m_aTooltip, src: pExplanation);
3207 }
3208 }
3209 else if(m_pBrush->IsEmpty() && GetSelectedLayerType(Index: 0, Type: LAYERTYPE_QUADS) != nullptr)
3210 str_copy(dst&: m_aTooltip, src: "Use left mouse button to drag and create a brush. Hold shift to select multiple quads. Press R to rotate selected quads. Use ctrl+right mouse to select layer.");
3211 else if(m_pBrush->IsEmpty())
3212 str_copy(dst&: m_aTooltip, src: "Use left mouse button to drag and create a brush. Use ctrl+right mouse to select layer.");
3213 else
3214 str_copy(dst&: m_aTooltip, src: "Use left mouse button to paint with the brush. Right button clears the brush.");
3215
3216 if(Ui()->CheckActiveItem(pId: &m_MapEditorId))
3217 {
3218 CUIRect r;
3219 r.x = s_StartWx;
3220 r.y = s_StartWy;
3221 r.w = wx - s_StartWx;
3222 r.h = wy - s_StartWy;
3223 if(r.w < 0)
3224 {
3225 r.x += r.w;
3226 r.w = -r.w;
3227 }
3228
3229 if(r.h < 0)
3230 {
3231 r.y += r.h;
3232 r.h = -r.h;
3233 }
3234
3235 if(s_Operation == OP_BRUSH_DRAW)
3236 {
3237 if(!m_pBrush->IsEmpty())
3238 {
3239 // draw with brush
3240 for(size_t k = 0; k < NumEditLayers; k++)
3241 {
3242 size_t BrushIndex = k % m_pBrush->m_vpLayers.size();
3243 if(apEditLayers[k].second->m_Type == m_pBrush->m_vpLayers[BrushIndex]->m_Type)
3244 {
3245 if(apEditLayers[k].second->m_Type == LAYERTYPE_TILES)
3246 {
3247 std::shared_ptr<CLayerTiles> pLayer = std::static_pointer_cast<CLayerTiles>(r: apEditLayers[k].second);
3248 std::shared_ptr<CLayerTiles> pBrushLayer = std::static_pointer_cast<CLayerTiles>(r: m_pBrush->m_vpLayers[BrushIndex]);
3249
3250 if(pLayer->m_Tele <= pBrushLayer->m_Tele && pLayer->m_Speedup <= pBrushLayer->m_Speedup && pLayer->m_Front <= pBrushLayer->m_Front && pLayer->m_Game <= pBrushLayer->m_Game && pLayer->m_Switch <= pBrushLayer->m_Switch && pLayer->m_Tune <= pBrushLayer->m_Tune)
3251 pLayer->BrushDraw(pBrush: pBrushLayer, wx, wy);
3252 }
3253 else
3254 {
3255 apEditLayers[k].second->BrushDraw(pBrush: m_pBrush->m_vpLayers[BrushIndex], x: wx, y: wy);
3256 }
3257 }
3258 }
3259 }
3260 }
3261 else if(s_Operation == OP_BRUSH_GRAB)
3262 {
3263 if(!Ui()->MouseButton(Index: 0))
3264 {
3265 std::shared_ptr<CLayerQuads> pQuadLayer = std::static_pointer_cast<CLayerQuads>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_QUADS));
3266 if(Input()->ShiftIsPressed() && pQuadLayer)
3267 {
3268 DeselectQuads();
3269 for(size_t i = 0; i < pQuadLayer->m_vQuads.size(); i++)
3270 {
3271 const CQuad &Quad = pQuadLayer->m_vQuads[i];
3272 float px = fx2f(v: Quad.m_aPoints[4].x);
3273 float py = fx2f(v: Quad.m_aPoints[4].y);
3274
3275 if(r.Inside(PointX: px, PointY: py) && !IsQuadSelected(Index: i))
3276 ToggleSelectQuad(Index: i);
3277 }
3278 }
3279 else
3280 {
3281 // grab brush
3282 char aBuf[256];
3283 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "grabbing %f %f %f %f", r.x, r.y, r.w, r.h);
3284 Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "editor", pStr: aBuf);
3285
3286 // TODO: do all layers
3287 int Grabs = 0;
3288 for(size_t k = 0; k < NumEditLayers; k++)
3289 Grabs += apEditLayers[k].second->BrushGrab(pBrush: m_pBrush, Rect: r);
3290 if(Grabs == 0)
3291 m_pBrush->Clear();
3292
3293 DeselectQuads();
3294 DeselectQuadPoints();
3295 }
3296 }
3297 else
3298 {
3299 for(size_t k = 0; k < NumEditLayers; k++)
3300 apEditLayers[k].second->BrushSelecting(Rect: r);
3301 Ui()->MapScreen();
3302 }
3303 }
3304 else if(s_Operation == OP_BRUSH_PAINT)
3305 {
3306 if(!Ui()->MouseButton(Index: 0))
3307 {
3308 for(size_t k = 0; k < NumEditLayers; k++)
3309 {
3310 size_t BrushIndex = k;
3311 if(m_pBrush->m_vpLayers.size() != NumEditLayers)
3312 BrushIndex = 0;
3313 std::shared_ptr<CLayer> pBrush = m_pBrush->IsEmpty() ? nullptr : m_pBrush->m_vpLayers[BrushIndex];
3314 apEditLayers[k].second->FillSelection(Empty: m_pBrush->IsEmpty(), pBrush, Rect: r);
3315 }
3316 std::shared_ptr<IEditorAction> Action = std::make_shared<CEditorBrushDrawAction>(args: this, args&: m_SelectedGroup);
3317 m_EditorHistory.RecordAction(pAction: Action);
3318 }
3319 else
3320 {
3321 for(size_t k = 0; k < NumEditLayers; k++)
3322 apEditLayers[k].second->BrushSelecting(Rect: r);
3323 Ui()->MapScreen();
3324 }
3325 }
3326 }
3327 else
3328 {
3329 if(Ui()->MouseButton(Index: 1))
3330 {
3331 m_pBrush->Clear();
3332 }
3333
3334 if(!Input()->ModifierIsPressed() && Ui()->MouseButton(Index: 0) && s_Operation == OP_NONE && !m_QuadKnifeActive)
3335 {
3336 Ui()->SetActiveItem(&m_MapEditorId);
3337
3338 if(m_pBrush->IsEmpty())
3339 s_Operation = OP_BRUSH_GRAB;
3340 else
3341 {
3342 s_Operation = OP_BRUSH_DRAW;
3343 for(size_t k = 0; k < NumEditLayers; k++)
3344 {
3345 size_t BrushIndex = k;
3346 if(m_pBrush->m_vpLayers.size() != NumEditLayers)
3347 BrushIndex = 0;
3348
3349 if(apEditLayers[k].second->m_Type == m_pBrush->m_vpLayers[BrushIndex]->m_Type)
3350 apEditLayers[k].second->BrushPlace(pBrush: m_pBrush->m_vpLayers[BrushIndex], x: wx, y: wy);
3351 }
3352 }
3353
3354 std::shared_ptr<CLayerTiles> pLayer = std::static_pointer_cast<CLayerTiles>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_TILES));
3355 if(Input()->ShiftIsPressed() && pLayer)
3356 s_Operation = OP_BRUSH_PAINT;
3357 }
3358
3359 if(!m_pBrush->IsEmpty())
3360 {
3361 m_pBrush->m_OffsetX = -(int)wx;
3362 m_pBrush->m_OffsetY = -(int)wy;
3363 for(const auto &pLayer : m_pBrush->m_vpLayers)
3364 {
3365 if(pLayer->m_Type == LAYERTYPE_TILES)
3366 {
3367 m_pBrush->m_OffsetX = -(int)(wx / 32.0f) * 32;
3368 m_pBrush->m_OffsetY = -(int)(wy / 32.0f) * 32;
3369 break;
3370 }
3371 }
3372
3373 std::shared_ptr<CLayerGroup> pGroup = GetSelectedGroup();
3374 if(!m_ShowPicker && pGroup)
3375 {
3376 m_pBrush->m_OffsetX += pGroup->m_OffsetX;
3377 m_pBrush->m_OffsetY += pGroup->m_OffsetY;
3378 m_pBrush->m_ParallaxX = pGroup->m_ParallaxX;
3379 m_pBrush->m_ParallaxY = pGroup->m_ParallaxY;
3380 m_pBrush->Render();
3381 float w, h;
3382 m_pBrush->GetSize(pWidth: &w, pHeight: &h);
3383
3384 IGraphics::CLineItem Array[4] = {
3385 IGraphics::CLineItem(0, 0, w, 0),
3386 IGraphics::CLineItem(w, 0, w, h),
3387 IGraphics::CLineItem(w, h, 0, h),
3388 IGraphics::CLineItem(0, h, 0, 0)};
3389 Graphics()->TextureClear();
3390 Graphics()->LinesBegin();
3391 Graphics()->LinesDraw(pArray: Array, Num: 4);
3392 Graphics()->LinesEnd();
3393 }
3394 }
3395 }
3396 }
3397
3398 // quad & sound editing
3399 {
3400 if(!m_ShowPicker && m_pBrush->IsEmpty())
3401 {
3402 // fetch layers
3403 std::shared_ptr<CLayerGroup> pGroup = GetSelectedGroup();
3404 if(pGroup)
3405 pGroup->MapScreen();
3406
3407 for(size_t k = 0; k < NumEditLayers; k++)
3408 {
3409 auto &[LayerIndex, pEditLayer] = apEditLayers[k];
3410
3411 if(pEditLayer->m_Type == LAYERTYPE_QUADS)
3412 {
3413 std::shared_ptr<CLayerQuads> pLayer = std::static_pointer_cast<CLayerQuads>(r: pEditLayer);
3414
3415 if(m_ShowEnvelopePreview == SHOWENV_NONE)
3416 m_ShowEnvelopePreview = SHOWENV_ALL;
3417
3418 if(m_QuadKnifeActive)
3419 DoQuadKnife(QuadIndex: m_vSelectedQuads[m_SelectedQuadIndex]);
3420 else
3421 {
3422 SetHotQuadPoint(pLayer);
3423
3424 Graphics()->TextureClear();
3425 Graphics()->QuadsBegin();
3426 for(size_t i = 0; i < pLayer->m_vQuads.size(); i++)
3427 {
3428 for(int v = 0; v < 4; v++)
3429 DoQuadPoint(LayerIndex, pLayer, pQuad: &pLayer->m_vQuads[i], QuadIndex: i, V: v);
3430
3431 DoQuad(LayerIndex, pLayer, pQuad: &pLayer->m_vQuads[i], Index: i);
3432 }
3433 Graphics()->QuadsEnd();
3434 }
3435 }
3436
3437 if(pEditLayer->m_Type == LAYERTYPE_SOUNDS)
3438 {
3439 std::shared_ptr<CLayerSounds> pLayer = std::static_pointer_cast<CLayerSounds>(r: pEditLayer);
3440
3441 Graphics()->TextureClear();
3442 Graphics()->QuadsBegin();
3443 for(size_t i = 0; i < pLayer->m_vSources.size(); i++)
3444 {
3445 DoSoundSource(LayerIndex, pSource: &pLayer->m_vSources[i], Index: i);
3446 }
3447 Graphics()->QuadsEnd();
3448 }
3449 }
3450
3451 Ui()->MapScreen();
3452 }
3453 }
3454
3455 // menu proof selection
3456 if(MapView()->ProofMode()->IsModeMenu() && !m_ShowPicker)
3457 {
3458 MapView()->ProofMode()->ResetMenuBackgroundPositions();
3459 for(int i = 0; i < (int)MapView()->ProofMode()->m_vMenuBackgroundPositions.size(); i++)
3460 {
3461 vec2 Pos = MapView()->ProofMode()->m_vMenuBackgroundPositions[i];
3462 Pos += MapView()->GetWorldOffset() - MapView()->ProofMode()->m_vMenuBackgroundPositions[MapView()->ProofMode()->m_CurrentMenuProofIndex];
3463 Pos.y -= 3.0f;
3464
3465 vec2 MousePos(m_MouseWorldNoParaX, m_MouseWorldNoParaY);
3466 if(distance(a: Pos, b: MousePos) <= 20.0f)
3467 {
3468 Ui()->SetHotItem(&MapView()->ProofMode()->m_vMenuBackgroundPositions[i]);
3469
3470 if(i != MapView()->ProofMode()->m_CurrentMenuProofIndex && Ui()->CheckActiveItem(pId: &MapView()->ProofMode()->m_vMenuBackgroundPositions[i]))
3471 {
3472 if(!Ui()->MouseButton(Index: 0))
3473 {
3474 MapView()->ProofMode()->m_CurrentMenuProofIndex = i;
3475 MapView()->SetWorldOffset(MapView()->ProofMode()->m_vMenuBackgroundPositions[i]);
3476 Ui()->SetActiveItem(nullptr);
3477 }
3478 }
3479 else if(Ui()->HotItem() == &MapView()->ProofMode()->m_vMenuBackgroundPositions[i])
3480 {
3481 char aTooltipPrefix[32] = "Switch proof position to";
3482 if(i == MapView()->ProofMode()->m_CurrentMenuProofIndex)
3483 str_copy(dst&: aTooltipPrefix, src: "Current proof position at");
3484
3485 char aNumBuf[8];
3486 if(i < (TILE_TIME_CHECKPOINT_LAST - TILE_TIME_CHECKPOINT_FIRST))
3487 str_format(buffer: aNumBuf, buffer_size: sizeof(aNumBuf), format: "#%d", i + 1);
3488 else
3489 aNumBuf[0] = '\0';
3490
3491 char aTooltipPositions[128];
3492 str_format(buffer: aTooltipPositions, buffer_size: sizeof(aTooltipPositions), format: "%s %s", MapView()->ProofMode()->m_vpMenuBackgroundPositionNames[i], aNumBuf);
3493
3494 for(int k : MapView()->ProofMode()->m_vMenuBackgroundCollisions.at(n: i))
3495 {
3496 if(k == MapView()->ProofMode()->m_CurrentMenuProofIndex)
3497 str_copy(dst&: aTooltipPrefix, src: "Current proof position at");
3498
3499 Pos = MapView()->ProofMode()->m_vMenuBackgroundPositions[k];
3500 Pos += MapView()->GetWorldOffset() - MapView()->ProofMode()->m_vMenuBackgroundPositions[MapView()->ProofMode()->m_CurrentMenuProofIndex];
3501 Pos.y -= 3.0f;
3502
3503 MousePos = vec2(m_MouseWorldNoParaX, m_MouseWorldNoParaY);
3504 if(distance(a: Pos, b: MousePos) > 20.0f)
3505 continue;
3506
3507 if(i < (TILE_TIME_CHECKPOINT_LAST - TILE_TIME_CHECKPOINT_FIRST))
3508 str_format(buffer: aNumBuf, buffer_size: sizeof(aNumBuf), format: "#%d", k + 1);
3509 else
3510 aNumBuf[0] = '\0';
3511
3512 char aTooltipPositionsCopy[128];
3513 str_copy(dst&: aTooltipPositionsCopy, src: aTooltipPositions);
3514 str_format(buffer: aTooltipPositions, buffer_size: sizeof(aTooltipPositions), format: "%s, %s %s", aTooltipPositionsCopy, MapView()->ProofMode()->m_vpMenuBackgroundPositionNames[k], aNumBuf);
3515 }
3516 str_format(buffer: m_aMenuBackgroundTooltip, buffer_size: sizeof(m_aMenuBackgroundTooltip), format: "%s %s", aTooltipPrefix, aTooltipPositions);
3517
3518 str_copy(dst&: m_aTooltip, src: m_aMenuBackgroundTooltip);
3519 if(Ui()->MouseButton(Index: 0))
3520 Ui()->SetActiveItem(&MapView()->ProofMode()->m_vMenuBackgroundPositions[i]);
3521 }
3522 break;
3523 }
3524 }
3525 }
3526
3527 if(Ui()->CheckActiveItem(pId: &m_MapEditorId))
3528 {
3529 // release mouse
3530 if(!Ui()->MouseButton(Index: 0))
3531 {
3532 if(s_Operation == OP_BRUSH_DRAW)
3533 {
3534 std::shared_ptr<IEditorAction> Action = std::make_shared<CEditorBrushDrawAction>(args: this, args&: m_SelectedGroup);
3535
3536 if(!Action->IsEmpty()) // Avoid recording tile draw action when placing quads only
3537 m_EditorHistory.RecordAction(pAction: Action);
3538 }
3539
3540 s_Operation = OP_NONE;
3541 Ui()->SetActiveItem(nullptr);
3542 }
3543 }
3544 if(!Input()->ModifierIsPressed() && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
3545 {
3546 float PanSpeed = Input()->ShiftIsPressed() ? 200.0f : 64.0f;
3547 if(Input()->KeyPress(Key: KEY_A))
3548 MapView()->OffsetWorld(Offset: {-PanSpeed * m_MouseWScale, 0});
3549 else if(Input()->KeyPress(Key: KEY_D))
3550 MapView()->OffsetWorld(Offset: {PanSpeed * m_MouseWScale, 0});
3551 if(Input()->KeyPress(Key: KEY_W))
3552 MapView()->OffsetWorld(Offset: {0, -PanSpeed * m_MouseWScale});
3553 else if(Input()->KeyPress(Key: KEY_S))
3554 MapView()->OffsetWorld(Offset: {0, PanSpeed * m_MouseWScale});
3555 }
3556 }
3557 else if(Ui()->CheckActiveItem(pId: &m_MapEditorId))
3558 {
3559 // release mouse
3560 if(!Ui()->MouseButton(Index: 0))
3561 {
3562 s_Operation = OP_NONE;
3563 Ui()->SetActiveItem(nullptr);
3564 }
3565 }
3566
3567 if(!m_ShowPicker && GetSelectedGroup() && GetSelectedGroup()->m_UseClipping)
3568 {
3569 std::shared_ptr<CLayerGroup> pGameGroup = m_Map.m_pGameGroup;
3570 pGameGroup->MapScreen();
3571
3572 Graphics()->TextureClear();
3573 Graphics()->LinesBegin();
3574
3575 CUIRect r;
3576 r.x = GetSelectedGroup()->m_ClipX;
3577 r.y = GetSelectedGroup()->m_ClipY;
3578 r.w = GetSelectedGroup()->m_ClipW;
3579 r.h = GetSelectedGroup()->m_ClipH;
3580
3581 IGraphics::CLineItem Array[4] = {
3582 IGraphics::CLineItem(r.x, r.y, r.x + r.w, r.y),
3583 IGraphics::CLineItem(r.x + r.w, r.y, r.x + r.w, r.y + r.h),
3584 IGraphics::CLineItem(r.x + r.w, r.y + r.h, r.x, r.y + r.h),
3585 IGraphics::CLineItem(r.x, r.y + r.h, r.x, r.y)};
3586 Graphics()->SetColor(r: 1, g: 0, b: 0, a: 1);
3587 Graphics()->LinesDraw(pArray: Array, Num: 4);
3588
3589 Graphics()->LinesEnd();
3590 }
3591
3592 MapView()->ProofMode()->RenderScreenSizes();
3593
3594 if(!m_ShowPicker && m_ShowTileInfo != SHOW_TILE_OFF && m_ShowEnvelopePreview != SHOWENV_NONE && GetSelectedLayer(Index: 0) && GetSelectedLayer(Index: 0)->m_Type == LAYERTYPE_QUADS)
3595 {
3596 GetSelectedGroup()->MapScreen();
3597
3598 std::shared_ptr<CLayerQuads> pLayer = std::static_pointer_cast<CLayerQuads>(r: GetSelectedLayer(Index: 0));
3599 IGraphics::CTextureHandle Texture;
3600 if(pLayer->m_Image >= 0 && pLayer->m_Image < (int)m_Map.m_vpImages.size())
3601 Texture = m_Map.m_vpImages[pLayer->m_Image]->m_Texture;
3602
3603 DoQuadEnvelopes(vQuads: pLayer->m_vQuads, Texture);
3604 m_ShowEnvelopePreview = SHOWENV_NONE;
3605 }
3606
3607 Ui()->MapScreen();
3608}
3609
3610void CEditor::SetHotQuadPoint(const std::shared_ptr<CLayerQuads> &pLayer)
3611{
3612 float wx = Ui()->MouseWorldX();
3613 float wy = Ui()->MouseWorldY();
3614
3615 float MinDist = 500.0f;
3616 void *pMinPoint = nullptr;
3617
3618 auto UpdateMinimum = [&](float px, float py, void *pId) {
3619 float dx = (px - wx) / m_MouseWScale;
3620 float dy = (py - wy) / m_MouseWScale;
3621
3622 float CurrDist = dx * dx + dy * dy;
3623 if(CurrDist < MinDist)
3624 {
3625 MinDist = CurrDist;
3626 pMinPoint = pId;
3627 return true;
3628 }
3629 return false;
3630 };
3631
3632 for(size_t i = 0; i < pLayer->m_vQuads.size(); i++)
3633 {
3634 CQuad &Quad = pLayer->m_vQuads.at(n: i);
3635
3636 if(m_ShowTileInfo != SHOW_TILE_OFF && m_ShowEnvelopePreview != SHOWENV_NONE && Quad.m_PosEnv >= 0)
3637 {
3638 for(auto &EnvPoint : m_Map.m_vpEnvelopes[Quad.m_PosEnv]->m_vPoints)
3639 {
3640 float px = fx2f(v: Quad.m_aPoints[4].x) + fx2f(v: EnvPoint.m_aValues[0]);
3641 float py = fx2f(v: Quad.m_aPoints[4].y) + fx2f(v: EnvPoint.m_aValues[1]);
3642 if(UpdateMinimum(px, py, &EnvPoint))
3643 m_CurrentQuadIndex = i;
3644 }
3645 }
3646
3647 for(auto &Point : Quad.m_aPoints)
3648 UpdateMinimum(fx2f(v: Point.x), fx2f(v: Point.y), &Point);
3649 }
3650
3651 if(pMinPoint != nullptr)
3652 Ui()->SetHotItem(pMinPoint);
3653}
3654
3655void CEditor::DoColorPickerButton(const void *pId, const CUIRect *pRect, ColorRGBA Color, const std::function<void(ColorRGBA Color)> &SetColor)
3656{
3657 CUIRect ColorRect;
3658 pRect->Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * Ui()->ButtonColorMul(pId)), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
3659 pRect->Margin(Cut: 1.0f, pOtherRect: &ColorRect);
3660 ColorRect.Draw(Color, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
3661
3662 const int ButtonResult = DoButton_Editor_Common(pId, pText: nullptr, Checked: 0, pRect, Flags: 0, pToolTip: "Click to show the color picker. Shift+rightclick to copy color to clipboard. Shift+leftclick to paste color from clipboard.");
3663 if(Input()->ShiftIsPressed())
3664 {
3665 if(ButtonResult == 1)
3666 {
3667 const char *pClipboard = Input()->GetClipboardText();
3668 if(*pClipboard == '#' || *pClipboard == '$') // ignore leading # (web color format) and $ (console color format)
3669 ++pClipboard;
3670 if(str_isallnum_hex(str: pClipboard))
3671 {
3672 std::optional<ColorRGBA> ParsedColor = color_parse<ColorRGBA>(pStr: pClipboard);
3673 if(ParsedColor)
3674 {
3675 m_ColorPickerPopupContext.m_State = EEditState::ONE_GO;
3676 SetColor(ParsedColor.value());
3677 }
3678 }
3679 }
3680 else if(ButtonResult == 2)
3681 {
3682 char aClipboard[9];
3683 str_format(buffer: aClipboard, buffer_size: sizeof(aClipboard), format: "%08X", Color.PackAlphaLast());
3684 Input()->SetClipboardText(aClipboard);
3685 }
3686 }
3687 else if(ButtonResult > 0)
3688 {
3689 if(m_ColorPickerPopupContext.m_ColorMode == CUi::SColorPickerPopupContext::MODE_UNSET)
3690 m_ColorPickerPopupContext.m_ColorMode = CUi::SColorPickerPopupContext::MODE_RGBA;
3691 m_ColorPickerPopupContext.m_RgbaColor = Color;
3692 m_ColorPickerPopupContext.m_HslaColor = color_cast<ColorHSLA>(rgb: Color);
3693 m_ColorPickerPopupContext.m_HsvaColor = color_cast<ColorHSVA>(hsl: m_ColorPickerPopupContext.m_HslaColor);
3694 m_ColorPickerPopupContext.m_Alpha = true;
3695 m_pColorPickerPopupActiveId = pId;
3696 Ui()->ShowPopupColorPicker(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext: &m_ColorPickerPopupContext);
3697 }
3698
3699 if(Ui()->IsPopupOpen(pId: &m_ColorPickerPopupContext))
3700 {
3701 if(m_pColorPickerPopupActiveId == pId)
3702 SetColor(m_ColorPickerPopupContext.m_RgbaColor);
3703 }
3704 else
3705 {
3706 m_pColorPickerPopupActiveId = nullptr;
3707 if(m_ColorPickerPopupContext.m_State == EEditState::EDITING)
3708 {
3709 ColorRGBA c = color_cast<ColorRGBA>(hsv: m_ColorPickerPopupContext.m_HsvaColor);
3710 m_ColorPickerPopupContext.m_State = EEditState::END;
3711 SetColor(c);
3712 m_ColorPickerPopupContext.m_State = EEditState::NONE;
3713 }
3714 }
3715}
3716
3717void CEditor::RenderLayers(CUIRect LayersBox)
3718{
3719 const float RowHeight = 12.0f;
3720 char aBuf[64];
3721
3722 CUIRect UnscrolledLayersBox = LayersBox;
3723
3724 static CScrollRegion s_ScrollRegion;
3725 vec2 ScrollOffset(0.0f, 0.0f);
3726 CScrollRegionParams ScrollParams;
3727 ScrollParams.m_ScrollbarWidth = 10.0f;
3728 ScrollParams.m_ScrollbarMargin = 3.0f;
3729 ScrollParams.m_ScrollUnit = RowHeight * 5.0f;
3730 s_ScrollRegion.Begin(pClipRect: &LayersBox, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
3731 LayersBox.y += ScrollOffset.y;
3732
3733 enum
3734 {
3735 OP_NONE = 0,
3736 OP_CLICK,
3737 OP_LAYER_DRAG,
3738 OP_GROUP_DRAG
3739 };
3740 static int s_Operation = OP_NONE;
3741 static int s_PreviousOperation = OP_NONE;
3742 static const void *s_pDraggedButton = nullptr;
3743 static float s_InitialMouseY = 0;
3744 static float s_InitialCutHeight = 0;
3745 constexpr float MinDragDistance = 5.0f;
3746 int GroupAfterDraggedLayer = -1;
3747 int LayerAfterDraggedLayer = -1;
3748 bool DraggedPositionFound = false;
3749 bool MoveLayers = false;
3750 bool MoveGroup = false;
3751 bool StartDragLayer = false;
3752 bool StartDragGroup = false;
3753 std::vector<int> vButtonsPerGroup;
3754
3755 auto SetOperation = [](int Operation) {
3756 if(Operation != s_Operation)
3757 {
3758 s_PreviousOperation = s_Operation;
3759 s_Operation = Operation;
3760 if(Operation == OP_NONE)
3761 {
3762 s_pDraggedButton = nullptr;
3763 }
3764 }
3765 };
3766
3767 vButtonsPerGroup.reserve(n: m_Map.m_vpGroups.size());
3768 for(const std::shared_ptr<CLayerGroup> &pGroup : m_Map.m_vpGroups)
3769 {
3770 vButtonsPerGroup.push_back(x: pGroup->m_vpLayers.size() + 1);
3771 }
3772
3773 if(s_pDraggedButton != nullptr && Ui()->ActiveItem() != s_pDraggedButton)
3774 {
3775 SetOperation(OP_NONE);
3776 }
3777
3778 if(s_Operation == OP_LAYER_DRAG || s_Operation == OP_GROUP_DRAG)
3779 {
3780 float MinDraggableValue = UnscrolledLayersBox.y;
3781 float MaxDraggableValue = MinDraggableValue;
3782 for(int NumButtons : vButtonsPerGroup)
3783 {
3784 MaxDraggableValue += NumButtons * (RowHeight + 2.0f) + 5.0f;
3785 }
3786 MaxDraggableValue += ScrollOffset.y;
3787
3788 if(s_Operation == OP_GROUP_DRAG)
3789 {
3790 MaxDraggableValue -= vButtonsPerGroup[m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f;
3791 }
3792 else if(s_Operation == OP_LAYER_DRAG)
3793 {
3794 MinDraggableValue += RowHeight + 2.0f;
3795 MaxDraggableValue -= m_vSelectedLayers.size() * (RowHeight + 2.0f) + 5.0f;
3796 }
3797
3798 UnscrolledLayersBox.HSplitTop(Cut: s_InitialCutHeight, pTop: nullptr, pBottom: &UnscrolledLayersBox);
3799 UnscrolledLayersBox.y -= s_InitialMouseY - Ui()->MouseY();
3800
3801 UnscrolledLayersBox.y = clamp(val: UnscrolledLayersBox.y, lo: MinDraggableValue, hi: MaxDraggableValue);
3802
3803 UnscrolledLayersBox.w = LayersBox.w;
3804 }
3805
3806 static bool s_ScrollToSelectionNext = false;
3807 const bool ScrollToSelection = LayerSelector()->SelectByTile() || s_ScrollToSelectionNext;
3808 s_ScrollToSelectionNext = false;
3809
3810 // render layers
3811 for(int g = 0; g < (int)m_Map.m_vpGroups.size(); g++)
3812 {
3813 if(s_Operation == OP_LAYER_DRAG && g > 0 && !DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight / 2)
3814 {
3815 DraggedPositionFound = true;
3816 GroupAfterDraggedLayer = g;
3817
3818 LayerAfterDraggedLayer = m_Map.m_vpGroups[g - 1]->m_vpLayers.size();
3819
3820 CUIRect Slot;
3821 LayersBox.HSplitTop(Cut: m_vSelectedLayers.size() * (RowHeight + 2.0f), pTop: &Slot, pBottom: &LayersBox);
3822 s_ScrollRegion.AddRect(Rect: Slot);
3823 }
3824
3825 CUIRect Slot, VisibleToggle;
3826 if(s_Operation == OP_GROUP_DRAG)
3827 {
3828 if(g == m_SelectedGroup)
3829 {
3830 UnscrolledLayersBox.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: &UnscrolledLayersBox);
3831 UnscrolledLayersBox.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &UnscrolledLayersBox);
3832 }
3833 else if(!DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight * vButtonsPerGroup[g] / 2 + 3.0f)
3834 {
3835 DraggedPositionFound = true;
3836 GroupAfterDraggedLayer = g;
3837
3838 CUIRect TmpSlot;
3839 if(m_Map.m_vpGroups[m_SelectedGroup]->m_Collapse)
3840 LayersBox.HSplitTop(Cut: RowHeight + 7.0f, pTop: &TmpSlot, pBottom: &LayersBox);
3841 else
3842 LayersBox.HSplitTop(Cut: vButtonsPerGroup[m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f, pTop: &TmpSlot, pBottom: &LayersBox);
3843 s_ScrollRegion.AddRect(Rect: TmpSlot, ShouldScrollHere: false);
3844 }
3845 }
3846 if(s_Operation != OP_GROUP_DRAG || g != m_SelectedGroup)
3847 {
3848 LayersBox.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: &LayersBox);
3849
3850 CUIRect TmpRect;
3851 LayersBox.HSplitTop(Cut: 2.0f, pTop: &TmpRect, pBottom: &LayersBox);
3852 s_ScrollRegion.AddRect(Rect: TmpRect);
3853 }
3854
3855 if(s_ScrollRegion.AddRect(Rect: Slot))
3856 {
3857 Slot.VSplitLeft(Cut: 15.0f, pLeft: &VisibleToggle, pRight: &Slot);
3858 if(DoButton_FontIcon(pId: &m_Map.m_vpGroups[g]->m_Visible, pText: m_Map.m_vpGroups[g]->m_Visible ? FONT_ICON_EYE : FONT_ICON_EYE_SLASH, Checked: m_Map.m_vpGroups[g]->m_Collapse ? 1 : 0, pRect: &VisibleToggle, Flags: 0, pToolTip: "Toggle group visibility", Corners: IGraphics::CORNER_L, FontSize: 8.0f))
3859 m_Map.m_vpGroups[g]->m_Visible = !m_Map.m_vpGroups[g]->m_Visible;
3860
3861 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "#%d %s", g, m_Map.m_vpGroups[g]->m_aName);
3862
3863 bool Clicked;
3864 bool Abrupted;
3865 if(int Result = DoButton_DraggableEx(pId: m_Map.m_vpGroups[g].get(), pText: aBuf, Checked: g == m_SelectedGroup, pRect: &Slot, pClicked: &Clicked, pAbrupted: &Abrupted,
3866 Flags: BUTTON_CONTEXT, pToolTip: m_Map.m_vpGroups[g]->m_Collapse ? "Select group. Shift click to select all layers. Double click to expand." : "Select group. Shift click to select all layers. Double click to collapse.", Corners: IGraphics::CORNER_R))
3867 {
3868 if(s_Operation == OP_NONE)
3869 {
3870 s_InitialMouseY = Ui()->MouseY();
3871 s_InitialCutHeight = s_InitialMouseY - UnscrolledLayersBox.y;
3872 SetOperation(OP_CLICK);
3873
3874 if(g != m_SelectedGroup)
3875 SelectLayer(LayerIndex: 0, GroupIndex: g);
3876 }
3877
3878 if(Abrupted)
3879 {
3880 SetOperation(OP_NONE);
3881 }
3882
3883 if(s_Operation == OP_CLICK && absolute(a: Ui()->MouseY() - s_InitialMouseY) > MinDragDistance)
3884 {
3885 StartDragGroup = true;
3886 s_pDraggedButton = m_Map.m_vpGroups[g].get();
3887 }
3888
3889 if(s_Operation == OP_CLICK && Clicked)
3890 {
3891 if(g != m_SelectedGroup)
3892 SelectLayer(LayerIndex: 0, GroupIndex: g);
3893
3894 if(Input()->ShiftIsPressed() && m_SelectedGroup == g)
3895 {
3896 m_vSelectedLayers.clear();
3897 for(size_t i = 0; i < m_Map.m_vpGroups[g]->m_vpLayers.size(); i++)
3898 {
3899 AddSelectedLayer(LayerIndex: i);
3900 }
3901 }
3902
3903 if(Result == 2)
3904 {
3905 static SPopupMenuId s_PopupGroupId;
3906 Ui()->DoPopupMenu(pId: &s_PopupGroupId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 145, Height: 256, pContext: this, pfnFunc: PopupGroup);
3907 }
3908
3909 if(!m_Map.m_vpGroups[g]->m_vpLayers.empty() && Input()->MouseDoubleClick())
3910 m_Map.m_vpGroups[g]->m_Collapse ^= 1;
3911
3912 SetOperation(OP_NONE);
3913 }
3914
3915 if(s_Operation == OP_GROUP_DRAG && Clicked)
3916 MoveGroup = true;
3917 }
3918 else if(s_pDraggedButton == m_Map.m_vpGroups[g].get())
3919 {
3920 SetOperation(OP_NONE);
3921 }
3922 }
3923
3924 for(int i = 0; i < (int)m_Map.m_vpGroups[g]->m_vpLayers.size(); i++)
3925 {
3926 if(m_Map.m_vpGroups[g]->m_Collapse)
3927 continue;
3928
3929 bool IsLayerSelected = false;
3930 if(m_SelectedGroup == g)
3931 {
3932 for(const auto &Selected : m_vSelectedLayers)
3933 {
3934 if(Selected == i)
3935 {
3936 IsLayerSelected = true;
3937 break;
3938 }
3939 }
3940 }
3941
3942 if(s_Operation == OP_GROUP_DRAG && g == m_SelectedGroup)
3943 {
3944 UnscrolledLayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &UnscrolledLayersBox);
3945 }
3946 else if(s_Operation == OP_LAYER_DRAG)
3947 {
3948 if(IsLayerSelected)
3949 {
3950 UnscrolledLayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &UnscrolledLayersBox);
3951 }
3952 else
3953 {
3954 if(!DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight / 2)
3955 {
3956 DraggedPositionFound = true;
3957 GroupAfterDraggedLayer = g + 1;
3958 LayerAfterDraggedLayer = i;
3959 for(size_t j = 0; j < m_vSelectedLayers.size(); j++)
3960 {
3961 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: nullptr, pBottom: &LayersBox);
3962 s_ScrollRegion.AddRect(Rect: Slot);
3963 }
3964 }
3965 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &LayersBox);
3966 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: ScrollToSelection && IsLayerSelected))
3967 continue;
3968 }
3969 }
3970 else
3971 {
3972 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &LayersBox);
3973 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: ScrollToSelection && IsLayerSelected))
3974 continue;
3975 }
3976
3977 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
3978
3979 CUIRect Button;
3980 Slot.VSplitLeft(Cut: 12.0f, pLeft: nullptr, pRight: &Slot);
3981 Slot.VSplitLeft(Cut: 15.0f, pLeft: &VisibleToggle, pRight: &Button);
3982
3983 if(DoButton_FontIcon(pId: &m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible, pText: m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible ? FONT_ICON_EYE : FONT_ICON_EYE_SLASH, Checked: 0, pRect: &VisibleToggle, Flags: 0, pToolTip: "Toggle layer visibility", Corners: IGraphics::CORNER_L, FontSize: 8.0f))
3984 m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible = !m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible;
3985
3986 if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_aName[0])
3987 str_copy(dst&: aBuf, src: m_Map.m_vpGroups[g]->m_vpLayers[i]->m_aName);
3988 else
3989 {
3990 if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_TILES)
3991 {
3992 std::shared_ptr<CLayerTiles> pTiles = std::static_pointer_cast<CLayerTiles>(r: m_Map.m_vpGroups[g]->m_vpLayers[i]);
3993 str_copy(dst&: aBuf, src: pTiles->m_Image >= 0 ? m_Map.m_vpImages[pTiles->m_Image]->m_aName : "Tiles");
3994 }
3995 else if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_QUADS)
3996 {
3997 std::shared_ptr<CLayerQuads> pQuads = std::static_pointer_cast<CLayerQuads>(r: m_Map.m_vpGroups[g]->m_vpLayers[i]);
3998 str_copy(dst&: aBuf, src: pQuads->m_Image >= 0 ? m_Map.m_vpImages[pQuads->m_Image]->m_aName : "Quads");
3999 }
4000 else if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_SOUNDS)
4001 {
4002 std::shared_ptr<CLayerSounds> pSounds = std::static_pointer_cast<CLayerSounds>(r: m_Map.m_vpGroups[g]->m_vpLayers[i]);
4003 str_copy(dst&: aBuf, src: pSounds->m_Sound >= 0 ? m_Map.m_vpSounds[pSounds->m_Sound]->m_aName : "Sounds");
4004 }
4005 }
4006
4007 int Checked = IsLayerSelected ? 1 : 0;
4008 if(m_Map.m_vpGroups[g]->m_vpLayers[i]->IsEntitiesLayer())
4009 {
4010 Checked += 6;
4011 }
4012
4013 bool Clicked;
4014 bool Abrupted;
4015 if(int Result = DoButton_DraggableEx(pId: m_Map.m_vpGroups[g]->m_vpLayers[i].get(), pText: aBuf, Checked, pRect: &Button, pClicked: &Clicked, pAbrupted: &Abrupted,
4016 Flags: BUTTON_CONTEXT, pToolTip: "Select layer. Shift click to select multiple.", Corners: IGraphics::CORNER_R))
4017 {
4018 if(s_Operation == OP_NONE)
4019 {
4020 s_InitialMouseY = Ui()->MouseY();
4021 s_InitialCutHeight = s_InitialMouseY - UnscrolledLayersBox.y;
4022
4023 SetOperation(OP_CLICK);
4024
4025 if(!Input()->ShiftIsPressed() && !IsLayerSelected)
4026 {
4027 SelectLayer(LayerIndex: i, GroupIndex: g);
4028 }
4029 }
4030
4031 if(Abrupted)
4032 {
4033 SetOperation(OP_NONE);
4034 }
4035
4036 if(s_Operation == OP_CLICK && absolute(a: Ui()->MouseY() - s_InitialMouseY) > MinDragDistance)
4037 {
4038 bool EntitiesLayerSelected = false;
4039 for(int k : m_vSelectedLayers)
4040 {
4041 if(m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[k]->IsEntitiesLayer())
4042 EntitiesLayerSelected = true;
4043 }
4044
4045 if(!EntitiesLayerSelected)
4046 StartDragLayer = true;
4047
4048 s_pDraggedButton = m_Map.m_vpGroups[g]->m_vpLayers[i].get();
4049 }
4050
4051 if(s_Operation == OP_CLICK && Clicked)
4052 {
4053 static SLayerPopupContext s_LayerPopupContext = {};
4054 s_LayerPopupContext.m_pEditor = this;
4055 if(Result == 1)
4056 {
4057 if(Input()->ShiftIsPressed() && m_SelectedGroup == g)
4058 {
4059 auto Position = std::find(first: m_vSelectedLayers.begin(), last: m_vSelectedLayers.end(), val: i);
4060 if(Position != m_vSelectedLayers.end())
4061 m_vSelectedLayers.erase(position: Position);
4062 else
4063 AddSelectedLayer(LayerIndex: i);
4064 }
4065 else if(!Input()->ShiftIsPressed())
4066 {
4067 SelectLayer(LayerIndex: i, GroupIndex: g);
4068 }
4069 }
4070 else if(Result == 2)
4071 {
4072 s_LayerPopupContext.m_vpLayers.clear();
4073 s_LayerPopupContext.m_vLayerIndices.clear();
4074
4075 if(!IsLayerSelected)
4076 {
4077 SelectLayer(LayerIndex: i, GroupIndex: g);
4078 }
4079
4080 if(m_vSelectedLayers.size() > 1)
4081 {
4082 bool AllTile = true;
4083 for(size_t j = 0; AllTile && j < m_vSelectedLayers.size(); j++)
4084 {
4085 int LayerIndex = m_vSelectedLayers[j];
4086 if(m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[LayerIndex]->m_Type == LAYERTYPE_TILES)
4087 {
4088 s_LayerPopupContext.m_vpLayers.push_back(x: std::static_pointer_cast<CLayerTiles>(r: m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[m_vSelectedLayers[j]]));
4089 s_LayerPopupContext.m_vLayerIndices.push_back(x: LayerIndex);
4090 }
4091 else
4092 AllTile = false;
4093 }
4094
4095 // Don't allow editing if all selected layers are not tile layers
4096 if(!AllTile)
4097 {
4098 s_LayerPopupContext.m_vpLayers.clear();
4099 s_LayerPopupContext.m_vLayerIndices.clear();
4100 }
4101 }
4102
4103 Ui()->DoPopupMenu(pId: &s_LayerPopupContext, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 270, pContext: &s_LayerPopupContext, pfnFunc: PopupLayer);
4104 }
4105
4106 SetOperation(OP_NONE);
4107 }
4108
4109 if(s_Operation == OP_LAYER_DRAG && Clicked)
4110 {
4111 MoveLayers = true;
4112 }
4113 }
4114 else if(s_pDraggedButton == m_Map.m_vpGroups[g]->m_vpLayers[i].get())
4115 {
4116 SetOperation(OP_NONE);
4117 }
4118 }
4119
4120 if(s_Operation != OP_GROUP_DRAG || g != m_SelectedGroup)
4121 {
4122 LayersBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &LayersBox);
4123 s_ScrollRegion.AddRect(Rect: Slot);
4124 }
4125 }
4126
4127 if(!DraggedPositionFound && s_Operation == OP_LAYER_DRAG)
4128 {
4129 GroupAfterDraggedLayer = m_Map.m_vpGroups.size();
4130 LayerAfterDraggedLayer = m_Map.m_vpGroups[GroupAfterDraggedLayer - 1]->m_vpLayers.size();
4131
4132 CUIRect TmpSlot;
4133 LayersBox.HSplitTop(Cut: m_vSelectedLayers.size() * (RowHeight + 2.0f), pTop: &TmpSlot, pBottom: &LayersBox);
4134 s_ScrollRegion.AddRect(Rect: TmpSlot);
4135 }
4136
4137 if(!DraggedPositionFound && s_Operation == OP_GROUP_DRAG)
4138 {
4139 GroupAfterDraggedLayer = m_Map.m_vpGroups.size();
4140
4141 CUIRect TmpSlot;
4142 if(m_Map.m_vpGroups[m_SelectedGroup]->m_Collapse)
4143 LayersBox.HSplitTop(Cut: RowHeight + 7.0f, pTop: &TmpSlot, pBottom: &LayersBox);
4144 else
4145 LayersBox.HSplitTop(Cut: vButtonsPerGroup[m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f, pTop: &TmpSlot, pBottom: &LayersBox);
4146 s_ScrollRegion.AddRect(Rect: TmpSlot, ShouldScrollHere: false);
4147 }
4148
4149 if(MoveLayers && 1 <= GroupAfterDraggedLayer && GroupAfterDraggedLayer <= (int)m_Map.m_vpGroups.size())
4150 {
4151 std::vector<std::shared_ptr<CLayer>> &vpNewGroupLayers = m_Map.m_vpGroups[GroupAfterDraggedLayer - 1]->m_vpLayers;
4152 if(0 <= LayerAfterDraggedLayer && LayerAfterDraggedLayer <= (int)vpNewGroupLayers.size())
4153 {
4154 std::vector<std::shared_ptr<CLayer>> vpSelectedLayers;
4155 std::vector<std::shared_ptr<CLayer>> &vpSelectedGroupLayers = m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers;
4156 std::shared_ptr<CLayer> pNextLayer = nullptr;
4157 if(LayerAfterDraggedLayer < (int)vpNewGroupLayers.size())
4158 pNextLayer = vpNewGroupLayers[LayerAfterDraggedLayer];
4159
4160 std::sort(first: m_vSelectedLayers.begin(), last: m_vSelectedLayers.end(), comp: std::greater<>());
4161 for(int k : m_vSelectedLayers)
4162 {
4163 vpSelectedLayers.insert(position: vpSelectedLayers.begin(), x: vpSelectedGroupLayers[k]);
4164 }
4165 for(int k : m_vSelectedLayers)
4166 {
4167 vpSelectedGroupLayers.erase(position: vpSelectedGroupLayers.begin() + k);
4168 }
4169
4170 auto InsertPosition = std::find(first: vpNewGroupLayers.begin(), last: vpNewGroupLayers.end(), val: pNextLayer);
4171 int InsertPositionIndex = InsertPosition - vpNewGroupLayers.begin();
4172 vpNewGroupLayers.insert(position: InsertPosition, first: vpSelectedLayers.begin(), last: vpSelectedLayers.end());
4173
4174 int NumSelectedLayers = m_vSelectedLayers.size();
4175 m_vSelectedLayers.clear();
4176 for(int i = 0; i < NumSelectedLayers; i++)
4177 m_vSelectedLayers.push_back(x: InsertPositionIndex + i);
4178
4179 m_SelectedGroup = GroupAfterDraggedLayer - 1;
4180 m_Map.OnModify();
4181 }
4182 }
4183
4184 if(MoveGroup && 0 <= GroupAfterDraggedLayer && GroupAfterDraggedLayer <= (int)m_Map.m_vpGroups.size())
4185 {
4186 std::shared_ptr<CLayerGroup> pSelectedGroup = m_Map.m_vpGroups[m_SelectedGroup];
4187 std::shared_ptr<CLayerGroup> pNextGroup = nullptr;
4188 if(GroupAfterDraggedLayer < (int)m_Map.m_vpGroups.size())
4189 pNextGroup = m_Map.m_vpGroups[GroupAfterDraggedLayer];
4190
4191 m_Map.m_vpGroups.erase(position: m_Map.m_vpGroups.begin() + m_SelectedGroup);
4192
4193 auto InsertPosition = std::find(first: m_Map.m_vpGroups.begin(), last: m_Map.m_vpGroups.end(), val: pNextGroup);
4194 m_Map.m_vpGroups.insert(position: InsertPosition, x: pSelectedGroup);
4195
4196 auto Pos = std::find(first: m_Map.m_vpGroups.begin(), last: m_Map.m_vpGroups.end(), val: pSelectedGroup);
4197 m_SelectedGroup = Pos - m_Map.m_vpGroups.begin();
4198
4199 m_Map.OnModify();
4200 }
4201
4202 static int s_InitialGroupIndex;
4203 static std::vector<int> s_vInitialLayerIndices;
4204
4205 if(MoveLayers || MoveGroup)
4206 {
4207 SetOperation(OP_NONE);
4208 }
4209 if(StartDragLayer)
4210 {
4211 SetOperation(OP_LAYER_DRAG);
4212 s_InitialGroupIndex = m_SelectedGroup;
4213 s_vInitialLayerIndices = std::vector(m_vSelectedLayers);
4214 }
4215 if(StartDragGroup)
4216 {
4217 s_InitialGroupIndex = m_SelectedGroup;
4218 SetOperation(OP_GROUP_DRAG);
4219 }
4220
4221 if(s_Operation == OP_LAYER_DRAG || s_Operation == OP_GROUP_DRAG)
4222 {
4223 if(s_pDraggedButton == nullptr)
4224 {
4225 SetOperation(OP_NONE);
4226 }
4227 else
4228 {
4229 s_ScrollRegion.DoEdgeScrolling();
4230 Ui()->SetActiveItem(s_pDraggedButton);
4231 }
4232 }
4233
4234 if(Input()->KeyPress(Key: KEY_DOWN) && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && CLineInput::GetActiveInput() == nullptr && s_Operation == OP_NONE)
4235 {
4236 if(Input()->ShiftIsPressed())
4237 {
4238 if(m_vSelectedLayers[m_vSelectedLayers.size() - 1] < (int)m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1)
4239 AddSelectedLayer(LayerIndex: m_vSelectedLayers[m_vSelectedLayers.size() - 1] + 1);
4240 }
4241 else
4242 {
4243 int CurrentLayer = 0;
4244 for(const auto &Selected : m_vSelectedLayers)
4245 CurrentLayer = maximum(a: Selected, b: CurrentLayer);
4246 SelectLayer(LayerIndex: CurrentLayer);
4247
4248 if(m_vSelectedLayers[0] < (int)m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1)
4249 {
4250 SelectLayer(LayerIndex: m_vSelectedLayers[0] + 1);
4251 }
4252 else
4253 {
4254 for(size_t Group = m_SelectedGroup + 1; Group < m_Map.m_vpGroups.size(); Group++)
4255 {
4256 if(!m_Map.m_vpGroups[Group]->m_vpLayers.empty())
4257 {
4258 SelectLayer(LayerIndex: 0, GroupIndex: Group);
4259 break;
4260 }
4261 }
4262 }
4263 }
4264 s_ScrollToSelectionNext = true;
4265 }
4266 if(Input()->KeyPress(Key: KEY_UP) && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && CLineInput::GetActiveInput() == nullptr && s_Operation == OP_NONE)
4267 {
4268 if(Input()->ShiftIsPressed())
4269 {
4270 if(m_vSelectedLayers[m_vSelectedLayers.size() - 1] > 0)
4271 AddSelectedLayer(LayerIndex: m_vSelectedLayers[m_vSelectedLayers.size() - 1] - 1);
4272 }
4273 else
4274 {
4275 int CurrentLayer = std::numeric_limits<int>::max();
4276 for(const auto &Selected : m_vSelectedLayers)
4277 CurrentLayer = minimum(a: Selected, b: CurrentLayer);
4278 SelectLayer(LayerIndex: CurrentLayer);
4279
4280 if(m_vSelectedLayers[0] > 0)
4281 {
4282 SelectLayer(LayerIndex: m_vSelectedLayers[0] - 1);
4283 }
4284 else
4285 {
4286 for(int Group = m_SelectedGroup - 1; Group >= 0; Group--)
4287 {
4288 if(!m_Map.m_vpGroups[Group]->m_vpLayers.empty())
4289 {
4290 SelectLayer(LayerIndex: m_Map.m_vpGroups[Group]->m_vpLayers.size() - 1, GroupIndex: Group);
4291 break;
4292 }
4293 }
4294 }
4295 }
4296 s_ScrollToSelectionNext = true;
4297 }
4298
4299 CUIRect AddGroupButton;
4300 LayersBox.HSplitTop(Cut: RowHeight + 1.0f, pTop: &AddGroupButton, pBottom: &LayersBox);
4301 if(s_ScrollRegion.AddRect(Rect: AddGroupButton))
4302 {
4303 AddGroupButton.HSplitTop(Cut: RowHeight, pTop: &AddGroupButton, pBottom: 0);
4304 static int s_AddGroupButton = 0;
4305 if(DoButton_Editor(pId: &s_AddGroupButton, pText: "Add group", Checked: 0, pRect: &AddGroupButton, Flags: IGraphics::CORNER_R, pToolTip: "Adds a new group"))
4306 {
4307 m_Map.NewGroup();
4308 m_SelectedGroup = m_Map.m_vpGroups.size() - 1;
4309 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionGroup>(args: this, args&: m_SelectedGroup, args: false));
4310 }
4311 }
4312
4313 s_ScrollRegion.End();
4314
4315 if(s_Operation == OP_NONE)
4316 {
4317 if(s_PreviousOperation == OP_GROUP_DRAG)
4318 {
4319 s_PreviousOperation = OP_NONE;
4320 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEditGroupProp>(args: this, args&: m_SelectedGroup, args: EGroupProp::PROP_ORDER, args&: s_InitialGroupIndex, args&: m_SelectedGroup));
4321 }
4322 else if(s_PreviousOperation == OP_LAYER_DRAG)
4323 {
4324 if(s_InitialGroupIndex != m_SelectedGroup)
4325 {
4326 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEditLayersGroupAndOrder>(args: this, args&: s_InitialGroupIndex, args&: s_vInitialLayerIndices, args&: m_SelectedGroup, args&: m_vSelectedLayers));
4327 }
4328 else
4329 {
4330 std::vector<std::shared_ptr<IEditorAction>> vpActions;
4331 std::vector<int> vLayerIndices = m_vSelectedLayers;
4332 std::sort(first: vLayerIndices.begin(), last: vLayerIndices.end());
4333 std::sort(first: s_vInitialLayerIndices.begin(), last: s_vInitialLayerIndices.end());
4334 for(int k = 0; k < (int)vLayerIndices.size(); k++)
4335 {
4336 int LayerIndex = vLayerIndices[k];
4337 vpActions.push_back(x: std::make_shared<CEditorActionEditLayerProp>(args: this, args&: m_SelectedGroup, args&: LayerIndex, args: ELayerProp::PROP_ORDER, args&: s_vInitialLayerIndices[k], args&: LayerIndex));
4338 }
4339 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionBulk>(args: this, args&: vpActions, args: nullptr, args: true));
4340 }
4341 s_PreviousOperation = OP_NONE;
4342 }
4343 }
4344}
4345
4346bool CEditor::ReplaceImage(const char *pFileName, int StorageType, bool CheckDuplicate)
4347{
4348 // check if we have that image already
4349 char aBuf[128];
4350 IStorage::StripPathAndExtension(pFilename: pFileName, pBuffer: aBuf, BufferSize: sizeof(aBuf));
4351 if(CheckDuplicate)
4352 {
4353 for(const auto &pImage : m_Map.m_vpImages)
4354 {
4355 if(!str_comp(a: pImage->m_aName, b: aBuf))
4356 {
4357 ShowFileDialogError(pFormat: "Image named '%s' was already added.", pImage->m_aName);
4358 return false;
4359 }
4360 }
4361 }
4362
4363 CEditorImage ImgInfo(this);
4364 if(!Graphics()->LoadPng(Image&: ImgInfo, pFilename: pFileName, StorageType))
4365 {
4366 ShowFileDialogError(pFormat: "Failed to load image from file '%s'.", pFileName);
4367 return false;
4368 }
4369
4370 std::shared_ptr<CEditorImage> pImg = m_Map.m_vpImages[m_SelectedImage];
4371 Graphics()->UnloadTexture(pIndex: &(pImg->m_Texture));
4372 pImg->Free();
4373 *pImg = ImgInfo;
4374 str_copy(dst&: pImg->m_aName, src: aBuf);
4375 pImg->m_External = IsVanillaImage(pImage: pImg->m_aName);
4376
4377 if(!pImg->m_External && g_Config.m_ClEditorDilate == 1 && pImg->m_Format == CImageInfo::FORMAT_RGBA)
4378 {
4379 DilateImage(pImageBuff: ImgInfo.m_pData, w: ImgInfo.m_Width, h: ImgInfo.m_Height);
4380 }
4381
4382 pImg->m_AutoMapper.Load(pTileName: pImg->m_aName);
4383 int TextureLoadFlag = Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
4384 if(ImgInfo.m_Width % 16 != 0 || ImgInfo.m_Height % 16 != 0)
4385 TextureLoadFlag = 0;
4386 pImg->m_Texture = Graphics()->LoadTextureRaw(Image: ImgInfo, Flags: TextureLoadFlag, pTexName: pFileName);
4387 ImgInfo.m_pData = nullptr;
4388 SortImages();
4389 for(size_t i = 0; i < m_Map.m_vpImages.size(); ++i)
4390 {
4391 if(!str_comp(a: m_Map.m_vpImages[i]->m_aName, b: pImg->m_aName))
4392 m_SelectedImage = i;
4393 }
4394 m_Dialog = DIALOG_NONE;
4395 return true;
4396}
4397
4398bool CEditor::ReplaceImageCallback(const char *pFileName, int StorageType, void *pUser)
4399{
4400 return static_cast<CEditor *>(pUser)->ReplaceImage(pFileName, StorageType, CheckDuplicate: true);
4401}
4402
4403bool CEditor::AddImage(const char *pFileName, int StorageType, void *pUser)
4404{
4405 CEditor *pEditor = (CEditor *)pUser;
4406
4407 // check if we have that image already
4408 char aBuf[128];
4409 IStorage::StripPathAndExtension(pFilename: pFileName, pBuffer: aBuf, BufferSize: sizeof(aBuf));
4410 for(const auto &pImage : pEditor->m_Map.m_vpImages)
4411 {
4412 if(!str_comp(a: pImage->m_aName, b: aBuf))
4413 {
4414 pEditor->ShowFileDialogError(pFormat: "Image named '%s' was already added.", pImage->m_aName);
4415 return false;
4416 }
4417 }
4418
4419 if(pEditor->m_Map.m_vpImages.size() >= MAX_MAPIMAGES)
4420 {
4421 pEditor->m_PopupEventType = POPEVENT_IMAGE_MAX;
4422 pEditor->m_PopupEventActivated = true;
4423 return false;
4424 }
4425
4426 CEditorImage ImgInfo(pEditor);
4427 if(!pEditor->Graphics()->LoadPng(Image&: ImgInfo, pFilename: pFileName, StorageType))
4428 {
4429 pEditor->ShowFileDialogError(pFormat: "Failed to load image from file '%s'.", pFileName);
4430 return false;
4431 }
4432
4433 std::shared_ptr<CEditorImage> pImg = std::make_shared<CEditorImage>(args&: pEditor);
4434 *pImg = ImgInfo;
4435 pImg->m_External = IsVanillaImage(pImage: aBuf);
4436
4437 if(!pImg->m_External && g_Config.m_ClEditorDilate == 1 && pImg->m_Format == CImageInfo::FORMAT_RGBA)
4438 {
4439 DilateImage(pImageBuff: ImgInfo.m_pData, w: ImgInfo.m_Width, h: ImgInfo.m_Height);
4440 }
4441
4442 int TextureLoadFlag = pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
4443 if(ImgInfo.m_Width % 16 != 0 || ImgInfo.m_Height % 16 != 0)
4444 TextureLoadFlag = 0;
4445 pImg->m_Texture = pEditor->Graphics()->LoadTextureRaw(Image: ImgInfo, Flags: TextureLoadFlag, pTexName: pFileName);
4446 ImgInfo.m_pData = nullptr;
4447 str_copy(dst&: pImg->m_aName, src: aBuf);
4448 pImg->m_AutoMapper.Load(pTileName: pImg->m_aName);
4449 pEditor->m_Map.m_vpImages.push_back(x: pImg);
4450 pEditor->SortImages();
4451 if(pEditor->m_SelectedImage >= 0 && (size_t)pEditor->m_SelectedImage < pEditor->m_Map.m_vpImages.size())
4452 {
4453 for(int i = 0; i <= pEditor->m_SelectedImage; ++i)
4454 if(!str_comp(a: pEditor->m_Map.m_vpImages[i]->m_aName, b: aBuf))
4455 {
4456 pEditor->m_SelectedImage++;
4457 break;
4458 }
4459 }
4460 pEditor->m_Dialog = DIALOG_NONE;
4461 return true;
4462}
4463
4464bool CEditor::AddSound(const char *pFileName, int StorageType, void *pUser)
4465{
4466 CEditor *pEditor = (CEditor *)pUser;
4467
4468 // check if we have that sound already
4469 char aBuf[128];
4470 IStorage::StripPathAndExtension(pFilename: pFileName, pBuffer: aBuf, BufferSize: sizeof(aBuf));
4471 for(const auto &pSound : pEditor->m_Map.m_vpSounds)
4472 {
4473 if(!str_comp(a: pSound->m_aName, b: aBuf))
4474 {
4475 pEditor->ShowFileDialogError(pFormat: "Sound named '%s' was already added.", pSound->m_aName);
4476 return false;
4477 }
4478 }
4479
4480 if(pEditor->m_Map.m_vpSounds.size() >= MAX_MAPSOUNDS)
4481 {
4482 pEditor->m_PopupEventType = POPEVENT_SOUND_MAX;
4483 pEditor->m_PopupEventActivated = true;
4484 return false;
4485 }
4486
4487 // load external
4488 void *pData;
4489 unsigned DataSize;
4490 if(!pEditor->Storage()->ReadFile(pFilename: pFileName, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
4491 {
4492 pEditor->ShowFileDialogError(pFormat: "Failed to open sound file '%s'.", pFileName);
4493 return false;
4494 }
4495
4496 // load sound
4497 const int SoundId = pEditor->Sound()->LoadOpusFromMem(pData, DataSize, FromEditor: true);
4498 if(SoundId == -1)
4499 {
4500 free(ptr: pData);
4501 pEditor->ShowFileDialogError(pFormat: "Failed to load sound from file '%s'.", pFileName);
4502 return false;
4503 }
4504
4505 // add sound
4506 std::shared_ptr<CEditorSound> pSound = std::make_shared<CEditorSound>(args&: pEditor);
4507 pSound->m_SoundId = SoundId;
4508 pSound->m_DataSize = DataSize;
4509 pSound->m_pData = pData;
4510 str_copy(dst&: pSound->m_aName, src: aBuf);
4511 pEditor->m_Map.m_vpSounds.push_back(x: pSound);
4512
4513 if(pEditor->m_SelectedSound >= 0 && (size_t)pEditor->m_SelectedSound < pEditor->m_Map.m_vpSounds.size())
4514 {
4515 for(int i = 0; i <= pEditor->m_SelectedSound; ++i)
4516 if(!str_comp(a: pEditor->m_Map.m_vpSounds[i]->m_aName, b: aBuf))
4517 {
4518 pEditor->m_SelectedSound++;
4519 break;
4520 }
4521 }
4522
4523 pEditor->OnDialogClose();
4524 pEditor->m_Dialog = DIALOG_NONE;
4525 return true;
4526}
4527
4528bool CEditor::ReplaceSound(const char *pFileName, int StorageType, bool CheckDuplicate)
4529{
4530 // check if we have that sound already
4531 char aBuf[128];
4532 IStorage::StripPathAndExtension(pFilename: pFileName, pBuffer: aBuf, BufferSize: sizeof(aBuf));
4533 if(CheckDuplicate)
4534 {
4535 for(const auto &pSound : m_Map.m_vpSounds)
4536 {
4537 if(!str_comp(a: pSound->m_aName, b: aBuf))
4538 {
4539 ShowFileDialogError(pFormat: "Sound named '%s' was already added.", pSound->m_aName);
4540 return false;
4541 }
4542 }
4543 }
4544
4545 // load external
4546 void *pData;
4547 unsigned DataSize;
4548 if(!Storage()->ReadFile(pFilename: pFileName, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
4549 {
4550 ShowFileDialogError(pFormat: "Failed to open sound file '%s'.", pFileName);
4551 return false;
4552 }
4553
4554 // load sound
4555 const int SoundId = Sound()->LoadOpusFromMem(pData, DataSize, FromEditor: true);
4556 if(SoundId == -1)
4557 {
4558 free(ptr: pData);
4559 ShowFileDialogError(pFormat: "Failed to load sound from file '%s'.", pFileName);
4560 return false;
4561 }
4562
4563 std::shared_ptr<CEditorSound> pSound = m_Map.m_vpSounds[m_SelectedSound];
4564
4565 // unload sample
4566 Sound()->UnloadSample(SampleId: pSound->m_SoundId);
4567 free(ptr: pSound->m_pData);
4568
4569 // replace sound
4570 str_copy(dst&: pSound->m_aName, src: aBuf);
4571 pSound->m_SoundId = SoundId;
4572 pSound->m_pData = pData;
4573 pSound->m_DataSize = DataSize;
4574
4575 OnDialogClose();
4576 m_Dialog = DIALOG_NONE;
4577 return true;
4578}
4579
4580bool CEditor::ReplaceSoundCallback(const char *pFileName, int StorageType, void *pUser)
4581{
4582 return static_cast<CEditor *>(pUser)->ReplaceSound(pFileName, StorageType, CheckDuplicate: true);
4583}
4584
4585void CEditor::SelectGameLayer()
4586{
4587 for(size_t g = 0; g < m_Map.m_vpGroups.size(); g++)
4588 {
4589 for(size_t i = 0; i < m_Map.m_vpGroups[g]->m_vpLayers.size(); i++)
4590 {
4591 if(m_Map.m_vpGroups[g]->m_vpLayers[i] == m_Map.m_pGameLayer)
4592 {
4593 SelectLayer(LayerIndex: i, GroupIndex: g);
4594 return;
4595 }
4596 }
4597 }
4598}
4599
4600std::vector<int> CEditor::SortImages()
4601{
4602 static const auto &&s_ImageNameComparator = [](const std::shared_ptr<CEditorImage> &pLhs, const std::shared_ptr<CEditorImage> &pRhs) {
4603 return str_comp(a: pLhs->m_aName, b: pRhs->m_aName) < 0;
4604 };
4605 if(!std::is_sorted(first: m_Map.m_vpImages.begin(), last: m_Map.m_vpImages.end(), comp: s_ImageNameComparator))
4606 {
4607 const std::vector<std::shared_ptr<CEditorImage>> vpTemp = m_Map.m_vpImages;
4608 std::vector<int> vSortedIndex;
4609 vSortedIndex.resize(new_size: vpTemp.size());
4610
4611 std::sort(first: m_Map.m_vpImages.begin(), last: m_Map.m_vpImages.end(), comp: s_ImageNameComparator);
4612 for(size_t OldIndex = 0; OldIndex < vpTemp.size(); OldIndex++)
4613 {
4614 for(size_t NewIndex = 0; NewIndex < m_Map.m_vpImages.size(); NewIndex++)
4615 {
4616 if(vpTemp[OldIndex] == m_Map.m_vpImages[NewIndex])
4617 {
4618 vSortedIndex[OldIndex] = NewIndex;
4619 break;
4620 }
4621 }
4622 }
4623 m_Map.ModifyImageIndex(pfnFunc: [vSortedIndex](int *pIndex) {
4624 if(*pIndex >= 0)
4625 *pIndex = vSortedIndex[*pIndex];
4626 });
4627
4628 return vSortedIndex;
4629 }
4630
4631 return std::vector<int>();
4632}
4633
4634void CEditor::RenderImagesList(CUIRect ToolBox)
4635{
4636 const float RowHeight = 12.0f;
4637
4638 static CScrollRegion s_ScrollRegion;
4639 vec2 ScrollOffset(0.0f, 0.0f);
4640 CScrollRegionParams ScrollParams;
4641 ScrollParams.m_ScrollbarWidth = 10.0f;
4642 ScrollParams.m_ScrollbarMargin = 3.0f;
4643 ScrollParams.m_ScrollUnit = RowHeight * 5;
4644 s_ScrollRegion.Begin(pClipRect: &ToolBox, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
4645 ToolBox.y += ScrollOffset.y;
4646
4647 bool ScrollToSelection = false;
4648 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !m_Map.m_vpImages.empty())
4649 {
4650 if(Input()->KeyPress(Key: KEY_DOWN))
4651 {
4652 int OldImage = m_SelectedImage;
4653 m_SelectedImage = clamp(val: m_SelectedImage, lo: 0, hi: (int)m_Map.m_vpImages.size() - 1);
4654 for(size_t i = m_SelectedImage + 1; i < m_Map.m_vpImages.size(); i++)
4655 {
4656 if(m_Map.m_vpImages[i]->m_External == m_Map.m_vpImages[m_SelectedImage]->m_External)
4657 {
4658 m_SelectedImage = i;
4659 break;
4660 }
4661 }
4662 if(m_SelectedImage == OldImage && !m_Map.m_vpImages[m_SelectedImage]->m_External)
4663 {
4664 for(size_t i = 0; i < m_Map.m_vpImages.size(); i++)
4665 {
4666 if(m_Map.m_vpImages[i]->m_External)
4667 {
4668 m_SelectedImage = i;
4669 break;
4670 }
4671 }
4672 }
4673 ScrollToSelection = OldImage != m_SelectedImage;
4674 }
4675 else if(Input()->KeyPress(Key: KEY_UP))
4676 {
4677 int OldImage = m_SelectedImage;
4678 m_SelectedImage = clamp(val: m_SelectedImage, lo: 0, hi: (int)m_Map.m_vpImages.size() - 1);
4679 for(int i = m_SelectedImage - 1; i >= 0; i--)
4680 {
4681 if(m_Map.m_vpImages[i]->m_External == m_Map.m_vpImages[m_SelectedImage]->m_External)
4682 {
4683 m_SelectedImage = i;
4684 break;
4685 }
4686 }
4687 if(m_SelectedImage == OldImage && m_Map.m_vpImages[m_SelectedImage]->m_External)
4688 {
4689 for(int i = (int)m_Map.m_vpImages.size() - 1; i >= 0; i--)
4690 {
4691 if(!m_Map.m_vpImages[i]->m_External)
4692 {
4693 m_SelectedImage = i;
4694 break;
4695 }
4696 }
4697 }
4698 ScrollToSelection = OldImage != m_SelectedImage;
4699 }
4700 }
4701
4702 for(int e = 0; e < 2; e++) // two passes, first embedded, then external
4703 {
4704 CUIRect Slot;
4705 ToolBox.HSplitTop(Cut: RowHeight + 3.0f, pTop: &Slot, pBottom: &ToolBox);
4706 if(s_ScrollRegion.AddRect(Rect: Slot))
4707 Ui()->DoLabel(pRect: &Slot, pText: e == 0 ? "Embedded" : "External", Size: 12.0f, Align: TEXTALIGN_MC);
4708
4709 for(int i = 0; i < (int)m_Map.m_vpImages.size(); i++)
4710 {
4711 if((e && !m_Map.m_vpImages[i]->m_External) ||
4712 (!e && m_Map.m_vpImages[i]->m_External))
4713 {
4714 continue;
4715 }
4716
4717 ToolBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &ToolBox);
4718 int Selected = m_SelectedImage == i;
4719 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: Selected && ScrollToSelection))
4720 continue;
4721 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
4722
4723 const bool ImageUsed = std::any_of(first: m_Map.m_vpGroups.cbegin(), last: m_Map.m_vpGroups.cend(), pred: [i](const auto &pGroup) {
4724 return std::any_of(pGroup->m_vpLayers.cbegin(), pGroup->m_vpLayers.cend(), [i](const auto &pLayer) {
4725 if(pLayer->m_Type == LAYERTYPE_QUADS)
4726 return std::static_pointer_cast<CLayerQuads>(pLayer)->m_Image == i;
4727 else if(pLayer->m_Type == LAYERTYPE_TILES)
4728 return std::static_pointer_cast<CLayerTiles>(pLayer)->m_Image == i;
4729 return false;
4730 });
4731 });
4732
4733 if(!ImageUsed)
4734 Selected += 2; // Image is unused
4735
4736 if(Selected < 2 && e == 1)
4737 {
4738 if(!IsVanillaImage(pImage: m_Map.m_vpImages[i]->m_aName))
4739 {
4740 Selected += 4; // Image should be embedded
4741 }
4742 }
4743
4744 if(int Result = DoButton_Ex(pId: &m_Map.m_vpImages[i], pText: m_Map.m_vpImages[i]->m_aName, Checked: Selected, pRect: &Slot,
4745 Flags: BUTTON_CONTEXT, pToolTip: "Select image.", Corners: IGraphics::CORNER_ALL))
4746 {
4747 m_SelectedImage = i;
4748
4749 if(Result == 2)
4750 {
4751 const std::shared_ptr<CEditorImage> pImg = m_Map.m_vpImages[m_SelectedImage];
4752 const int Height = pImg->m_External ? 73 : 107;
4753 static SPopupMenuId s_PopupImageId;
4754 Ui()->DoPopupMenu(pId: &s_PopupImageId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 140, Height, pContext: this, pfnFunc: PopupImage);
4755 }
4756 }
4757 }
4758
4759 // separator
4760 ToolBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &ToolBox);
4761 if(s_ScrollRegion.AddRect(Rect: Slot))
4762 {
4763 IGraphics::CLineItem LineItem(Slot.x, Slot.y + Slot.h / 2, Slot.x + Slot.w, Slot.y + Slot.h / 2);
4764 Graphics()->TextureClear();
4765 Graphics()->LinesBegin();
4766 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
4767 Graphics()->LinesEnd();
4768 }
4769 }
4770
4771 // new image
4772 static int s_AddImageButton = 0;
4773 CUIRect AddImageButton;
4774 ToolBox.HSplitTop(Cut: 5.0f + RowHeight + 1.0f, pTop: &AddImageButton, pBottom: &ToolBox);
4775 if(s_ScrollRegion.AddRect(Rect: AddImageButton))
4776 {
4777 AddImageButton.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &AddImageButton);
4778 AddImageButton.HSplitTop(Cut: RowHeight, pTop: &AddImageButton, pBottom: nullptr);
4779 if(DoButton_Editor(pId: &s_AddImageButton, pText: "Add", Checked: 0, pRect: &AddImageButton, Flags: 0, pToolTip: "Load a new image to use in the map"))
4780 InvokeFileDialog(StorageType: IStorage::TYPE_ALL, FileType: FILETYPE_IMG, pTitle: "Add Image", pButtonText: "Add", pBasepath: "mapres", FilenameAsDefault: false, pfnFunc: AddImage, pUser: this);
4781 }
4782 s_ScrollRegion.End();
4783}
4784
4785void CEditor::RenderSelectedImage(CUIRect View)
4786{
4787 if(m_SelectedImage < 0 || (size_t)m_SelectedImage >= m_Map.m_vpImages.size())
4788 return;
4789
4790 View.Margin(Cut: 10.0f, pOtherRect: &View);
4791 if(View.h < View.w)
4792 View.w = View.h;
4793 else
4794 View.h = View.w;
4795 float Max = maximum<float>(a: m_Map.m_vpImages[m_SelectedImage]->m_Width, b: m_Map.m_vpImages[m_SelectedImage]->m_Height);
4796 View.w *= m_Map.m_vpImages[m_SelectedImage]->m_Width / Max;
4797 View.h *= m_Map.m_vpImages[m_SelectedImage]->m_Height / Max;
4798 Graphics()->TextureSet(Texture: m_Map.m_vpImages[m_SelectedImage]->m_Texture);
4799 Graphics()->BlendNormal();
4800 Graphics()->WrapClamp();
4801 Graphics()->QuadsBegin();
4802 IGraphics::CQuadItem QuadItem(View.x, View.y, View.w, View.h);
4803 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
4804 Graphics()->QuadsEnd();
4805 Graphics()->WrapNormal();
4806}
4807
4808void CEditor::RenderSounds(CUIRect ToolBox)
4809{
4810 const float RowHeight = 12.0f;
4811
4812 static CScrollRegion s_ScrollRegion;
4813 vec2 ScrollOffset(0.0f, 0.0f);
4814 CScrollRegionParams ScrollParams;
4815 ScrollParams.m_ScrollbarWidth = 10.0f;
4816 ScrollParams.m_ScrollbarMargin = 3.0f;
4817 ScrollParams.m_ScrollUnit = RowHeight * 5;
4818 s_ScrollRegion.Begin(pClipRect: &ToolBox, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
4819 ToolBox.y += ScrollOffset.y;
4820
4821 bool ScrollToSelection = false;
4822 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !m_Map.m_vpSounds.empty())
4823 {
4824 if(Input()->KeyPress(Key: KEY_DOWN))
4825 {
4826 m_SelectedSound = (m_SelectedSound + 1) % m_Map.m_vpSounds.size();
4827 ScrollToSelection = true;
4828 }
4829 else if(Input()->KeyPress(Key: KEY_UP))
4830 {
4831 m_SelectedSound = (m_SelectedSound + m_Map.m_vpSounds.size() - 1) % m_Map.m_vpSounds.size();
4832 ScrollToSelection = true;
4833 }
4834 }
4835
4836 CUIRect Slot;
4837 ToolBox.HSplitTop(Cut: RowHeight + 3.0f, pTop: &Slot, pBottom: &ToolBox);
4838 if(s_ScrollRegion.AddRect(Rect: Slot))
4839 Ui()->DoLabel(pRect: &Slot, pText: "Embedded", Size: 12.0f, Align: TEXTALIGN_MC);
4840
4841 for(int i = 0; i < (int)m_Map.m_vpSounds.size(); i++)
4842 {
4843 ToolBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &ToolBox);
4844 int Selected = m_SelectedSound == i;
4845 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: Selected && ScrollToSelection))
4846 continue;
4847 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
4848
4849 const bool SoundUsed = std::any_of(first: m_Map.m_vpGroups.cbegin(), last: m_Map.m_vpGroups.cend(), pred: [i](const auto &pGroup) {
4850 return std::any_of(pGroup->m_vpLayers.cbegin(), pGroup->m_vpLayers.cend(), [i](const auto &pLayer) {
4851 if(pLayer->m_Type == LAYERTYPE_SOUNDS)
4852 return std::static_pointer_cast<CLayerSounds>(pLayer)->m_Sound == i;
4853 return false;
4854 });
4855 });
4856
4857 if(!SoundUsed)
4858 Selected += 2; // Sound is unused
4859
4860 if(int Result = DoButton_Ex(pId: &m_Map.m_vpSounds[i], pText: m_Map.m_vpSounds[i]->m_aName, Checked: Selected, pRect: &Slot,
4861 Flags: BUTTON_CONTEXT, pToolTip: "Select sound.", Corners: IGraphics::CORNER_ALL))
4862 {
4863 m_SelectedSound = i;
4864
4865 if(Result == 2)
4866 {
4867 static SPopupMenuId s_PopupSoundId;
4868 Ui()->DoPopupMenu(pId: &s_PopupSoundId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 140, Height: 90, pContext: this, pfnFunc: PopupSound);
4869 }
4870 }
4871 }
4872
4873 // separator
4874 ToolBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &ToolBox);
4875 if(s_ScrollRegion.AddRect(Rect: Slot))
4876 {
4877 IGraphics::CLineItem LineItem(Slot.x, Slot.y + Slot.h / 2, Slot.x + Slot.w, Slot.y + Slot.h / 2);
4878 Graphics()->TextureClear();
4879 Graphics()->LinesBegin();
4880 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
4881 Graphics()->LinesEnd();
4882 }
4883
4884 // new sound
4885 static int s_AddSoundButton = 0;
4886 CUIRect AddSoundButton;
4887 ToolBox.HSplitTop(Cut: 5.0f + RowHeight + 1.0f, pTop: &AddSoundButton, pBottom: &ToolBox);
4888 if(s_ScrollRegion.AddRect(Rect: AddSoundButton))
4889 {
4890 AddSoundButton.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &AddSoundButton);
4891 AddSoundButton.HSplitTop(Cut: RowHeight, pTop: &AddSoundButton, pBottom: nullptr);
4892 if(DoButton_Editor(pId: &s_AddSoundButton, pText: "Add", Checked: 0, pRect: &AddSoundButton, Flags: 0, pToolTip: "Load a new sound to use in the map"))
4893 InvokeFileDialog(StorageType: IStorage::TYPE_ALL, FileType: FILETYPE_SOUND, pTitle: "Add Sound", pButtonText: "Add", pBasepath: "mapres", FilenameAsDefault: false, pfnFunc: AddSound, pUser: this);
4894 }
4895 s_ScrollRegion.End();
4896}
4897
4898static int EditorListdirCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
4899{
4900 CEditor *pEditor = (CEditor *)pUser;
4901 if((pInfo->m_pName[0] == '.' && (pInfo->m_pName[1] == 0 ||
4902 (pInfo->m_pName[1] == '.' && pInfo->m_pName[2] == 0 && (pEditor->m_FileDialogShowingRoot || (!pEditor->m_FileDialogMultipleStorages && (!str_comp(a: pEditor->m_pFileDialogPath, b: "maps") || !str_comp(a: pEditor->m_pFileDialogPath, b: "mapres"))))))) ||
4903 (!IsDir && ((pEditor->m_FileDialogFileType == CEditor::FILETYPE_MAP && !str_endswith(str: pInfo->m_pName, suffix: ".map")) ||
4904 (pEditor->m_FileDialogFileType == CEditor::FILETYPE_IMG && !str_endswith(str: pInfo->m_pName, suffix: ".png")) ||
4905 (pEditor->m_FileDialogFileType == CEditor::FILETYPE_SOUND && !str_endswith(str: pInfo->m_pName, suffix: ".opus")))))
4906 return 0;
4907
4908 CEditor::CFilelistItem Item;
4909 str_copy(dst&: Item.m_aFilename, src: pInfo->m_pName);
4910 if(IsDir)
4911 str_format(buffer: Item.m_aName, buffer_size: sizeof(Item.m_aName), format: "%s/", pInfo->m_pName);
4912 else
4913 {
4914 int LenEnding = pEditor->m_FileDialogFileType == CEditor::FILETYPE_SOUND ? 5 : 4;
4915 str_truncate(dst: Item.m_aName, dst_size: sizeof(Item.m_aName), src: pInfo->m_pName, truncation_len: str_length(str: pInfo->m_pName) - LenEnding);
4916 }
4917 Item.m_IsDir = IsDir != 0;
4918 Item.m_IsLink = false;
4919 Item.m_StorageType = StorageType;
4920 Item.m_TimeModified = pInfo->m_TimeModified;
4921 pEditor->m_vCompleteFileList.push_back(x: Item);
4922
4923 return 0;
4924}
4925
4926void CEditor::SortFilteredFileList()
4927{
4928 if(m_SortByFilename == 1)
4929 {
4930 std::sort(first: m_vpFilteredFileList.begin(), last: m_vpFilteredFileList.end(), comp: CEditor::CompareFilenameAscending);
4931 }
4932 else
4933 {
4934 std::sort(first: m_vpFilteredFileList.begin(), last: m_vpFilteredFileList.end(), comp: CEditor::CompareFilenameDescending);
4935 }
4936
4937 if(m_SortByTimeModified == 1)
4938 {
4939 std::stable_sort(first: m_vpFilteredFileList.begin(), last: m_vpFilteredFileList.end(), comp: CEditor::CompareTimeModifiedAscending);
4940 }
4941 else if(m_SortByTimeModified == -1)
4942 {
4943 std::stable_sort(first: m_vpFilteredFileList.begin(), last: m_vpFilteredFileList.end(), comp: CEditor::CompareTimeModifiedDescending);
4944 }
4945}
4946
4947void CEditor::RenderFileDialog()
4948{
4949 // GUI coordsys
4950 Ui()->MapScreen();
4951 CUIRect View = *Ui()->Screen();
4952 CUIRect Preview = {.x: 0.0f, .y: 0.0f, .w: 0.0f, .h: 0.0f};
4953 float Width = View.w, Height = View.h;
4954
4955 View.Draw(Color: ColorRGBA(0, 0, 0, 0.25f), Corners: 0, Rounding: 0);
4956 View.VMargin(Cut: 150.0f, pOtherRect: &View);
4957 View.HMargin(Cut: 50.0f, pOtherRect: &View);
4958 View.Draw(Color: ColorRGBA(0, 0, 0, 0.75f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
4959 View.Margin(Cut: 10.0f, pOtherRect: &View);
4960
4961 CUIRect Title, FileBox, FileBoxLabel, ButtonBar, PathBox;
4962 View.HSplitTop(Cut: 18.0f, pTop: &Title, pBottom: &View);
4963 View.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &View); // some spacing
4964 View.HSplitBottom(Cut: 14.0f, pTop: &View, pBottom: &ButtonBar);
4965 View.HSplitBottom(Cut: 10.0f, pTop: &View, pBottom: nullptr); // some spacing
4966 View.HSplitBottom(Cut: 14.0f, pTop: &View, pBottom: &PathBox);
4967 View.HSplitBottom(Cut: 5.0f, pTop: &View, pBottom: nullptr); // some spacing
4968 View.HSplitBottom(Cut: 14.0f, pTop: &View, pBottom: &FileBox);
4969 FileBox.VSplitLeft(Cut: 55.0f, pLeft: &FileBoxLabel, pRight: &FileBox);
4970 View.HSplitBottom(Cut: 10.0f, pTop: &View, pBottom: nullptr); // some spacing
4971 if(m_FileDialogFileType == CEditor::FILETYPE_IMG || m_FileDialogFileType == CEditor::FILETYPE_SOUND)
4972 View.VSplitMid(pLeft: &View, pRight: &Preview);
4973
4974 // title bar
4975 if(!m_FileDialogShowingRoot)
4976 {
4977 CUIRect ButtonTimeModified, ButtonFileName;
4978 Title.VSplitRight(Cut: 10.0f, pLeft: &Title, pRight: nullptr);
4979 Title.VSplitRight(Cut: 90.0f, pLeft: &Title, pRight: &ButtonTimeModified);
4980 Title.VSplitRight(Cut: 10.0f, pLeft: &Title, pRight: nullptr);
4981 Title.VSplitRight(Cut: 90.0f, pLeft: &Title, pRight: &ButtonFileName);
4982 Title.VSplitRight(Cut: 10.0f, pLeft: &Title, pRight: nullptr);
4983
4984 const char *aSortIndicator[3] = {"▼", "", "▲"};
4985
4986 static int s_ButtonTimeModified = 0;
4987 char aBufLabelButtonTimeModified[64];
4988 str_format(buffer: aBufLabelButtonTimeModified, buffer_size: sizeof(aBufLabelButtonTimeModified), format: "Time modified %s", aSortIndicator[m_SortByTimeModified + 1]);
4989 if(DoButton_Editor(pId: &s_ButtonTimeModified, pText: aBufLabelButtonTimeModified, Checked: 0, pRect: &ButtonTimeModified, Flags: 0, pToolTip: "Sort by time modified"))
4990 {
4991 if(m_SortByTimeModified == 1)
4992 {
4993 m_SortByTimeModified = -1;
4994 }
4995 else if(m_SortByTimeModified == -1)
4996 {
4997 m_SortByTimeModified = 0;
4998 }
4999 else
5000 {
5001 m_SortByTimeModified = 1;
5002 }
5003
5004 RefreshFilteredFileList();
5005 }
5006
5007 static int s_ButtonFileName = 0;
5008 char aBufLabelButtonFilename[64];
5009 str_format(buffer: aBufLabelButtonFilename, buffer_size: sizeof(aBufLabelButtonFilename), format: "Filename %s", aSortIndicator[m_SortByFilename + 1]);
5010 if(DoButton_Editor(pId: &s_ButtonFileName, pText: aBufLabelButtonFilename, Checked: 0, pRect: &ButtonFileName, Flags: 0, pToolTip: "Sort by file name"))
5011 {
5012 if(m_SortByFilename == 1)
5013 {
5014 m_SortByFilename = -1;
5015 m_SortByTimeModified = 0;
5016 }
5017 else
5018 {
5019 m_SortByFilename = 1;
5020 m_SortByTimeModified = 0;
5021 }
5022
5023 RefreshFilteredFileList();
5024 }
5025 }
5026
5027 Title.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 4.0f);
5028 Title.VMargin(Cut: 10.0f, pOtherRect: &Title);
5029 Ui()->DoLabel(pRect: &Title, pText: m_pFileDialogTitle, Size: 12.0f, Align: TEXTALIGN_ML);
5030
5031 // pathbox
5032 if(m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType >= IStorage::TYPE_SAVE)
5033 {
5034 char aPath[IO_MAX_PATH_LENGTH], aBuf[128 + IO_MAX_PATH_LENGTH];
5035 Storage()->GetCompletePath(Type: m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType, pDir: m_pFileDialogPath, pBuffer: aPath, BufferSize: sizeof(aPath));
5036 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Current path: %s", aPath);
5037 Ui()->DoLabel(pRect: &PathBox, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_ML);
5038 }
5039
5040 const auto &&UpdateFileNameInput = [this]() {
5041 if(m_FilesSelectedIndex >= 0 && !m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsDir)
5042 {
5043 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
5044 fs_split_file_extension(filename: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
5045 m_FileDialogFileNameInput.Set(aNameWithoutExt);
5046 }
5047 else
5048 m_FileDialogFileNameInput.Clear();
5049 };
5050
5051 // filebox
5052 static CListBox s_ListBox;
5053 s_ListBox.SetActive(!Ui()->IsPopupOpen());
5054
5055 if(m_FileDialogStorageType == IStorage::TYPE_SAVE)
5056 {
5057 Ui()->DoLabel(pRect: &FileBoxLabel, pText: "Filename:", Size: 10.0f, Align: TEXTALIGN_ML);
5058 if(DoEditBox(pLineInput: &m_FileDialogFileNameInput, pRect: &FileBox, FontSize: 10.0f))
5059 {
5060 // remove '/' and '\'
5061 for(int i = 0; m_FileDialogFileNameInput.GetString()[i]; ++i)
5062 {
5063 if(m_FileDialogFileNameInput.GetString()[i] == '/' || m_FileDialogFileNameInput.GetString()[i] == '\\')
5064 {
5065 m_FileDialogFileNameInput.SetRange(pString: m_FileDialogFileNameInput.GetString() + i + 1, Begin: i, End: m_FileDialogFileNameInput.GetLength());
5066 --i;
5067 }
5068 }
5069 m_FilesSelectedIndex = -1;
5070 m_aFilesSelectedName[0] = '\0';
5071 // find first valid entry, if it exists
5072 for(size_t i = 0; i < m_vpFilteredFileList.size(); i++)
5073 {
5074 if(str_comp_nocase(a: m_vpFilteredFileList[i]->m_aName, b: m_FileDialogFileNameInput.GetString()) == 0)
5075 {
5076 m_FilesSelectedIndex = i;
5077 str_copy(dst&: m_aFilesSelectedName, src: m_vpFilteredFileList[i]->m_aName);
5078 break;
5079 }
5080 }
5081 if(m_FilesSelectedIndex >= 0)
5082 s_ListBox.ScrollToSelected();
5083 }
5084
5085 if(m_FileDialogOpening)
5086 Ui()->SetActiveItem(&m_FileDialogFileNameInput);
5087 }
5088 else
5089 {
5090 // render search bar
5091 Ui()->DoLabel(pRect: &FileBoxLabel, pText: "Search:", Size: 10.0f, Align: TEXTALIGN_ML);
5092 if(m_FileDialogOpening || (Input()->KeyPress(Key: KEY_F) && Input()->ModifierIsPressed()))
5093 {
5094 Ui()->SetActiveItem(&m_FileDialogFilterInput);
5095 m_FileDialogFilterInput.SelectAll();
5096 }
5097 if(Ui()->DoClearableEditBox(pLineInput: &m_FileDialogFilterInput, pRect: &FileBox, FontSize: 10.0f))
5098 {
5099 RefreshFilteredFileList();
5100 if(m_vpFilteredFileList.empty())
5101 {
5102 m_FilesSelectedIndex = -1;
5103 }
5104 else if(m_FilesSelectedIndex == -1 || (!m_FileDialogFilterInput.IsEmpty() && !str_find_nocase(haystack: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aName, needle: m_FileDialogFilterInput.GetString())))
5105 {
5106 // we need to refresh selection
5107 m_FilesSelectedIndex = -1;
5108 for(size_t i = 0; i < m_vpFilteredFileList.size(); i++)
5109 {
5110 if(str_find_nocase(haystack: m_vpFilteredFileList[i]->m_aName, needle: m_FileDialogFilterInput.GetString()))
5111 {
5112 m_FilesSelectedIndex = i;
5113 break;
5114 }
5115 }
5116 if(m_FilesSelectedIndex == -1)
5117 {
5118 // select first item
5119 m_FilesSelectedIndex = 0;
5120 }
5121 }
5122 if(m_FilesSelectedIndex >= 0)
5123 str_copy(dst&: m_aFilesSelectedName, src: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aName);
5124 else
5125 m_aFilesSelectedName[0] = '\0';
5126 UpdateFileNameInput();
5127 s_ListBox.ScrollToSelected();
5128 m_FilePreviewState = PREVIEW_UNLOADED;
5129 }
5130 }
5131
5132 m_FileDialogOpening = false;
5133
5134 if(m_FilesSelectedIndex > -1)
5135 {
5136 if(m_FilePreviewState == PREVIEW_UNLOADED)
5137 {
5138 if(m_FileDialogFileType == CEditor::FILETYPE_IMG && str_endswith(str: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, suffix: ".png"))
5139 {
5140 char aBuffer[IO_MAX_PATH_LENGTH];
5141 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s/%s", m_pFileDialogPath, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename);
5142 if(Graphics()->LoadPng(Image&: m_FilePreviewImageInfo, pFilename: aBuffer, StorageType: m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType))
5143 {
5144 Graphics()->UnloadTexture(pIndex: &m_FilePreviewImage);
5145 m_FilePreviewImage = Graphics()->LoadTextureRawMove(Image&: m_FilePreviewImageInfo, Flags: 0, pTexName: aBuffer);
5146 m_FilePreviewImageInfo.m_pData = nullptr;
5147 m_FilePreviewState = PREVIEW_LOADED;
5148 }
5149 else
5150 {
5151 m_FilePreviewState = PREVIEW_ERROR;
5152 }
5153 }
5154 else if(m_FileDialogFileType == CEditor::FILETYPE_SOUND && str_endswith(str: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, suffix: ".opus"))
5155 {
5156 char aBuffer[IO_MAX_PATH_LENGTH];
5157 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s/%s", m_pFileDialogPath, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename);
5158 Sound()->UnloadSample(SampleId: m_FilePreviewSound);
5159 m_FilePreviewSound = Sound()->LoadOpus(pFilename: aBuffer, StorageType: m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType);
5160 m_FilePreviewState = m_FilePreviewSound == -1 ? PREVIEW_ERROR : PREVIEW_LOADED;
5161 }
5162 }
5163
5164 if(m_FileDialogFileType == CEditor::FILETYPE_IMG)
5165 {
5166 Preview.Margin(Cut: 10.0f, pOtherRect: &Preview);
5167 if(m_FilePreviewState == PREVIEW_LOADED)
5168 {
5169 int w = m_FilePreviewImageInfo.m_Width;
5170 int h = m_FilePreviewImageInfo.m_Height;
5171 if(m_FilePreviewImageInfo.m_Width > Preview.w)
5172 {
5173 h = m_FilePreviewImageInfo.m_Height * Preview.w / m_FilePreviewImageInfo.m_Width;
5174 w = Preview.w;
5175 }
5176 if(h > Preview.h)
5177 {
5178 w = w * Preview.h / h;
5179 h = Preview.h;
5180 }
5181
5182 Graphics()->TextureSet(Texture: m_FilePreviewImage);
5183 Graphics()->BlendNormal();
5184 Graphics()->QuadsBegin();
5185 IGraphics::CQuadItem QuadItem(Preview.x, Preview.y, w, h);
5186 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
5187 Graphics()->QuadsEnd();
5188 }
5189 else if(m_FilePreviewState == PREVIEW_ERROR)
5190 {
5191 SLabelProperties Props;
5192 Props.m_MaxWidth = Preview.w;
5193 Ui()->DoLabel(pRect: &Preview, pText: "Failed to load the image (check the local console for details).", Size: 12.0f, Align: TEXTALIGN_TL, LabelProps: Props);
5194 }
5195 }
5196 else if(m_FileDialogFileType == CEditor::FILETYPE_SOUND)
5197 {
5198 Preview.Margin(Cut: 10.0f, pOtherRect: &Preview);
5199 if(m_FilePreviewState == PREVIEW_LOADED)
5200 {
5201 Preview.HSplitTop(Cut: 20.0f, pTop: &Preview, pBottom: nullptr);
5202 Preview.VSplitLeft(Cut: Preview.h / 4.0f, pLeft: nullptr, pRight: &Preview);
5203
5204 static int s_PlayPauseButton, s_StopButton, s_SeekBar = 0;
5205 DoAudioPreview(View: Preview, pPlayPauseButtonId: &s_PlayPauseButton, pStopButtonId: &s_StopButton, pSeekBarId: &s_SeekBar, SampleId: m_FilePreviewSound);
5206 }
5207 else if(m_FilePreviewState == PREVIEW_ERROR)
5208 {
5209 SLabelProperties Props;
5210 Props.m_MaxWidth = Preview.w;
5211 Ui()->DoLabel(pRect: &Preview, pText: "Failed to load the sound (check the local console for details). Make sure you enabled sounds in the settings.", Size: 12.0f, Align: TEXTALIGN_TL, LabelProps: Props);
5212 }
5213 }
5214 }
5215
5216 s_ListBox.DoStart(RowHeight: 15.0f, NumItems: m_vpFilteredFileList.size(), ItemsPerRow: 1, RowsPerScroll: 5, SelectedIndex: m_FilesSelectedIndex, pRect: &View, Background: false);
5217
5218 for(size_t i = 0; i < m_vpFilteredFileList.size(); i++)
5219 {
5220 const CListboxItem Item = s_ListBox.DoNextItem(pId: m_vpFilteredFileList[i], Selected: m_FilesSelectedIndex >= 0 && (size_t)m_FilesSelectedIndex == i);
5221 if(!Item.m_Visible)
5222 continue;
5223
5224 CUIRect Button, FileIcon, TimeModified;
5225 Item.m_Rect.VSplitLeft(Cut: Item.m_Rect.h, pLeft: &FileIcon, pRight: &Button);
5226 Button.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Button);
5227 Button.VSplitRight(Cut: 100.0f, pLeft: &Button, pRight: &TimeModified);
5228 Button.VSplitRight(Cut: 5.0f, pLeft: &Button, pRight: nullptr);
5229
5230 const char *pIconType;
5231 if(!m_vpFilteredFileList[i]->m_IsDir)
5232 {
5233 switch(m_FileDialogFileType)
5234 {
5235 case FILETYPE_MAP:
5236 pIconType = FONT_ICON_MAP;
5237 break;
5238 case FILETYPE_IMG:
5239 pIconType = FONT_ICON_IMAGE;
5240 break;
5241 case FILETYPE_SOUND:
5242 pIconType = FONT_ICON_MUSIC;
5243 break;
5244 default:
5245 pIconType = FONT_ICON_FILE;
5246 }
5247 }
5248 else
5249 {
5250 if(m_vpFilteredFileList[i]->m_IsLink || str_comp(a: m_vpFilteredFileList[i]->m_aFilename, b: "..") == 0)
5251 pIconType = FONT_ICON_FOLDER_TREE;
5252 else
5253 pIconType = FONT_ICON_FOLDER;
5254 }
5255
5256 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
5257 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
5258 Ui()->DoLabel(pRect: &FileIcon, pText: pIconType, Size: 12.0f, Align: TEXTALIGN_ML);
5259 TextRender()->SetRenderFlags(0);
5260 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
5261
5262 SLabelProperties Props;
5263 Props.m_MaxWidth = Button.w;
5264 Props.m_EllipsisAtEnd = true;
5265 Ui()->DoLabel(pRect: &Button, pText: m_vpFilteredFileList[i]->m_aName, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
5266
5267 if(!m_vpFilteredFileList[i]->m_IsLink && str_comp(a: m_vpFilteredFileList[i]->m_aFilename, b: "..") != 0)
5268 {
5269 char aBufTimeModified[64];
5270 str_timestamp_ex(time: m_vpFilteredFileList[i]->m_TimeModified, buffer: aBufTimeModified, buffer_size: sizeof(aBufTimeModified), format: "%d.%m.%Y %H:%M");
5271 Ui()->DoLabel(pRect: &TimeModified, pText: aBufTimeModified, Size: 10.0f, Align: TEXTALIGN_MR);
5272 }
5273 }
5274
5275 const int NewSelection = s_ListBox.DoEnd();
5276 if(NewSelection != m_FilesSelectedIndex)
5277 {
5278 m_FilesSelectedIndex = NewSelection;
5279 str_copy(dst&: m_aFilesSelectedName, src: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aName);
5280 const bool WasChanged = m_FileDialogFileNameInput.WasChanged();
5281 UpdateFileNameInput();
5282 if(!WasChanged) // ensure that changed flag is not set if it wasn't previously set, as this would reset the selection after DoEditBox is called
5283 m_FileDialogFileNameInput.WasChanged(); // this clears the changed flag
5284 m_FilePreviewState = PREVIEW_UNLOADED;
5285 }
5286
5287 const float ButtonSpacing = ButtonBar.w > 600.0f ? 40.0f : 10.0f;
5288
5289 // the buttons
5290 static int s_OkButton = 0;
5291 static int s_CancelButton = 0;
5292 static int s_RefreshButton = 0;
5293 static int s_ShowDirectoryButton = 0;
5294 static int s_DeleteButton = 0;
5295 static int s_NewFolderButton = 0;
5296
5297 CUIRect Button;
5298 ButtonBar.VSplitRight(Cut: 50.0f, pLeft: &ButtonBar, pRight: &Button);
5299 const bool IsDir = m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsDir;
5300 if(DoButton_Editor(pId: &s_OkButton, pText: IsDir ? "Open" : m_pFileDialogButtonText, Checked: 0, pRect: &Button, Flags: 0, pToolTip: nullptr) || s_ListBox.WasItemActivated() || (s_ListBox.Active() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
5301 {
5302 if(IsDir) // folder
5303 {
5304 m_FileDialogFilterInput.Clear();
5305 Ui()->SetActiveItem(&m_FileDialogFilterInput);
5306 const bool ParentFolder = str_comp(a: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, b: "..") == 0;
5307 if(ParentFolder) // parent folder
5308 {
5309 str_copy(dst&: m_aFilesSelectedName, src: fs_filename(path: m_pFileDialogPath));
5310 str_append(dst&: m_aFilesSelectedName, src: "/");
5311 if(fs_parent_dir(path: m_pFileDialogPath))
5312 {
5313 if(str_comp(a: m_pFileDialogPath, b: m_aFileDialogCurrentFolder) == 0)
5314 {
5315 m_FileDialogShowingRoot = true;
5316 if(m_FileDialogStorageType == IStorage::TYPE_ALL)
5317 {
5318 m_aFilesSelectedName[0] = '\0'; // will select first list item
5319 }
5320 else
5321 {
5322 Storage()->GetCompletePath(Type: m_FileDialogStorageType, pDir: m_pFileDialogPath, pBuffer: m_aFilesSelectedName, BufferSize: sizeof(m_aFilesSelectedName));
5323 str_append(dst&: m_aFilesSelectedName, src: "/");
5324 }
5325 }
5326 else
5327 {
5328 m_pFileDialogPath = m_aFileDialogCurrentFolder; // leave the link
5329 str_copy(dst&: m_aFilesSelectedName, src: m_aFileDialogCurrentLink);
5330 str_append(dst&: m_aFilesSelectedName, src: "/");
5331 }
5332 }
5333 }
5334 else // sub folder
5335 {
5336 if(m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsLink)
5337 {
5338 m_pFileDialogPath = m_aFileDialogCurrentLink; // follow the link
5339 str_copy(dst&: m_aFileDialogCurrentLink, src: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename);
5340 }
5341 else
5342 {
5343 char aTemp[IO_MAX_PATH_LENGTH];
5344 str_copy(dst&: aTemp, src: m_pFileDialogPath);
5345 str_format(buffer: m_pFileDialogPath, buffer_size: IO_MAX_PATH_LENGTH, format: "%s/%s", aTemp, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename);
5346 }
5347 if(m_FileDialogShowingRoot)
5348 m_FileDialogStorageType = m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType;
5349 m_FileDialogShowingRoot = false;
5350 }
5351 FilelistPopulate(StorageType: m_FileDialogStorageType, KeepSelection: ParentFolder);
5352 UpdateFileNameInput();
5353 }
5354 else // file
5355 {
5356 const int StorageType = m_FilesSelectedIndex >= 0 ? m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType : m_FileDialogStorageType;
5357 str_format(buffer: m_aFileSaveName, buffer_size: sizeof(m_aFileSaveName), format: "%s/%s", m_pFileDialogPath, m_FileDialogFileNameInput.GetString());
5358 if(!str_endswith(str: m_aFileSaveName, suffix: FILETYPE_EXTENSIONS[m_FileDialogFileType]))
5359 str_append(dst&: m_aFileSaveName, src: FILETYPE_EXTENSIONS[m_FileDialogFileType]);
5360 if(!str_comp(a: m_pFileDialogButtonText, b: "Save") && Storage()->FileExists(pFilename: m_aFileSaveName, Type: StorageType))
5361 {
5362 if(m_pfnFileDialogFunc == &CallbackSaveMap)
5363 m_PopupEventType = POPEVENT_SAVE;
5364 else if(m_pfnFileDialogFunc == &CallbackSaveCopyMap)
5365 m_PopupEventType = POPEVENT_SAVE_COPY;
5366 else if(m_pfnFileDialogFunc == &CallbackSaveImage)
5367 m_PopupEventType = POPEVENT_SAVE_IMG;
5368 else
5369 m_PopupEventType = POPEVENT_SAVE_SOUND;
5370 m_PopupEventActivated = true;
5371 }
5372 else if(m_pfnFileDialogFunc)
5373 m_pfnFileDialogFunc(m_aFileSaveName, StorageType, m_pFileDialogUser);
5374 }
5375 }
5376
5377 ButtonBar.VSplitRight(Cut: ButtonSpacing, pLeft: &ButtonBar, pRight: nullptr);
5378 ButtonBar.VSplitRight(Cut: 50.0f, pLeft: &ButtonBar, pRight: &Button);
5379 if(DoButton_Editor(pId: &s_CancelButton, pText: "Cancel", Checked: 0, pRect: &Button, Flags: 0, pToolTip: nullptr) || (s_ListBox.Active() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE)))
5380 {
5381 OnDialogClose();
5382 m_Dialog = DIALOG_NONE;
5383 }
5384
5385 ButtonBar.VSplitRight(Cut: ButtonSpacing, pLeft: &ButtonBar, pRight: nullptr);
5386 ButtonBar.VSplitRight(Cut: 50.0f, pLeft: &ButtonBar, pRight: &Button);
5387 if(DoButton_Editor(pId: &s_RefreshButton, pText: "Refresh", Checked: 0, pRect: &Button, Flags: 0, pToolTip: nullptr) || (s_ListBox.Active() && (Input()->KeyIsPressed(Key: KEY_F5) || (Input()->ModifierIsPressed() && Input()->KeyIsPressed(Key: KEY_R)))))
5388 FilelistPopulate(StorageType: m_FileDialogLastPopulatedStorageType, KeepSelection: true);
5389
5390 if(m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType != IStorage::TYPE_ALL)
5391 {
5392 ButtonBar.VSplitRight(Cut: ButtonSpacing, pLeft: &ButtonBar, pRight: nullptr);
5393 ButtonBar.VSplitRight(Cut: 90.0f, pLeft: &ButtonBar, pRight: &Button);
5394 if(DoButton_Editor(pId: &s_ShowDirectoryButton, pText: "Show directory", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Open the current directory in the file browser"))
5395 {
5396 char aOpenPath[IO_MAX_PATH_LENGTH];
5397 Storage()->GetCompletePath(Type: m_FilesSelectedIndex >= 0 ? m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, pDir: m_pFileDialogPath, pBuffer: aOpenPath, BufferSize: sizeof(aOpenPath));
5398 if(!open_file(path: aOpenPath))
5399 {
5400 ShowFileDialogError(pFormat: "Failed to open the directory '%s'.", aOpenPath);
5401 }
5402 }
5403 }
5404
5405 ButtonBar.VSplitRight(Cut: ButtonSpacing, pLeft: &ButtonBar, pRight: nullptr);
5406 ButtonBar.VSplitRight(Cut: 50.0f, pLeft: &ButtonBar, pRight: &Button);
5407 static CUi::SConfirmPopupContext s_ConfirmDeletePopupContext;
5408 if(m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType == IStorage::TYPE_SAVE && !m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsLink && str_comp(a: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, b: "..") != 0)
5409 {
5410 if(DoButton_Editor(pId: &s_DeleteButton, pText: "Delete", Checked: 0, pRect: &Button, Flags: 0, pToolTip: nullptr) || (s_ListBox.Active() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DELETE)))
5411 {
5412 s_ConfirmDeletePopupContext.Reset();
5413 s_ConfirmDeletePopupContext.YesNoButtons();
5414 str_format(buffer: s_ConfirmDeletePopupContext.m_aMessage, buffer_size: sizeof(s_ConfirmDeletePopupContext.m_aMessage), format: "Are you sure that you want to delete the %s '%s/%s'?", IsDir ? "folder" : "file", m_pFileDialogPath, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename);
5415 Ui()->ShowPopupConfirm(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext: &s_ConfirmDeletePopupContext);
5416 }
5417 if(s_ConfirmDeletePopupContext.m_Result == CUi::SConfirmPopupContext::CONFIRMED)
5418 {
5419 char aDeleteFilePath[IO_MAX_PATH_LENGTH];
5420 str_format(buffer: aDeleteFilePath, buffer_size: sizeof(aDeleteFilePath), format: "%s/%s", m_pFileDialogPath, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename);
5421 if(IsDir)
5422 {
5423 if(Storage()->RemoveFolder(pFilename: aDeleteFilePath, Type: IStorage::TYPE_SAVE))
5424 FilelistPopulate(StorageType: m_FileDialogLastPopulatedStorageType, KeepSelection: true);
5425 else
5426 ShowFileDialogError(pFormat: "Failed to delete folder '%s'. Make sure it's empty first.", aDeleteFilePath);
5427 }
5428 else
5429 {
5430 if(Storage()->RemoveFile(pFilename: aDeleteFilePath, Type: IStorage::TYPE_SAVE))
5431 FilelistPopulate(StorageType: m_FileDialogLastPopulatedStorageType, KeepSelection: true);
5432 else
5433 ShowFileDialogError(pFormat: "Failed to delete file '%s'.", aDeleteFilePath);
5434 }
5435 UpdateFileNameInput();
5436 }
5437 if(s_ConfirmDeletePopupContext.m_Result != CUi::SConfirmPopupContext::UNSET)
5438 s_ConfirmDeletePopupContext.Reset();
5439 }
5440 else
5441 s_ConfirmDeletePopupContext.Reset();
5442
5443 if(!m_FileDialogShowingRoot && m_FileDialogStorageType == IStorage::TYPE_SAVE)
5444 {
5445 ButtonBar.VSplitLeft(Cut: 70.0f, pLeft: &Button, pRight: &ButtonBar);
5446 if(DoButton_Editor(pId: &s_NewFolderButton, pText: "New folder", Checked: 0, pRect: &Button, Flags: 0, pToolTip: nullptr))
5447 {
5448 m_FileDialogNewFolderNameInput.Clear();
5449 static SPopupMenuId s_PopupNewFolderId;
5450 constexpr float PopupWidth = 400.0f;
5451 constexpr float PopupHeight = 110.0f;
5452 Ui()->DoPopupMenu(pId: &s_PopupNewFolderId, X: Width / 2.0f - PopupWidth / 2.0f, Y: Height / 2.0f - PopupHeight / 2.0f, Width: PopupWidth, Height: PopupHeight, pContext: this, pfnFunc: PopupNewFolder);
5453 Ui()->SetActiveItem(&m_FileDialogNewFolderNameInput);
5454 }
5455 }
5456}
5457
5458void CEditor::RefreshFilteredFileList()
5459{
5460 m_vpFilteredFileList.clear();
5461 for(const CFilelistItem &Item : m_vCompleteFileList)
5462 {
5463 if(m_FileDialogFilterInput.IsEmpty() || str_find_nocase(haystack: Item.m_aName, needle: m_FileDialogFilterInput.GetString()))
5464 {
5465 m_vpFilteredFileList.push_back(x: &Item);
5466 }
5467 }
5468 if(!m_FileDialogShowingRoot)
5469 SortFilteredFileList();
5470 if(!m_vpFilteredFileList.empty())
5471 {
5472 if(m_aFilesSelectedName[0])
5473 {
5474 for(size_t i = 0; i < m_vpFilteredFileList.size(); i++)
5475 {
5476 if(m_aFilesSelectedName[0] && str_comp(a: m_vpFilteredFileList[i]->m_aName, b: m_aFilesSelectedName) == 0)
5477 {
5478 m_FilesSelectedIndex = i;
5479 break;
5480 }
5481 }
5482 }
5483 m_FilesSelectedIndex = clamp<int>(val: m_FilesSelectedIndex, lo: 0, hi: m_vpFilteredFileList.size() - 1);
5484 str_copy(dst&: m_aFilesSelectedName, src: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aName);
5485 }
5486 else
5487 {
5488 m_FilesSelectedIndex = -1;
5489 m_aFilesSelectedName[0] = '\0';
5490 }
5491}
5492
5493void CEditor::FilelistPopulate(int StorageType, bool KeepSelection)
5494{
5495 m_FileDialogLastPopulatedStorageType = StorageType;
5496 m_vCompleteFileList.clear();
5497 if(m_FileDialogShowingRoot)
5498 {
5499 {
5500 CFilelistItem Item;
5501 str_copy(dst&: Item.m_aFilename, src: m_pFileDialogPath);
5502 str_copy(dst&: Item.m_aName, src: "All combined");
5503 Item.m_IsDir = true;
5504 Item.m_IsLink = true;
5505 Item.m_StorageType = IStorage::TYPE_ALL;
5506 Item.m_TimeModified = 0;
5507 m_vCompleteFileList.push_back(x: Item);
5508 }
5509
5510 for(int CheckStorageType = IStorage::TYPE_SAVE; CheckStorageType < Storage()->NumPaths(); ++CheckStorageType)
5511 {
5512 if(Storage()->FolderExists(pFilename: m_pFileDialogPath, Type: CheckStorageType))
5513 {
5514 CFilelistItem Item;
5515 str_copy(dst&: Item.m_aFilename, src: m_pFileDialogPath);
5516 Storage()->GetCompletePath(Type: CheckStorageType, pDir: m_pFileDialogPath, pBuffer: Item.m_aName, BufferSize: sizeof(Item.m_aName));
5517 str_append(dst: Item.m_aName, src: "/", dst_size: sizeof(Item.m_aName));
5518 Item.m_IsDir = true;
5519 Item.m_IsLink = true;
5520 Item.m_StorageType = CheckStorageType;
5521 Item.m_TimeModified = 0;
5522 m_vCompleteFileList.push_back(x: Item);
5523 }
5524 }
5525 }
5526 else
5527 {
5528 // Add links for downloadedmaps and themes
5529 if(!str_comp(a: m_pFileDialogPath, b: "maps"))
5530 {
5531 if(str_comp(a: m_pFileDialogButtonText, b: "Save") != 0 && Storage()->FolderExists(pFilename: "downloadedmaps", Type: StorageType))
5532 {
5533 CFilelistItem Item;
5534 str_copy(dst&: Item.m_aFilename, src: "downloadedmaps");
5535 str_copy(dst&: Item.m_aName, src: "downloadedmaps/");
5536 Item.m_IsDir = true;
5537 Item.m_IsLink = true;
5538 Item.m_StorageType = StorageType;
5539 Item.m_TimeModified = 0;
5540 m_vCompleteFileList.push_back(x: Item);
5541 }
5542
5543 if(Storage()->FolderExists(pFilename: "themes", Type: StorageType))
5544 {
5545 CFilelistItem Item;
5546 str_copy(dst&: Item.m_aFilename, src: "themes");
5547 str_copy(dst&: Item.m_aName, src: "themes/");
5548 Item.m_IsDir = true;
5549 Item.m_IsLink = true;
5550 Item.m_StorageType = StorageType;
5551 Item.m_TimeModified = 0;
5552 m_vCompleteFileList.push_back(x: Item);
5553 }
5554 }
5555 Storage()->ListDirectoryInfo(Type: StorageType, pPath: m_pFileDialogPath, pfnCallback: EditorListdirCallback, pUser: this);
5556 }
5557 RefreshFilteredFileList();
5558 if(!KeepSelection)
5559 {
5560 m_FilesSelectedIndex = m_vpFilteredFileList.empty() ? -1 : 0;
5561 if(m_FilesSelectedIndex >= 0)
5562 str_copy(dst&: m_aFilesSelectedName, src: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aName);
5563 else
5564 m_aFilesSelectedName[0] = '\0';
5565 }
5566 m_FilePreviewState = PREVIEW_UNLOADED;
5567}
5568
5569void CEditor::InvokeFileDialog(int StorageType, int FileType, const char *pTitle, const char *pButtonText,
5570 const char *pBasePath, bool FilenameAsDefault,
5571 bool (*pfnFunc)(const char *pFileName, int StorageType, void *pUser), void *pUser)
5572{
5573 m_FileDialogStorageType = StorageType;
5574 if(m_FileDialogStorageType == IStorage::TYPE_ALL)
5575 {
5576 int NumStoragesWithFolder = 0;
5577 for(int CheckStorageType = IStorage::TYPE_SAVE; CheckStorageType < Storage()->NumPaths(); ++CheckStorageType)
5578 {
5579 if(Storage()->FolderExists(pFilename: pBasePath, Type: CheckStorageType))
5580 {
5581 NumStoragesWithFolder++;
5582 }
5583 }
5584 m_FileDialogMultipleStorages = NumStoragesWithFolder > 1;
5585 }
5586 else
5587 {
5588 m_FileDialogMultipleStorages = false;
5589 }
5590
5591 Ui()->ClosePopupMenus();
5592 m_pFileDialogTitle = pTitle;
5593 m_pFileDialogButtonText = pButtonText;
5594 m_pfnFileDialogFunc = pfnFunc;
5595 m_pFileDialogUser = pUser;
5596 m_FileDialogFileNameInput.Clear();
5597 m_FileDialogFilterInput.Clear();
5598 m_aFileDialogCurrentFolder[0] = 0;
5599 m_aFileDialogCurrentLink[0] = 0;
5600 m_pFileDialogPath = m_aFileDialogCurrentFolder;
5601 m_FileDialogFileType = FileType;
5602 m_FilePreviewState = PREVIEW_UNLOADED;
5603 m_FileDialogOpening = true;
5604 m_FileDialogShowingRoot = false;
5605
5606 if(FilenameAsDefault)
5607 {
5608 char aDefaultName[IO_MAX_PATH_LENGTH];
5609 fs_split_file_extension(filename: fs_filename(path: m_aFileName), name: aDefaultName, name_size: sizeof(aDefaultName));
5610 m_FileDialogFileNameInput.Set(aDefaultName);
5611 }
5612 if(pBasePath)
5613 str_copy(dst&: m_aFileDialogCurrentFolder, src: pBasePath);
5614
5615 FilelistPopulate(StorageType: m_FileDialogStorageType);
5616
5617 m_FileDialogOpening = true;
5618 m_Dialog = DIALOG_FILE;
5619}
5620
5621void CEditor::ShowFileDialogError(const char *pFormat, ...)
5622{
5623 char aMessage[1024];
5624 va_list VarArgs;
5625 va_start(VarArgs, pFormat);
5626 str_format_v(buffer: aMessage, buffer_size: sizeof(aMessage), format: pFormat, args: VarArgs);
5627 va_end(VarArgs);
5628
5629 auto ContextIterator = m_PopupMessageContexts.find(x: aMessage);
5630 CUi::SMessagePopupContext *pContext;
5631 if(ContextIterator != m_PopupMessageContexts.end())
5632 {
5633 pContext = ContextIterator->second;
5634 Ui()->ClosePopupMenu(pId: pContext);
5635 }
5636 else
5637 {
5638 pContext = new CUi::SMessagePopupContext();
5639 pContext->ErrorColor();
5640 str_copy(dst&: pContext->m_aMessage, src: aMessage);
5641 m_PopupMessageContexts[pContext->m_aMessage] = pContext;
5642 }
5643 Ui()->ShowPopupMessage(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext);
5644}
5645
5646void CEditor::RenderModebar(CUIRect View)
5647{
5648 CUIRect Mentions, IngameMoved, ModeButtons, ModeButton;
5649 View.HSplitTop(Cut: 12.0f, pTop: &Mentions, pBottom: &View);
5650 View.HSplitTop(Cut: 12.0f, pTop: &IngameMoved, pBottom: &View);
5651 View.HSplitTop(Cut: 8.0f, pTop: nullptr, pBottom: &ModeButtons);
5652 const float Width = m_ToolBoxWidth - 5.0f;
5653 ModeButtons.VSplitLeft(Cut: Width, pLeft: &ModeButtons, pRight: nullptr);
5654 const float ButtonWidth = Width / 3;
5655
5656 // mentions
5657 if(m_Mentions)
5658 {
5659 char aBuf[64];
5660 if(m_Mentions == 1)
5661 str_copy(dst&: aBuf, src: Localize(pStr: "1 new mention"));
5662 else if(m_Mentions <= 9)
5663 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d new mentions"), m_Mentions);
5664 else
5665 str_copy(dst&: aBuf, src: Localize(pStr: "9+ new mentions"));
5666
5667 TextRender()->TextColor(rgb: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
5668 Ui()->DoLabel(pRect: &Mentions, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MC);
5669 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
5670 }
5671
5672 // ingame moved warning
5673 if(m_IngameMoved)
5674 {
5675 TextRender()->TextColor(rgb: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
5676 Ui()->DoLabel(pRect: &IngameMoved, pText: Localize(pStr: "Moved ingame"), Size: 10.0f, Align: TEXTALIGN_MC);
5677 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
5678 }
5679
5680 // mode buttons
5681 {
5682 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
5683 static int s_LayersButton = 0;
5684 if(DoButton_FontIcon(pId: &s_LayersButton, pText: FONT_ICON_LAYER_GROUP, Checked: m_Mode == MODE_LAYERS, pRect: &ModeButton, Flags: 0, pToolTip: "Go to layers management.", Corners: IGraphics::CORNER_L))
5685 {
5686 m_Mode = MODE_LAYERS;
5687 }
5688
5689 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
5690 static int s_ImagesButton = 0;
5691 if(DoButton_FontIcon(pId: &s_ImagesButton, pText: FONT_ICON_IMAGE, Checked: m_Mode == MODE_IMAGES, pRect: &ModeButton, Flags: 0, pToolTip: "Go to images management.", Corners: IGraphics::CORNER_NONE))
5692 {
5693 m_Mode = MODE_IMAGES;
5694 }
5695
5696 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
5697 static int s_SoundsButton = 0;
5698 if(DoButton_FontIcon(pId: &s_SoundsButton, pText: FONT_ICON_MUSIC, Checked: m_Mode == MODE_SOUNDS, pRect: &ModeButton, Flags: 0, pToolTip: "Go to sounds management.", Corners: IGraphics::CORNER_R))
5699 {
5700 m_Mode = MODE_SOUNDS;
5701 }
5702
5703 if(Input()->KeyPress(Key: KEY_LEFT) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
5704 {
5705 m_Mode = (m_Mode + NUM_MODES - 1) % NUM_MODES;
5706 }
5707 else if(Input()->KeyPress(Key: KEY_RIGHT) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
5708 {
5709 m_Mode = (m_Mode + 1) % NUM_MODES;
5710 }
5711 }
5712}
5713
5714void CEditor::RenderStatusbar(CUIRect View, CUIRect *pTooltipRect)
5715{
5716 const bool ButtonsDisabled = m_ShowPicker;
5717
5718 CUIRect Button;
5719 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
5720 static int s_EnvelopeButton = 0;
5721 if(DoButton_Editor(pId: &s_EnvelopeButton, pText: "Envelopes", Checked: ButtonsDisabled ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES, pRect: &Button, Flags: 0, pToolTip: "Toggles the envelope editor.") == 1)
5722 {
5723 m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES ? EXTRAEDITOR_NONE : EXTRAEDITOR_ENVELOPES;
5724 }
5725
5726 View.VSplitRight(Cut: 10.0f, pLeft: &View, pRight: nullptr);
5727 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
5728 static int s_SettingsButton = 0;
5729 if(DoButton_Editor(pId: &s_SettingsButton, pText: "Server settings", Checked: ButtonsDisabled ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS, pRect: &Button, Flags: 0, pToolTip: "Toggles the server settings editor.") == 1)
5730 {
5731 m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS ? EXTRAEDITOR_NONE : EXTRAEDITOR_SERVER_SETTINGS;
5732 }
5733
5734 View.VSplitRight(Cut: 10.0f, pLeft: &View, pRight: nullptr);
5735 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
5736 static int s_HistoryButton = 0;
5737 if(DoButton_Editor(pId: &s_HistoryButton, pText: "History", Checked: ButtonsDisabled ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_HISTORY, pRect: &Button, Flags: 0, pToolTip: "Toggles the editor history view.") == 1)
5738 {
5739 m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_HISTORY ? EXTRAEDITOR_NONE : EXTRAEDITOR_HISTORY;
5740 }
5741
5742 View.VSplitRight(Cut: 10.0f, pLeft: pTooltipRect, pRight: nullptr);
5743}
5744
5745void CEditor::RenderTooltip(CUIRect TooltipRect)
5746{
5747 if(str_comp(a: m_aTooltip, b: "") == 0)
5748 return;
5749
5750 char aBuf[256];
5751 if(ms_pUiGotContext && ms_pUiGotContext == Ui()->HotItem())
5752 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s Right click for context menu.", m_aTooltip);
5753 else
5754 str_copy(dst&: aBuf, src: m_aTooltip);
5755
5756 SLabelProperties Props;
5757 Props.m_MaxWidth = TooltipRect.w;
5758 Props.m_EllipsisAtEnd = true;
5759 Ui()->DoLabel(pRect: &TooltipRect, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
5760}
5761
5762bool CEditor::IsEnvelopeUsed(int EnvelopeIndex) const
5763{
5764 for(const auto &pGroup : m_Map.m_vpGroups)
5765 {
5766 for(const auto &pLayer : pGroup->m_vpLayers)
5767 {
5768 if(pLayer->m_Type == LAYERTYPE_QUADS)
5769 {
5770 std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(r: pLayer);
5771 for(const auto &Quad : pLayerQuads->m_vQuads)
5772 {
5773 if(Quad.m_PosEnv == EnvelopeIndex || Quad.m_ColorEnv == EnvelopeIndex)
5774 {
5775 return true;
5776 }
5777 }
5778 }
5779 else if(pLayer->m_Type == LAYERTYPE_SOUNDS)
5780 {
5781 std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(r: pLayer);
5782 for(const auto &Source : pLayerSounds->m_vSources)
5783 {
5784 if(Source.m_PosEnv == EnvelopeIndex || Source.m_SoundEnv == EnvelopeIndex)
5785 {
5786 return true;
5787 }
5788 }
5789 }
5790 else if(pLayer->m_Type == LAYERTYPE_TILES)
5791 {
5792 std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(r: pLayer);
5793 if(pLayerTiles->m_ColorEnv == EnvelopeIndex)
5794 return true;
5795 }
5796 }
5797 }
5798 return false;
5799}
5800
5801void CEditor::RemoveUnusedEnvelopes()
5802{
5803 m_EnvelopeEditorHistory.BeginBulk();
5804 int DeletedCount = 0;
5805 for(size_t Envelope = 0; Envelope < m_Map.m_vpEnvelopes.size();)
5806 {
5807 if(IsEnvelopeUsed(EnvelopeIndex: Envelope))
5808 {
5809 ++Envelope;
5810 }
5811 else
5812 {
5813 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEveloppeDelete>(args: this, args&: Envelope));
5814 m_Map.DeleteEnvelope(Index: Envelope);
5815 DeletedCount++;
5816 }
5817 }
5818 char aDisplay[256];
5819 str_format(buffer: aDisplay, buffer_size: sizeof(aDisplay), format: "Tool 'Remove unused envelopes': delete %d envelopes", DeletedCount);
5820 m_EnvelopeEditorHistory.EndBulk(pDisplay: aDisplay);
5821}
5822
5823void CEditor::ZoomAdaptOffsetX(float ZoomFactor, const CUIRect &View)
5824{
5825 float PosX = g_Config.m_EdZoomTarget ? (Ui()->MouseX() - View.x) / View.w : 0.5f;
5826 m_OffsetEnvelopeX = PosX - (PosX - m_OffsetEnvelopeX) * ZoomFactor;
5827}
5828
5829void CEditor::UpdateZoomEnvelopeX(const CUIRect &View)
5830{
5831 float OldZoom = m_ZoomEnvelopeX.GetValue();
5832 if(m_ZoomEnvelopeX.UpdateValue())
5833 ZoomAdaptOffsetX(ZoomFactor: OldZoom / m_ZoomEnvelopeX.GetValue(), View);
5834}
5835
5836void CEditor::ZoomAdaptOffsetY(float ZoomFactor, const CUIRect &View)
5837{
5838 float PosY = g_Config.m_EdZoomTarget ? 1.0f - (Ui()->MouseY() - View.y) / View.h : 0.5f;
5839 m_OffsetEnvelopeY = PosY - (PosY - m_OffsetEnvelopeY) * ZoomFactor;
5840}
5841
5842void CEditor::UpdateZoomEnvelopeY(const CUIRect &View)
5843{
5844 float OldZoom = m_ZoomEnvelopeY.GetValue();
5845 if(m_ZoomEnvelopeY.UpdateValue())
5846 ZoomAdaptOffsetY(ZoomFactor: OldZoom / m_ZoomEnvelopeY.GetValue(), View);
5847}
5848
5849void CEditor::ResetZoomEnvelope(const std::shared_ptr<CEnvelope> &pEnvelope, int ActiveChannels)
5850{
5851 auto [Bottom, Top] = pEnvelope->GetValueRange(ChannelMask: ActiveChannels);
5852 float EndTime = pEnvelope->EndTime();
5853 float ValueRange = absolute(a: Top - Bottom);
5854
5855 if(ValueRange < m_ZoomEnvelopeY.GetMinValue())
5856 {
5857 // Set view to some sane default if range is too small
5858 m_OffsetEnvelopeY = 0.5f - ValueRange / m_ZoomEnvelopeY.GetMinValue() / 2.0f - Bottom / m_ZoomEnvelopeY.GetMinValue();
5859 m_ZoomEnvelopeY.SetValueInstant(m_ZoomEnvelopeY.GetMinValue());
5860 }
5861 else if(ValueRange > m_ZoomEnvelopeY.GetMaxValue())
5862 {
5863 m_OffsetEnvelopeY = -Bottom / m_ZoomEnvelopeY.GetMaxValue();
5864 m_ZoomEnvelopeY.SetValueInstant(m_ZoomEnvelopeY.GetMaxValue());
5865 }
5866 else
5867 {
5868 // calculate biggest possible spacing
5869 float SpacingFactor = minimum(a: 1.25f, b: m_ZoomEnvelopeY.GetMaxValue() / ValueRange);
5870 m_ZoomEnvelopeY.SetValueInstant(SpacingFactor * ValueRange);
5871 float Space = 1.0f / SpacingFactor;
5872 float Spacing = (1.0f - Space) / 2.0f;
5873
5874 if(Top >= 0 && Bottom >= 0)
5875 m_OffsetEnvelopeY = Spacing - Bottom / m_ZoomEnvelopeY.GetValue();
5876 else if(Top <= 0 && Bottom <= 0)
5877 m_OffsetEnvelopeY = Spacing - Bottom / m_ZoomEnvelopeY.GetValue();
5878 else
5879 m_OffsetEnvelopeY = Spacing + Space * absolute(a: Bottom) / ValueRange;
5880 }
5881
5882 if(EndTime < m_ZoomEnvelopeX.GetMinValue())
5883 {
5884 m_OffsetEnvelopeX = 0.5f - EndTime / m_ZoomEnvelopeX.GetMinValue();
5885 m_ZoomEnvelopeX.SetValueInstant(m_ZoomEnvelopeX.GetMinValue());
5886 }
5887 else if(EndTime > m_ZoomEnvelopeX.GetMaxValue())
5888 {
5889 m_OffsetEnvelopeX = 0.0f;
5890 m_ZoomEnvelopeX.SetValueInstant(m_ZoomEnvelopeX.GetMaxValue());
5891 }
5892 else
5893 {
5894 float SpacingFactor = minimum(a: 1.25f, b: m_ZoomEnvelopeX.GetMaxValue() / EndTime);
5895 m_ZoomEnvelopeX.SetValueInstant(SpacingFactor * EndTime);
5896 float Space = 1.0f / SpacingFactor;
5897 float Spacing = (1.0f - Space) / 2.0f;
5898
5899 m_OffsetEnvelopeX = Spacing;
5900 }
5901}
5902
5903float fxt2f(int t)
5904{
5905 return t / 1000.0f;
5906}
5907
5908int f2fxt(float t)
5909{
5910 return static_cast<int>(t * 1000.0f);
5911}
5912
5913float CEditor::ScreenToEnvelopeX(const CUIRect &View, float x) const
5914{
5915 return (x - View.x - View.w * m_OffsetEnvelopeX) / View.w * m_ZoomEnvelopeX.GetValue();
5916}
5917
5918float CEditor::EnvelopeToScreenX(const CUIRect &View, float x) const
5919{
5920 return View.x + View.w * m_OffsetEnvelopeX + x / m_ZoomEnvelopeX.GetValue() * View.w;
5921}
5922
5923float CEditor::ScreenToEnvelopeY(const CUIRect &View, float y) const
5924{
5925 return (View.h - y + View.y) / View.h * m_ZoomEnvelopeY.GetValue() - m_OffsetEnvelopeY * m_ZoomEnvelopeY.GetValue();
5926}
5927
5928float CEditor::EnvelopeToScreenY(const CUIRect &View, float y) const
5929{
5930 return View.y + View.h - y / m_ZoomEnvelopeY.GetValue() * View.h - m_OffsetEnvelopeY * View.h;
5931}
5932
5933float CEditor::ScreenToEnvelopeDX(const CUIRect &View, float dx)
5934{
5935 return dx / Graphics()->ScreenWidth() * Ui()->Screen()->w / View.w * m_ZoomEnvelopeX.GetValue();
5936}
5937
5938float CEditor::ScreenToEnvelopeDY(const CUIRect &View, float dy)
5939{
5940 return dy / Graphics()->ScreenHeight() * Ui()->Screen()->h / View.h * m_ZoomEnvelopeY.GetValue();
5941}
5942
5943void CEditor::RemoveTimeOffsetEnvelope(const std::shared_ptr<CEnvelope> &pEnvelope)
5944{
5945 int TimeOffset = pEnvelope->m_vPoints[0].m_Time;
5946 for(auto &Point : pEnvelope->m_vPoints)
5947 Point.m_Time -= TimeOffset;
5948
5949 m_OffsetEnvelopeX += fxt2f(t: TimeOffset) / m_ZoomEnvelopeX.GetValue();
5950};
5951
5952static float ClampDelta(float Val, float Delta, float Min, float Max)
5953{
5954 if(Val + Delta <= Min)
5955 return Min - Val;
5956 if(Val + Delta >= Max)
5957 return Max - Val;
5958 return Delta;
5959}
5960
5961class CTimeStep
5962{
5963public:
5964 template<class T>
5965 CTimeStep(T t)
5966 {
5967 if constexpr(std::is_same_v<T, std::chrono::milliseconds>)
5968 m_Unit = ETimeUnit::MILLISECONDS;
5969 else if constexpr(std::is_same_v<T, std::chrono::seconds>)
5970 m_Unit = ETimeUnit::SECONDS;
5971 else
5972 m_Unit = ETimeUnit::MINUTES;
5973
5974 m_Value = t;
5975 }
5976
5977 CTimeStep operator*(int k) const
5978 {
5979 return CTimeStep(m_Value * k, m_Unit);
5980 }
5981
5982 CTimeStep operator-(const CTimeStep &Other)
5983 {
5984 return CTimeStep(m_Value - Other.m_Value, m_Unit);
5985 }
5986
5987 void Format(char *pBuffer, size_t BufferSize)
5988 {
5989 int Milliseconds = m_Value.count() % 1000;
5990 int Seconds = std::chrono::duration_cast<std::chrono::seconds>(d: m_Value).count() % 60;
5991 int Minutes = std::chrono::duration_cast<std::chrono::minutes>(d: m_Value).count();
5992
5993 switch(m_Unit)
5994 {
5995 case ETimeUnit::MILLISECONDS:
5996 if(Minutes != 0)
5997 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%d:%02d.%03dmin", Minutes, Seconds, Milliseconds);
5998 else if(Seconds != 0)
5999 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%d.%03ds", Seconds, Milliseconds);
6000 else
6001 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%dms", Milliseconds);
6002 break;
6003 case ETimeUnit::SECONDS:
6004 if(Minutes != 0)
6005 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%d:%02dmin", Minutes, Seconds);
6006 else
6007 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%ds", Seconds);
6008 break;
6009 case ETimeUnit::MINUTES:
6010 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%dmin", Minutes);
6011 break;
6012 }
6013 }
6014
6015 float AsSeconds() const
6016 {
6017 return std::chrono::duration_cast<std::chrono::duration<float>>(d: m_Value).count();
6018 }
6019
6020private:
6021 enum class ETimeUnit
6022 {
6023 MILLISECONDS,
6024 SECONDS,
6025 MINUTES
6026 } m_Unit;
6027 std::chrono::milliseconds m_Value;
6028
6029 CTimeStep(std::chrono::milliseconds Value, ETimeUnit Unit)
6030 {
6031 m_Value = Value;
6032 m_Unit = Unit;
6033 }
6034};
6035
6036void CEditor::SetHotEnvelopePoint(const CUIRect &View, const std::shared_ptr<CEnvelope> &pEnvelope, int ActiveChannels)
6037{
6038 if(!Ui()->MouseInside(pRect: &View))
6039 return;
6040
6041 float mx = Ui()->MouseX();
6042 float my = Ui()->MouseY();
6043
6044 float MinDist = 200.0f;
6045 int *pMinPoint = nullptr;
6046
6047 auto UpdateMinimum = [&](float px, float py, int *pId) {
6048 float dx = px - mx;
6049 float dy = py - my;
6050
6051 float CurrDist = dx * dx + dy * dy;
6052 if(CurrDist < MinDist)
6053 {
6054 MinDist = CurrDist;
6055 pMinPoint = pId;
6056 }
6057 };
6058
6059 for(size_t i = 0; i < pEnvelope->m_vPoints.size(); i++)
6060 {
6061 for(int c = pEnvelope->GetChannels() - 1; c >= 0; c--)
6062 {
6063 if(!(ActiveChannels & (1 << c)))
6064 continue;
6065
6066 if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER)
6067 {
6068 float px = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]));
6069 float py = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]));
6070 UpdateMinimum(px, py, &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]);
6071 }
6072
6073 if(pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER)
6074 {
6075 float px = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]));
6076 float py = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]));
6077 UpdateMinimum(px, py, &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]);
6078 }
6079
6080 float px = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time));
6081 float py = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]));
6082 UpdateMinimum(px, py, &pEnvelope->m_vPoints[i].m_aValues[c]);
6083 }
6084 }
6085
6086 if(pMinPoint != nullptr)
6087 Ui()->SetHotItem(pMinPoint);
6088}
6089
6090void CEditor::RenderEnvelopeEditor(CUIRect View)
6091{
6092 if(m_SelectedEnvelope < 0)
6093 m_SelectedEnvelope = 0;
6094 if(m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size())
6095 m_SelectedEnvelope = m_Map.m_vpEnvelopes.size() - 1;
6096
6097 std::shared_ptr<CEnvelope> pEnvelope = nullptr;
6098 if(m_SelectedEnvelope >= 0 && m_SelectedEnvelope < (int)m_Map.m_vpEnvelopes.size())
6099 pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope];
6100
6101 static EEnvelopeEditorOp s_Operation = EEnvelopeEditorOp::OP_NONE;
6102 static std::vector<float> s_vAccurateDragValuesX = {};
6103 static std::vector<float> s_vAccurateDragValuesY = {};
6104 static float s_MouseXStart = 0.0f;
6105 static float s_MouseYStart = 0.0f;
6106
6107 static CLineInput s_NameInput;
6108
6109 CUIRect ToolBar, CurveBar, ColorBar, DragBar;
6110 View.HSplitTop(Cut: 30.0f, pTop: &DragBar, pBottom: nullptr);
6111 DragBar.y -= 2.0f;
6112 DragBar.w += 2.0f;
6113 DragBar.h += 4.0f;
6114 DoEditorDragBar(View, pDragBar: &DragBar, Side: EDragSide::SIDE_TOP, pValue: &m_aExtraEditorSplits[EXTRAEDITOR_ENVELOPES]);
6115 View.HSplitTop(Cut: 15.0f, pTop: &ToolBar, pBottom: &View);
6116 View.HSplitTop(Cut: 15.0f, pTop: &CurveBar, pBottom: &View);
6117 ToolBar.Margin(Cut: 2.0f, pOtherRect: &ToolBar);
6118 CurveBar.Margin(Cut: 2.0f, pOtherRect: &CurveBar);
6119
6120 bool CurrentEnvelopeSwitched = false;
6121
6122 // do the toolbar
6123 static int s_ActiveChannels = 0xf;
6124 {
6125 CUIRect Button;
6126 std::shared_ptr<CEnvelope> pNewEnv = nullptr;
6127
6128 // redo button
6129 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
6130 static int s_RedoButton = 0;
6131 if(DoButton_FontIcon(pId: &s_RedoButton, pText: FONT_ICON_REDO, Checked: m_EnvelopeEditorHistory.CanRedo() ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: "[Ctrl+Y] Redo last action", Corners: IGraphics::CORNER_R, FontSize: 11.0f) == 1)
6132 {
6133 m_EnvelopeEditorHistory.Redo();
6134 }
6135
6136 // undo button
6137 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
6138 ToolBar.VSplitRight(Cut: 10.0f, pLeft: &ToolBar, pRight: nullptr);
6139 static int s_UndoButton = 0;
6140 if(DoButton_FontIcon(pId: &s_UndoButton, pText: FONT_ICON_UNDO, Checked: m_EnvelopeEditorHistory.CanUndo() ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: "[Ctrl+Z] Undo last action", Corners: IGraphics::CORNER_L, FontSize: 11.0f) == 1)
6141 {
6142 m_EnvelopeEditorHistory.Undo();
6143 }
6144
6145 ToolBar.VSplitRight(Cut: 50.0f, pLeft: &ToolBar, pRight: &Button);
6146 static int s_NewSoundButton = 0;
6147 if(DoButton_Editor(pId: &s_NewSoundButton, pText: "Sound+", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Creates a new sound envelope"))
6148 {
6149 m_Map.OnModify();
6150 pNewEnv = m_Map.NewEnvelope(Type: CEnvelope::EType::SOUND);
6151 }
6152
6153 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
6154 ToolBar.VSplitRight(Cut: 50.0f, pLeft: &ToolBar, pRight: &Button);
6155 static int s_New4dButton = 0;
6156 if(DoButton_Editor(pId: &s_New4dButton, pText: "Color+", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Creates a new color envelope"))
6157 {
6158 m_Map.OnModify();
6159 pNewEnv = m_Map.NewEnvelope(Type: CEnvelope::EType::COLOR);
6160 }
6161
6162 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
6163 ToolBar.VSplitRight(Cut: 50.0f, pLeft: &ToolBar, pRight: &Button);
6164 static int s_New2dButton = 0;
6165 if(DoButton_Editor(pId: &s_New2dButton, pText: "Pos.+", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Creates a new position envelope"))
6166 {
6167 m_Map.OnModify();
6168 pNewEnv = m_Map.NewEnvelope(Type: CEnvelope::EType::POSITION);
6169 }
6170
6171 if(m_SelectedEnvelope >= 0)
6172 {
6173 // Delete button
6174 ToolBar.VSplitRight(Cut: 10.0f, pLeft: &ToolBar, pRight: nullptr);
6175 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
6176 static int s_DeleteButton = 0;
6177 if(DoButton_Editor(pId: &s_DeleteButton, pText: "✗", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Delete this envelope"))
6178 {
6179 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEveloppeDelete>(args: this, args&: m_SelectedEnvelope));
6180 m_Map.DeleteEnvelope(Index: m_SelectedEnvelope);
6181 if(m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size())
6182 m_SelectedEnvelope = m_Map.m_vpEnvelopes.size() - 1;
6183 pEnvelope = m_SelectedEnvelope >= 0 ? m_Map.m_vpEnvelopes[m_SelectedEnvelope] : nullptr;
6184 m_Map.OnModify();
6185 }
6186
6187 // Move right button
6188 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
6189 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
6190 static int s_MoveRightButton = 0;
6191 if(DoButton_Ex(pId: &s_MoveRightButton, pText: "→", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Move this envelope to the right", Corners: IGraphics::CORNER_R))
6192 {
6193 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEdit>(args: this, args&: m_SelectedEnvelope, args: CEditorActionEnvelopeEdit::EEditType::ORDER, args&: m_SelectedEnvelope, args: m_SelectedEnvelope + 1));
6194 m_Map.SwapEnvelopes(Index0: m_SelectedEnvelope, Index1: m_SelectedEnvelope + 1);
6195 m_SelectedEnvelope = clamp<int>(val: m_SelectedEnvelope + 1, lo: 0, hi: m_Map.m_vpEnvelopes.size() - 1);
6196 pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope];
6197 m_Map.OnModify();
6198 }
6199
6200 // Move left button
6201 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
6202 static int s_MoveLeftButton = 0;
6203 if(DoButton_Ex(pId: &s_MoveLeftButton, pText: "←", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Move this envelope to the left", Corners: IGraphics::CORNER_L))
6204 {
6205 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEdit>(args: this, args&: m_SelectedEnvelope, args: CEditorActionEnvelopeEdit::EEditType::ORDER, args&: m_SelectedEnvelope, args: m_SelectedEnvelope - 1));
6206 m_Map.SwapEnvelopes(Index0: m_SelectedEnvelope - 1, Index1: m_SelectedEnvelope);
6207 m_SelectedEnvelope = clamp<int>(val: m_SelectedEnvelope - 1, lo: 0, hi: m_Map.m_vpEnvelopes.size() - 1);
6208 pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope];
6209 m_Map.OnModify();
6210 }
6211
6212 if(pEnvelope)
6213 {
6214 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
6215 ToolBar.VSplitRight(Cut: 20.0f, pLeft: &ToolBar, pRight: &Button);
6216 static int s_ZoomOutButton = 0;
6217 if(DoButton_FontIcon(pId: &s_ZoomOutButton, pText: FONT_ICON_MINUS, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[NumPad-] Zoom out horizontally, hold shift to zoom vertically", Corners: IGraphics::CORNER_R, FontSize: 9.0f))
6218 {
6219 if(Input()->ShiftIsPressed())
6220 m_ZoomEnvelopeY.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeY.GetValue());
6221 else
6222 m_ZoomEnvelopeX.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeX.GetValue());
6223 }
6224
6225 ToolBar.VSplitRight(Cut: 20.0f, pLeft: &ToolBar, pRight: &Button);
6226 static int s_ResetZoomButton = 0;
6227 if(DoButton_FontIcon(pId: &s_ResetZoomButton, pText: FONT_ICON_MAGNIFYING_GLASS, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[NumPad*] Reset zoom to default value", Corners: IGraphics::CORNER_NONE, FontSize: 9.0f))
6228 ResetZoomEnvelope(pEnvelope, ActiveChannels: s_ActiveChannels);
6229
6230 ToolBar.VSplitRight(Cut: 20.0f, pLeft: &ToolBar, pRight: &Button);
6231 static int s_ZoomInButton = 0;
6232 if(DoButton_FontIcon(pId: &s_ZoomInButton, pText: FONT_ICON_PLUS, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[NumPad+] Zoom in horizontally, hold shift to zoom vertically", Corners: IGraphics::CORNER_L, FontSize: 9.0f))
6233 {
6234 if(Input()->ShiftIsPressed())
6235 m_ZoomEnvelopeY.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeY.GetValue());
6236 else
6237 m_ZoomEnvelopeX.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeX.GetValue());
6238 }
6239 }
6240
6241 // Margin on the right side
6242 ToolBar.VSplitRight(Cut: 7.0f, pLeft: &ToolBar, pRight: nullptr);
6243 }
6244
6245 if(pNewEnv) // add the default points
6246 {
6247 if(pNewEnv->GetChannels() == 4)
6248 {
6249 pNewEnv->AddPoint(Time: 0, v0: f2fx(v: 1.0f), v1: f2fx(v: 1.0f), v2: f2fx(v: 1.0f), v3: f2fx(v: 1.0f));
6250 pNewEnv->AddPoint(Time: 1000, v0: f2fx(v: 1.0f), v1: f2fx(v: 1.0f), v2: f2fx(v: 1.0f), v3: f2fx(v: 1.0f));
6251 }
6252 else
6253 {
6254 pNewEnv->AddPoint(Time: 0, v0: 0);
6255 pNewEnv->AddPoint(Time: 1000, v0: 0);
6256 }
6257
6258 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeAdd>(args: this, args&: pNewEnv));
6259 }
6260
6261 CUIRect Shifter, Inc, Dec;
6262 ToolBar.VSplitLeft(Cut: 60.0f, pLeft: &Shifter, pRight: &ToolBar);
6263 Shifter.VSplitRight(Cut: 15.0f, pLeft: &Shifter, pRight: &Inc);
6264 Shifter.VSplitLeft(Cut: 15.0f, pLeft: &Dec, pRight: &Shifter);
6265 char aBuf[64];
6266 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d/%d", m_SelectedEnvelope + 1, (int)m_Map.m_vpEnvelopes.size());
6267
6268 ColorRGBA EnvColor = ColorRGBA(1, 1, 1, 0.5f);
6269 if(!m_Map.m_vpEnvelopes.empty())
6270 {
6271 EnvColor = IsEnvelopeUsed(EnvelopeIndex: m_SelectedEnvelope) ? ColorRGBA(1, 0.7f, 0.7f, 0.5f) : ColorRGBA(0.7f, 1, 0.7f, 0.5f);
6272 }
6273
6274 static int s_EnvelopeSelector = 0;
6275 auto NewValueRes = UiDoValueSelector(pId: &s_EnvelopeSelector, pRect: &Shifter, pLabel: aBuf, Current: m_SelectedEnvelope + 1, Min: 1, Max: m_Map.m_vpEnvelopes.size(), Step: 1, Scale: 1.0f, pToolTip: "Select Envelope", IsDegree: false, IsHex: false, Corners: IGraphics::CORNER_NONE, pColor: &EnvColor, ShowValue: false);
6276 int NewValue = NewValueRes.m_Value;
6277 if(NewValue - 1 != m_SelectedEnvelope)
6278 {
6279 m_SelectedEnvelope = NewValue - 1;
6280 CurrentEnvelopeSwitched = true;
6281 }
6282
6283 static int s_PrevButton = 0;
6284 if(DoButton_FontIcon(pId: &s_PrevButton, pText: FONT_ICON_MINUS, Checked: 0, pRect: &Dec, Flags: 0, pToolTip: "Previous Envelope", Corners: IGraphics::CORNER_L, FontSize: 7.0f))
6285 {
6286 m_SelectedEnvelope--;
6287 if(m_SelectedEnvelope < 0)
6288 m_SelectedEnvelope = m_Map.m_vpEnvelopes.size() - 1;
6289 CurrentEnvelopeSwitched = true;
6290 }
6291
6292 static int s_NextButton = 0;
6293 if(DoButton_FontIcon(pId: &s_NextButton, pText: FONT_ICON_PLUS, Checked: 0, pRect: &Inc, Flags: 0, pToolTip: "Next Envelope", Corners: IGraphics::CORNER_R, FontSize: 7.0f))
6294 {
6295 m_SelectedEnvelope++;
6296 if(m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size())
6297 m_SelectedEnvelope = 0;
6298 CurrentEnvelopeSwitched = true;
6299 }
6300
6301 if(pEnvelope)
6302 {
6303 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: nullptr, pRight: &ToolBar);
6304 ToolBar.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolBar);
6305 Ui()->DoLabel(pRect: &Button, pText: "Name:", Size: 10.0f, Align: TEXTALIGN_MR);
6306
6307 ToolBar.VSplitLeft(Cut: 3.0f, pLeft: nullptr, pRight: &ToolBar);
6308 ToolBar.VSplitLeft(Cut: ToolBar.w > ToolBar.h * 40 ? 80.0f : 60.0f, pLeft: &Button, pRight: &ToolBar);
6309
6310 s_NameInput.SetBuffer(pStr: pEnvelope->m_aName, MaxSize: sizeof(pEnvelope->m_aName));
6311 if(DoEditBox(pLineInput: &s_NameInput, pRect: &Button, FontSize: 10.0f, Corners: IGraphics::CORNER_ALL, pToolTip: "The name of the selected envelope"))
6312 {
6313 m_Map.OnModify();
6314 }
6315 }
6316 }
6317
6318 bool ShowColorBar = false;
6319 if(pEnvelope && pEnvelope->GetChannels() == 4)
6320 {
6321 ShowColorBar = true;
6322 View.HSplitTop(Cut: 20.0f, pTop: &ColorBar, pBottom: &View);
6323 ColorBar.Margin(Cut: 2.0f, pOtherRect: &ColorBar);
6324 }
6325
6326 RenderBackground(View, Texture: m_CheckerTexture, Size: 32.0f, Brightness: 0.1f);
6327
6328 if(pEnvelope)
6329 {
6330 if(m_ResetZoomEnvelope)
6331 {
6332 m_ResetZoomEnvelope = false;
6333 ResetZoomEnvelope(pEnvelope, ActiveChannels: s_ActiveChannels);
6334 }
6335
6336 static int s_EnvelopeEditorId = 0;
6337
6338 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)};
6339
6340 CUIRect Button;
6341
6342 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: &Button, pRight: &ToolBar);
6343
6344 static const char *s_aapNames[4][CEnvPoint::MAX_CHANNELS] = {
6345 {"V", "", "", ""},
6346 {"", "", "", ""},
6347 {"X", "Y", "R", ""},
6348 {"R", "G", "B", "A"},
6349 };
6350
6351 static const char *s_aapDescriptions[4][CEnvPoint::MAX_CHANNELS] = {
6352 {"Volume of the envelope", "", "", ""},
6353 {"", "", "", ""},
6354 {"X-axis of the envelope", "Y-axis of the envelope", "Rotation of the envelope", ""},
6355 {"Red value of the envelope", "Green value of the envelope", "Blue value of the envelope", "Alpha value of the envelope"},
6356 };
6357
6358 static int s_aChannelButtons[CEnvPoint::MAX_CHANNELS] = {0};
6359 int Bit = 1;
6360
6361 for(int i = 0; i < CEnvPoint::MAX_CHANNELS; i++, Bit <<= 1)
6362 {
6363 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: &Button, pRight: &ToolBar);
6364 if(i < pEnvelope->GetChannels())
6365 {
6366 int Corners = IGraphics::CORNER_NONE;
6367 if(pEnvelope->GetChannels() == 1)
6368 Corners = IGraphics::CORNER_ALL;
6369 else if(i == 0)
6370 Corners = IGraphics::CORNER_L;
6371 else if(i == pEnvelope->GetChannels() - 1)
6372 Corners = IGraphics::CORNER_R;
6373
6374 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], BaseColor: aColors[i], Corners))
6375 s_ActiveChannels ^= Bit;
6376 }
6377 }
6378
6379 // toggle sync button
6380 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: nullptr, pRight: &ToolBar);
6381 ToolBar.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolBar);
6382
6383 static int s_SyncButton;
6384 if(DoButton_Editor(pId: &s_SyncButton, pText: "Sync", Checked: pEnvelope->m_Synchronized, pRect: &Button, Flags: 0, pToolTip: "Synchronize envelope animation to game time (restarts when you touch the start line)"))
6385 {
6386 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEdit>(args: this, args&: m_SelectedEnvelope, args: CEditorActionEnvelopeEdit::EEditType::SYNC, args&: pEnvelope->m_Synchronized, args: !pEnvelope->m_Synchronized));
6387 pEnvelope->m_Synchronized = !pEnvelope->m_Synchronized;
6388 m_Map.OnModify();
6389 }
6390
6391 const bool ShouldPan = s_Operation == EEnvelopeEditorOp::OP_NONE && (Ui()->MouseButton(Index: 2) || (Ui()->MouseButton(Index: 0) && Input()->ModifierIsPressed()));
6392 if(m_pContainerPanned == &s_EnvelopeEditorId)
6393 {
6394 if(!ShouldPan)
6395 m_pContainerPanned = nullptr;
6396 else
6397 {
6398 m_OffsetEnvelopeX += Ui()->MouseDeltaX() / Graphics()->ScreenWidth() * Ui()->Screen()->w / View.w;
6399 m_OffsetEnvelopeY -= Ui()->MouseDeltaY() / Graphics()->ScreenHeight() * Ui()->Screen()->h / View.h;
6400 }
6401 }
6402
6403 if(Ui()->MouseInside(pRect: &View) && m_Dialog == DIALOG_NONE)
6404 {
6405 Ui()->SetHotItem(&s_EnvelopeEditorId);
6406
6407 if(ShouldPan && m_pContainerPanned == nullptr)
6408 m_pContainerPanned = &s_EnvelopeEditorId;
6409
6410 if(Input()->KeyPress(Key: KEY_KP_MULTIPLY))
6411 ResetZoomEnvelope(pEnvelope, ActiveChannels: s_ActiveChannels);
6412 if(Input()->ShiftIsPressed())
6413 {
6414 if(Input()->KeyPress(Key: KEY_KP_MINUS))
6415 m_ZoomEnvelopeY.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeY.GetValue());
6416 if(Input()->KeyPress(Key: KEY_KP_PLUS))
6417 m_ZoomEnvelopeY.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeY.GetValue());
6418 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
6419 m_ZoomEnvelopeY.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeY.GetValue());
6420 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
6421 m_ZoomEnvelopeY.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeY.GetValue());
6422 }
6423 else
6424 {
6425 if(Input()->KeyPress(Key: KEY_KP_MINUS))
6426 m_ZoomEnvelopeX.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeX.GetValue());
6427 if(Input()->KeyPress(Key: KEY_KP_PLUS))
6428 m_ZoomEnvelopeX.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeX.GetValue());
6429 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
6430 m_ZoomEnvelopeX.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeX.GetValue());
6431 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
6432 m_ZoomEnvelopeX.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeX.GetValue());
6433 }
6434 }
6435
6436 if(Ui()->HotItem() == &s_EnvelopeEditorId)
6437 {
6438 // do stuff
6439 if(Ui()->MouseButton(Index: 0))
6440 {
6441 if(Input()->MouseDoubleClick())
6442 {
6443 // add point
6444 float Time = ScreenToEnvelopeX(View, x: Ui()->MouseX());
6445 ColorRGBA Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
6446 if(in_range(a: Time, lower: 0.0f, upper: pEnvelope->EndTime()))
6447 pEnvelope->Eval(Time, Result&: Channels, Channels: 4);
6448
6449 int FixedTime = std::round(x: Time * 1000.0f);
6450 bool TimeFound = false;
6451 for(CEnvPoint &Point : pEnvelope->m_vPoints)
6452 {
6453 if(Point.m_Time == FixedTime)
6454 TimeFound = true;
6455 }
6456
6457 if(!TimeFound)
6458 m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionAddEnvelopePoint>(args: this, args&: m_SelectedEnvelope, args&: FixedTime, args&: Channels));
6459
6460 if(FixedTime < 0)
6461 RemoveTimeOffsetEnvelope(pEnvelope);
6462 m_Map.OnModify();
6463 }
6464 else if(s_Operation != EEnvelopeEditorOp::OP_BOX_SELECT && !Input()->ModifierIsPressed())
6465 {
6466 static int s_BoxSelectId = 0;
6467 Ui()->SetActiveItem(&s_BoxSelectId);
6468 s_Operation = EEnvelopeEditorOp::OP_BOX_SELECT;
6469 s_MouseXStart = Ui()->MouseX();
6470 s_MouseYStart = Ui()->MouseY();
6471 }
6472 }
6473
6474 m_ShowEnvelopePreview = SHOWENV_SELECTED;
6475 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.");
6476 }
6477
6478 UpdateZoomEnvelopeX(View);
6479 UpdateZoomEnvelopeY(View);
6480
6481 {
6482 float UnitsPerLineY = 0.001f;
6483 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};
6484 for(float Value : s_aUnitPerLineOptionsY)
6485 {
6486 if(Value / m_ZoomEnvelopeY.GetValue() * View.h < 40.0f)
6487 UnitsPerLineY = Value;
6488 }
6489 int NumLinesY = m_ZoomEnvelopeY.GetValue() / static_cast<float>(UnitsPerLineY) + 1;
6490
6491 Ui()->ClipEnable(pRect: &View);
6492 Graphics()->TextureClear();
6493 Graphics()->LinesBegin();
6494 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.2f);
6495
6496 float BaseValue = static_cast<int>(m_OffsetEnvelopeY * m_ZoomEnvelopeY.GetValue() / UnitsPerLineY) * UnitsPerLineY;
6497 for(int i = 0; i <= NumLinesY; i++)
6498 {
6499 float Value = UnitsPerLineY * i - BaseValue;
6500 IGraphics::CLineItem LineItem(View.x, EnvelopeToScreenY(View, y: Value), View.x + View.w, EnvelopeToScreenY(View, y: Value));
6501 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
6502 }
6503
6504 Graphics()->LinesEnd();
6505
6506 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
6507 for(int i = 0; i <= NumLinesY; i++)
6508 {
6509 float Value = UnitsPerLineY * i - BaseValue;
6510 char aValueBuffer[16];
6511 if(UnitsPerLineY >= 1.0f)
6512 {
6513 str_from_int(value: static_cast<int>(Value), dst&: aValueBuffer);
6514 }
6515 else
6516 {
6517 str_format(buffer: aValueBuffer, buffer_size: sizeof(aValueBuffer), format: "%.3f", Value);
6518 }
6519 Ui()->TextRender()->Text(x: View.x, y: EnvelopeToScreenY(View, y: Value) + 4.0f, Size: 8.0f, pText: aValueBuffer);
6520 }
6521 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
6522 Ui()->ClipDisable();
6523 }
6524
6525 {
6526 using namespace std::chrono_literals;
6527 CTimeStep UnitsPerLineX = 1ms;
6528 static const CTimeStep s_aUnitPerLineOptionsX[] = {5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2s, 5s, 10s, 15s, 30s, 1min};
6529 for(CTimeStep Value : s_aUnitPerLineOptionsX)
6530 {
6531 if(Value.AsSeconds() / m_ZoomEnvelopeX.GetValue() * View.w < 160.0f)
6532 UnitsPerLineX = Value;
6533 }
6534 int NumLinesX = m_ZoomEnvelopeX.GetValue() / static_cast<float>(UnitsPerLineX.AsSeconds()) + 1;
6535
6536 Ui()->ClipEnable(pRect: &View);
6537 Graphics()->TextureClear();
6538 Graphics()->LinesBegin();
6539 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.2f);
6540
6541 CTimeStep BaseValue = UnitsPerLineX * static_cast<int>(m_OffsetEnvelopeX * m_ZoomEnvelopeX.GetValue() / UnitsPerLineX.AsSeconds());
6542 for(int i = 0; i <= NumLinesX; i++)
6543 {
6544 float Value = UnitsPerLineX.AsSeconds() * i - BaseValue.AsSeconds();
6545 IGraphics::CLineItem LineItem(EnvelopeToScreenX(View, x: Value), View.y, EnvelopeToScreenX(View, x: Value), View.y + View.h);
6546 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
6547 }
6548
6549 Graphics()->LinesEnd();
6550
6551 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
6552 for(int i = 0; i <= NumLinesX; i++)
6553 {
6554 CTimeStep Value = UnitsPerLineX * i - BaseValue;
6555 if(Value.AsSeconds() >= 0)
6556 {
6557 char aValueBuffer[16];
6558 Value.Format(pBuffer: aValueBuffer, BufferSize: sizeof(aValueBuffer));
6559
6560 Ui()->TextRender()->Text(x: EnvelopeToScreenX(View, x: Value.AsSeconds()) + 1.0f, y: View.y + View.h - 8.0f, Size: 8.0f, pText: aValueBuffer);
6561 }
6562 }
6563 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
6564 Ui()->ClipDisable();
6565 }
6566
6567 // render tangents for bezier curves
6568 {
6569 Ui()->ClipEnable(pRect: &View);
6570 Graphics()->TextureClear();
6571 Graphics()->LinesBegin();
6572 for(int c = 0; c < pEnvelope->GetChannels(); c++)
6573 {
6574 if(!(s_ActiveChannels & (1 << c)))
6575 continue;
6576
6577 for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++)
6578 {
6579 float PosX = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time));
6580 float PosY = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]));
6581
6582 // Out-Tangent
6583 if(pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER)
6584 {
6585 float TangentX = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]));
6586 float TangentY = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]));
6587
6588 if(IsTangentOutPointSelected(Index: i, Channel: c))
6589 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
6590 else
6591 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 0.4f);
6592
6593 IGraphics::CLineItem LineItem(TangentX, TangentY, PosX, PosY);
6594 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
6595 }
6596
6597 // In-Tangent
6598 if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER)
6599 {
6600 float TangentX = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]));
6601 float TangentY = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]));
6602
6603 if(IsTangentInPointSelected(Index: i, Channel: c))
6604 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
6605 else
6606 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 0.4f);
6607
6608 IGraphics::CLineItem LineItem(TangentX, TangentY, PosX, PosY);
6609 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
6610 }
6611 }
6612 }
6613 Graphics()->LinesEnd();
6614 Ui()->ClipDisable();
6615 }
6616
6617 // render lines
6618 {
6619 float EndTimeTotal = maximum(a: 0.000001f, b: pEnvelope->EndTime());
6620 float EndX = clamp(val: EnvelopeToScreenX(View, x: EndTimeTotal), lo: View.x, hi: View.x + View.w);
6621 float StartX = clamp(val: View.x + View.w * m_OffsetEnvelopeX, lo: View.x, hi: View.x + View.w);
6622
6623 float EndTime = ScreenToEnvelopeX(View, x: EndX);
6624 float StartTime = ScreenToEnvelopeX(View, x: StartX);
6625
6626 Ui()->ClipEnable(pRect: &View);
6627 Graphics()->TextureClear();
6628 Graphics()->LinesBegin();
6629 for(int c = 0; c < pEnvelope->GetChannels(); c++)
6630 {
6631 if(s_ActiveChannels & (1 << c))
6632 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1);
6633 else
6634 Graphics()->SetColor(r: aColors[c].r * 0.5f, g: aColors[c].g * 0.5f, b: aColors[c].b * 0.5f, a: 1);
6635
6636 int Steps = static_cast<int>(((EndX - StartX) / Ui()->Screen()->w) * Graphics()->ScreenWidth());
6637 float StepTime = (EndTime - StartTime) / static_cast<float>(Steps);
6638 float StepSize = (EndX - StartX) / static_cast<float>(Steps);
6639
6640 ColorRGBA Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
6641 pEnvelope->Eval(Time: StartTime, Result&: Channels, Channels: c + 1);
6642 float PrevY = EnvelopeToScreenY(View, y: Channels[c]);
6643 for(int i = 1; i < Steps; i++)
6644 {
6645 Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
6646 pEnvelope->Eval(Time: StartTime + i * StepTime, Result&: Channels, Channels: c + 1);
6647 float CurrentY = EnvelopeToScreenY(View, y: Channels[c]);
6648
6649 IGraphics::CLineItem LineItem(
6650 StartX + (i - 1) * StepSize,
6651 PrevY,
6652 StartX + i * StepSize,
6653 CurrentY);
6654 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
6655
6656 PrevY = CurrentY;
6657 }
6658 }
6659 Graphics()->LinesEnd();
6660 Ui()->ClipDisable();
6661 }
6662
6663 // render curve options
6664 {
6665 for(int i = 0; i < (int)pEnvelope->m_vPoints.size() - 1; i++)
6666 {
6667 float t0 = fxt2f(t: pEnvelope->m_vPoints[i].m_Time);
6668 float t1 = fxt2f(t: pEnvelope->m_vPoints[i + 1].m_Time);
6669
6670 CUIRect CurveButton;
6671 CurveButton.x = EnvelopeToScreenX(View, x: t0 + (t1 - t0) * 0.5f);
6672 CurveButton.y = CurveBar.y;
6673 CurveButton.h = CurveBar.h;
6674 CurveButton.w = CurveBar.h;
6675 CurveButton.x -= CurveButton.w / 2.0f;
6676 const void *pId = &pEnvelope->m_vPoints[i].m_Curvetype;
6677 const char *apTypeName[] = {"N", "L", "S", "F", "M", "B"};
6678 const char *pTypeName = "!?";
6679 if(0 <= pEnvelope->m_vPoints[i].m_Curvetype && pEnvelope->m_vPoints[i].m_Curvetype < (int)std::size(apTypeName))
6680 pTypeName = apTypeName[pEnvelope->m_vPoints[i].m_Curvetype];
6681
6682 if(CurveButton.x >= View.x)
6683 {
6684 if(DoButton_Editor(pId, pText: pTypeName, Checked: 0, pRect: &CurveButton, Flags: 0, pToolTip: "Switch curve type (N = step, L = linear, S = slow, F = fast, M = smooth, B = bezier)"))
6685 {
6686 int PrevCurve = pEnvelope->m_vPoints[i].m_Curvetype;
6687 pEnvelope->m_vPoints[i].m_Curvetype = (pEnvelope->m_vPoints[i].m_Curvetype + 1) % NUM_CURVETYPES;
6688
6689 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEditPoint>(args: this,
6690 args&: m_SelectedEnvelope, args&: i, args: 0, args: CEditorActionEnvelopeEditPoint::EEditType::CURVE_TYPE, args&: PrevCurve, args&: pEnvelope->m_vPoints[i].m_Curvetype));
6691 m_Map.OnModify();
6692 }
6693 }
6694 }
6695 }
6696
6697 // render colorbar
6698 if(ShowColorBar)
6699 {
6700 Ui()->ClipEnable(pRect: &ColorBar);
6701
6702 float StartX = maximum(a: EnvelopeToScreenX(View, x: 0), b: ColorBar.x);
6703 float EndX = EnvelopeToScreenX(View, x: pEnvelope->EndTime());
6704 CUIRect BackgroundView{
6705 .x: StartX,
6706 .y: ColorBar.y,
6707 .w: minimum(a: EndX - StartX, b: ColorBar.x + ColorBar.w - StartX),
6708 .h: ColorBar.h};
6709 RenderBackground(View: BackgroundView, Texture: m_CheckerTexture, Size: 16.0f, Brightness: 1.0f);
6710
6711 Graphics()->TextureClear();
6712 Graphics()->QuadsBegin();
6713 for(int i = 0; i < (int)pEnvelope->m_vPoints.size() - 1; i++)
6714 {
6715 float r0 = fx2f(v: pEnvelope->m_vPoints[i].m_aValues[0]);
6716 float g0 = fx2f(v: pEnvelope->m_vPoints[i].m_aValues[1]);
6717 float b0 = fx2f(v: pEnvelope->m_vPoints[i].m_aValues[2]);
6718 float a0 = fx2f(v: pEnvelope->m_vPoints[i].m_aValues[3]);
6719 float r1 = fx2f(v: pEnvelope->m_vPoints[i + 1].m_aValues[0]);
6720 float g1 = fx2f(v: pEnvelope->m_vPoints[i + 1].m_aValues[1]);
6721 float b1 = fx2f(v: pEnvelope->m_vPoints[i + 1].m_aValues[2]);
6722 float a1 = fx2f(v: pEnvelope->m_vPoints[i + 1].m_aValues[3]);
6723
6724 IGraphics::CColorVertex Array[4] = {IGraphics::CColorVertex(0, r0, g0, b0, a0),
6725 IGraphics::CColorVertex(1, r1, g1, b1, a1),
6726 IGraphics::CColorVertex(2, r1, g1, b1, a1),
6727 IGraphics::CColorVertex(3, r0, g0, b0, a0)};
6728 Graphics()->SetColorVertex(pArray: Array, Num: 4);
6729
6730 float x0 = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time));
6731 float x1 = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i + 1].m_Time));
6732
6733 IGraphics::CQuadItem QuadItem(x0, ColorBar.y, x1 - x0, ColorBar.h);
6734 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
6735 }
6736 Graphics()->QuadsEnd();
6737 Ui()->ClipDisable();
6738 }
6739
6740 // render handles
6741 if(CurrentEnvelopeSwitched)
6742 {
6743 DeselectEnvPoints();
6744 m_ResetZoomEnvelope = true;
6745 }
6746
6747 {
6748 static SPopupMenuId s_PopupEnvPointId;
6749 const auto &&ShowPopupEnvPoint = [&]() {
6750 Ui()->DoPopupMenu(pId: &s_PopupEnvPointId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 150, Height: 56 + (pEnvelope->GetChannels() == 4 && !IsTangentSelected() ? 16.0f : 0.0f), pContext: this, pfnFunc: PopupEnvPoint);
6751 };
6752
6753 if(s_Operation == EEnvelopeEditorOp::OP_NONE)
6754 {
6755 SetHotEnvelopePoint(View, pEnvelope, ActiveChannels: s_ActiveChannels);
6756 if(!Ui()->MouseButton(Index: 0))
6757 m_EnvOpTracker.Stop(Switch: false);
6758 }
6759 else
6760 {
6761 m_EnvOpTracker.Begin(Operation: s_Operation);
6762 }
6763
6764 Ui()->ClipEnable(pRect: &View);
6765 Graphics()->TextureClear();
6766 Graphics()->QuadsBegin();
6767 for(int c = 0; c < pEnvelope->GetChannels(); c++)
6768 {
6769 if(!(s_ActiveChannels & (1 << c)))
6770 continue;
6771
6772 for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++)
6773 {
6774 // point handle
6775 {
6776 CUIRect Final;
6777 Final.x = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time));
6778 Final.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]));
6779 Final.x -= 2.0f;
6780 Final.y -= 2.0f;
6781 Final.w = 4.0f;
6782 Final.h = 4.0f;
6783
6784 const void *pId = &pEnvelope->m_vPoints[i].m_aValues[c];
6785
6786 if(IsEnvPointSelected(Index: i, Channel: c))
6787 {
6788 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
6789 CUIRect Background = {
6790 .x: Final.x - 0.2f * Final.w,
6791 .y: Final.y - 0.2f * Final.h,
6792 .w: Final.w * 1.4f,
6793 .h: Final.h * 1.4f};
6794 IGraphics::CQuadItem QuadItem(Background.x, Background.y, Background.w, Background.h);
6795 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
6796 }
6797
6798 if(Ui()->CheckActiveItem(pId))
6799 {
6800 m_ShowEnvelopePreview = SHOWENV_SELECTED;
6801
6802 if(s_Operation == EEnvelopeEditorOp::OP_SELECT)
6803 {
6804 float dx = s_MouseXStart - Ui()->MouseX();
6805 float dy = s_MouseYStart - Ui()->MouseY();
6806
6807 if(dx * dx + dy * dy > 20.0f)
6808 {
6809 s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT;
6810
6811 if(!IsEnvPointSelected(Index: i, Channel: c))
6812 SelectEnvPoint(Index: i, Channel: c);
6813 }
6814 }
6815
6816 if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_X || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_Y)
6817 {
6818 if(Input()->ShiftIsPressed())
6819 {
6820 if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_Y)
6821 {
6822 s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT_X;
6823 s_vAccurateDragValuesX.clear();
6824 for(auto [SelectedIndex, _] : m_vSelectedEnvelopePoints)
6825 s_vAccurateDragValuesX.push_back(x: pEnvelope->m_vPoints[SelectedIndex].m_Time);
6826 }
6827 else
6828 {
6829 float DeltaX = ScreenToEnvelopeDX(View, dx: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f);
6830
6831 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
6832 {
6833 int SelectedIndex = m_vSelectedEnvelopePoints[k].first;
6834 int BoundLow = f2fxt(t: ScreenToEnvelopeX(View, x: View.x));
6835 int BoundHigh = f2fxt(t: ScreenToEnvelopeX(View, x: View.x + View.w));
6836 for(int j = 0; j < SelectedIndex; j++)
6837 {
6838 if(!IsEnvPointSelected(Index: j))
6839 BoundLow = maximum(a: pEnvelope->m_vPoints[j].m_Time + 1, b: BoundLow);
6840 }
6841 for(int j = SelectedIndex + 1; j < (int)pEnvelope->m_vPoints.size(); j++)
6842 {
6843 if(!IsEnvPointSelected(Index: j))
6844 BoundHigh = minimum(a: pEnvelope->m_vPoints[j].m_Time - 1, b: BoundHigh);
6845 }
6846
6847 DeltaX = ClampDelta(Val: s_vAccurateDragValuesX[k], Delta: DeltaX, Min: BoundLow, Max: BoundHigh);
6848 }
6849 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
6850 {
6851 int SelectedIndex = m_vSelectedEnvelopePoints[k].first;
6852 s_vAccurateDragValuesX[k] += DeltaX;
6853 pEnvelope->m_vPoints[SelectedIndex].m_Time = std::round(x: s_vAccurateDragValuesX[k]);
6854 }
6855 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
6856 {
6857 int SelectedIndex = m_vSelectedEnvelopePoints[k].first;
6858 if(SelectedIndex == 0 && pEnvelope->m_vPoints[SelectedIndex].m_Time != 0)
6859 {
6860 RemoveTimeOffsetEnvelope(pEnvelope);
6861 float Offset = s_vAccurateDragValuesX[k];
6862 for(auto &Value : s_vAccurateDragValuesX)
6863 Value -= Offset;
6864 break;
6865 }
6866 }
6867 }
6868 }
6869 else
6870 {
6871 if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_X)
6872 {
6873 s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT_Y;
6874 s_vAccurateDragValuesY.clear();
6875 for(auto [SelectedIndex, SelectedChannel] : m_vSelectedEnvelopePoints)
6876 s_vAccurateDragValuesY.push_back(x: pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]);
6877 }
6878 else
6879 {
6880 float DeltaY = ScreenToEnvelopeDY(View, dy: Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f);
6881 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
6882 {
6883 auto [SelectedIndex, SelectedChannel] = m_vSelectedEnvelopePoints[k];
6884 s_vAccurateDragValuesY[k] -= DeltaY;
6885 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: s_vAccurateDragValuesY[k]);
6886
6887 if(pEnvelope->GetChannels() == 1 || pEnvelope->GetChannels() == 4)
6888 {
6889 pEnvelope->m_vPoints[i].m_aValues[c] = clamp(val: pEnvelope->m_vPoints[i].m_aValues[c], lo: 0, hi: 1024);
6890 s_vAccurateDragValuesY[k] = clamp<float>(val: s_vAccurateDragValuesY[k], lo: 0, hi: 1024);
6891 }
6892 }
6893 }
6894 }
6895 }
6896
6897 if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU)
6898 {
6899 if(!Ui()->MouseButton(Index: 1))
6900 {
6901 if(m_vSelectedEnvelopePoints.size() == 1)
6902 {
6903 m_UpdateEnvPointInfo = true;
6904 ShowPopupEnvPoint();
6905 }
6906 else if(m_vSelectedEnvelopePoints.size() > 1)
6907 {
6908 static SPopupMenuId s_PopupEnvPointMultiId;
6909 Ui()->DoPopupMenu(pId: &s_PopupEnvPointMultiId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 80, Height: 22, pContext: this, pfnFunc: PopupEnvPointMulti);
6910 }
6911 Ui()->SetActiveItem(nullptr);
6912 s_Operation = EEnvelopeEditorOp::OP_NONE;
6913 }
6914 }
6915 else if(!Ui()->MouseButton(Index: 0))
6916 {
6917 Ui()->SetActiveItem(nullptr);
6918 m_SelectedQuadEnvelope = -1;
6919
6920 if(s_Operation == EEnvelopeEditorOp::OP_SELECT)
6921 {
6922 if(Input()->ShiftIsPressed())
6923 ToggleEnvPoint(Index: i, Channel: c);
6924 else
6925 SelectEnvPoint(Index: i, Channel: c);
6926 }
6927
6928 s_Operation = EEnvelopeEditorOp::OP_NONE;
6929 m_Map.OnModify();
6930 }
6931
6932 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
6933 }
6934 else if(Ui()->HotItem() == pId)
6935 {
6936 if(Ui()->MouseButton(Index: 0))
6937 {
6938 Ui()->SetActiveItem(pId);
6939 s_Operation = EEnvelopeEditorOp::OP_SELECT;
6940 m_SelectedQuadEnvelope = m_SelectedEnvelope;
6941
6942 s_MouseXStart = Ui()->MouseX();
6943 s_MouseYStart = Ui()->MouseY();
6944 }
6945 else if(Ui()->MouseButtonClicked(Index: 1))
6946 {
6947 if(Input()->ShiftIsPressed())
6948 {
6949 m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionDeleteEnvelopePoint>(args: this, args&: m_SelectedEnvelope, args&: i));
6950 }
6951 else
6952 {
6953 s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU;
6954 if(!IsEnvPointSelected(Index: i, Channel: c))
6955 SelectEnvPoint(Index: i, Channel: c);
6956 Ui()->SetActiveItem(pId);
6957 }
6958 }
6959
6960 m_ShowEnvelopePreview = SHOWENV_SELECTED;
6961 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
6962 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.");
6963 ms_pUiGotContext = pId;
6964 }
6965 else
6966 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1.0f);
6967
6968 IGraphics::CQuadItem QuadItem(Final.x, Final.y, Final.w, Final.h);
6969 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
6970 }
6971
6972 // tangent handles for bezier curves
6973 if(i >= 0 && i < (int)pEnvelope->m_vPoints.size())
6974 {
6975 // Out-Tangent handle
6976 if(pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER)
6977 {
6978 CUIRect Final;
6979 Final.x = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]));
6980 Final.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]));
6981 Final.x -= 2.0f;
6982 Final.y -= 2.0f;
6983 Final.w = 4.0f;
6984 Final.h = 4.0f;
6985
6986 // handle logic
6987 const void *pId = &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c];
6988
6989 if(IsTangentOutPointSelected(Index: i, Channel: c))
6990 {
6991 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
6992 IGraphics::CFreeformItem FreeformItem(
6993 Final.x + Final.w / 2.0f,
6994 Final.y - 1,
6995 Final.x + Final.w / 2.0f,
6996 Final.y - 1,
6997 Final.x + Final.w + 1,
6998 Final.y + Final.h + 1,
6999 Final.x - 1,
7000 Final.y + Final.h + 1);
7001 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
7002 }
7003
7004 if(Ui()->CheckActiveItem(pId))
7005 {
7006 m_ShowEnvelopePreview = SHOWENV_SELECTED;
7007
7008 if(s_Operation == EEnvelopeEditorOp::OP_SELECT)
7009 {
7010 float dx = s_MouseXStart - Ui()->MouseX();
7011 float dy = s_MouseYStart - Ui()->MouseY();
7012
7013 if(dx * dx + dy * dy > 20.0f)
7014 {
7015 s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT;
7016
7017 s_vAccurateDragValuesX = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c])};
7018 s_vAccurateDragValuesY = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c])};
7019
7020 if(!IsTangentOutPointSelected(Index: i, Channel: c))
7021 SelectTangentOutPoint(Index: i, Channel: c);
7022 }
7023 }
7024
7025 if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT)
7026 {
7027 float DeltaX = ScreenToEnvelopeDX(View, dx: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f);
7028 float DeltaY = ScreenToEnvelopeDY(View, dy: Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f);
7029 s_vAccurateDragValuesX[0] += DeltaX;
7030 s_vAccurateDragValuesY[0] -= DeltaY;
7031
7032 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = std::round(x: s_vAccurateDragValuesX[0]);
7033 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c] = std::round(x: s_vAccurateDragValuesY[0]);
7034
7035 // clamp time value
7036 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = clamp<int>(val: pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c], lo: 0, hi: f2fxt(t: ScreenToEnvelopeX(View, x: View.x + View.w)) - pEnvelope->m_vPoints[i].m_Time);
7037 s_vAccurateDragValuesX[0] = clamp<float>(val: s_vAccurateDragValuesX[0], lo: 0, hi: f2fxt(t: ScreenToEnvelopeX(View, x: View.x + View.w)) - pEnvelope->m_vPoints[i].m_Time);
7038 }
7039
7040 if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU)
7041 {
7042 if(!Ui()->MouseButton(Index: 1))
7043 {
7044 if(IsTangentOutPointSelected(Index: i, Channel: c))
7045 {
7046 m_UpdateEnvPointInfo = true;
7047 ShowPopupEnvPoint();
7048 }
7049 Ui()->SetActiveItem(nullptr);
7050 s_Operation = EEnvelopeEditorOp::OP_NONE;
7051 }
7052 }
7053 else if(!Ui()->MouseButton(Index: 0))
7054 {
7055 Ui()->SetActiveItem(nullptr);
7056 m_SelectedQuadEnvelope = -1;
7057
7058 if(s_Operation == EEnvelopeEditorOp::OP_SELECT)
7059 SelectTangentOutPoint(Index: i, Channel: c);
7060
7061 s_Operation = EEnvelopeEditorOp::OP_NONE;
7062 m_Map.OnModify();
7063 }
7064
7065 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
7066 }
7067 else if(Ui()->HotItem() == pId)
7068 {
7069 if(Ui()->MouseButton(Index: 0))
7070 {
7071 Ui()->SetActiveItem(pId);
7072 s_Operation = EEnvelopeEditorOp::OP_SELECT;
7073 m_SelectedQuadEnvelope = m_SelectedEnvelope;
7074
7075 s_MouseXStart = Ui()->MouseX();
7076 s_MouseYStart = Ui()->MouseY();
7077 }
7078 else if(Ui()->MouseButtonClicked(Index: 1))
7079 {
7080 if(Input()->ShiftIsPressed())
7081 {
7082 SelectTangentOutPoint(Index: i, Channel: c);
7083 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = 0.0f;
7084 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c] = 0.0f;
7085 m_Map.OnModify();
7086 }
7087 else
7088 {
7089 s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU;
7090 SelectTangentOutPoint(Index: i, Channel: c);
7091 Ui()->SetActiveItem(pId);
7092 }
7093 }
7094
7095 m_ShowEnvelopePreview = SHOWENV_SELECTED;
7096 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
7097 str_copy(dst&: m_aTooltip, src: "Bezier out-tangent. Left mouse to drag. Hold ctrl to be more precise. Shift + right-click to reset.");
7098 ms_pUiGotContext = pId;
7099 }
7100 else
7101 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1.0f);
7102
7103 // draw triangle
7104 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);
7105 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
7106 }
7107
7108 // In-Tangent handle
7109 if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER)
7110 {
7111 CUIRect Final;
7112 Final.x = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]));
7113 Final.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]));
7114 Final.x -= 2.0f;
7115 Final.y -= 2.0f;
7116 Final.w = 4.0f;
7117 Final.h = 4.0f;
7118
7119 // handle logic
7120 const void *pId = &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c];
7121
7122 if(IsTangentInPointSelected(Index: i, Channel: c))
7123 {
7124 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
7125 IGraphics::CFreeformItem FreeformItem(
7126 Final.x + Final.w / 2.0f,
7127 Final.y - 1,
7128 Final.x + Final.w / 2.0f,
7129 Final.y - 1,
7130 Final.x + Final.w + 1,
7131 Final.y + Final.h + 1,
7132 Final.x - 1,
7133 Final.y + Final.h + 1);
7134 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
7135 }
7136
7137 if(Ui()->CheckActiveItem(pId))
7138 {
7139 m_ShowEnvelopePreview = SHOWENV_SELECTED;
7140
7141 if(s_Operation == EEnvelopeEditorOp::OP_SELECT)
7142 {
7143 float dx = s_MouseXStart - Ui()->MouseX();
7144 float dy = s_MouseYStart - Ui()->MouseY();
7145
7146 if(dx * dx + dy * dy > 20.0f)
7147 {
7148 s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT;
7149
7150 s_vAccurateDragValuesX = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c])};
7151 s_vAccurateDragValuesY = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c])};
7152
7153 if(!IsTangentInPointSelected(Index: i, Channel: c))
7154 SelectTangentInPoint(Index: i, Channel: c);
7155 }
7156 }
7157
7158 if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT)
7159 {
7160 float DeltaX = ScreenToEnvelopeDX(View, dx: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f);
7161 float DeltaY = ScreenToEnvelopeDY(View, dy: Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f);
7162 s_vAccurateDragValuesX[0] += DeltaX;
7163 s_vAccurateDragValuesY[0] -= DeltaY;
7164
7165 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = std::round(x: s_vAccurateDragValuesX[0]);
7166 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c] = std::round(x: s_vAccurateDragValuesY[0]);
7167
7168 // clamp time value
7169 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = clamp(val: pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c], lo: f2fxt(t: ScreenToEnvelopeX(View, x: View.x)) - pEnvelope->m_vPoints[i].m_Time, hi: 0);
7170 s_vAccurateDragValuesX[0] = clamp<float>(val: s_vAccurateDragValuesX[0], lo: f2fxt(t: ScreenToEnvelopeX(View, x: View.x)) - pEnvelope->m_vPoints[i].m_Time, hi: 0);
7171 }
7172
7173 if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU)
7174 {
7175 if(!Ui()->MouseButton(Index: 1))
7176 {
7177 if(IsTangentInPointSelected(Index: i, Channel: c))
7178 {
7179 m_UpdateEnvPointInfo = true;
7180 ShowPopupEnvPoint();
7181 }
7182 Ui()->SetActiveItem(nullptr);
7183 s_Operation = EEnvelopeEditorOp::OP_NONE;
7184 }
7185 }
7186 else if(!Ui()->MouseButton(Index: 0))
7187 {
7188 Ui()->SetActiveItem(nullptr);
7189 m_SelectedQuadEnvelope = -1;
7190
7191 if(s_Operation == EEnvelopeEditorOp::OP_SELECT)
7192 SelectTangentInPoint(Index: i, Channel: c);
7193
7194 s_Operation = EEnvelopeEditorOp::OP_NONE;
7195 m_Map.OnModify();
7196 }
7197
7198 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
7199 }
7200 else if(Ui()->HotItem() == pId)
7201 {
7202 if(Ui()->MouseButton(Index: 0))
7203 {
7204 Ui()->SetActiveItem(pId);
7205 s_Operation = EEnvelopeEditorOp::OP_SELECT;
7206 m_SelectedQuadEnvelope = m_SelectedEnvelope;
7207
7208 s_MouseXStart = Ui()->MouseX();
7209 s_MouseYStart = Ui()->MouseY();
7210 }
7211 else if(Ui()->MouseButtonClicked(Index: 1))
7212 {
7213 if(Input()->ShiftIsPressed())
7214 {
7215 SelectTangentInPoint(Index: i, Channel: c);
7216 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = 0.0f;
7217 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c] = 0.0f;
7218 m_Map.OnModify();
7219 }
7220 else
7221 {
7222 s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU;
7223 SelectTangentInPoint(Index: i, Channel: c);
7224 Ui()->SetActiveItem(pId);
7225 }
7226 }
7227
7228 m_ShowEnvelopePreview = SHOWENV_SELECTED;
7229 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
7230 str_copy(dst&: m_aTooltip, src: "Bezier in-tangent. Left mouse to drag. Hold ctrl to be more precise. Shift + right-click to reset.");
7231 ms_pUiGotContext = pId;
7232 }
7233 else
7234 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1.0f);
7235
7236 // draw triangle
7237 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);
7238 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
7239 }
7240 }
7241 }
7242 }
7243 Graphics()->QuadsEnd();
7244 Ui()->ClipDisable();
7245 }
7246
7247 // handle scaling
7248 static float s_ScaleFactorX = 1.0f;
7249 static float s_ScaleFactorY = 1.0f;
7250 static float s_MidpointX = 0.0f;
7251 static float s_MidpointY = 0.0f;
7252 static std::vector<float> s_vInitialPositionsX;
7253 static std::vector<float> s_vInitialPositionsY;
7254 if(s_Operation == EEnvelopeEditorOp::OP_NONE && !s_NameInput.IsActive() && Input()->KeyIsPressed(Key: KEY_S) && !Input()->ModifierIsPressed() && !m_vSelectedEnvelopePoints.empty())
7255 {
7256 s_Operation = EEnvelopeEditorOp::OP_SCALE;
7257 s_ScaleFactorX = 1.0f;
7258 s_ScaleFactorY = 1.0f;
7259 auto [FirstPointIndex, FirstPointChannel] = m_vSelectedEnvelopePoints.front();
7260
7261 float MaximumX = pEnvelope->m_vPoints[FirstPointIndex].m_Time;
7262 float MinimumX = MaximumX;
7263 s_vInitialPositionsX.clear();
7264 for(auto [SelectedIndex, _] : m_vSelectedEnvelopePoints)
7265 {
7266 float Value = pEnvelope->m_vPoints[SelectedIndex].m_Time;
7267 s_vInitialPositionsX.push_back(x: Value);
7268 MaximumX = maximum(a: MaximumX, b: Value);
7269 MinimumX = minimum(a: MinimumX, b: Value);
7270 }
7271 s_MidpointX = (MaximumX - MinimumX) / 2.0f + MinimumX;
7272
7273 float MaximumY = pEnvelope->m_vPoints[FirstPointIndex].m_aValues[FirstPointChannel];
7274 float MinimumY = MaximumY;
7275 s_vInitialPositionsY.clear();
7276 for(auto [SelectedIndex, SelectedChannel] : m_vSelectedEnvelopePoints)
7277 {
7278 float Value = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel];
7279 s_vInitialPositionsY.push_back(x: Value);
7280 MaximumY = maximum(a: MaximumY, b: Value);
7281 MinimumY = minimum(a: MinimumY, b: Value);
7282 }
7283 s_MidpointY = (MaximumY - MinimumY) / 2.0f + MinimumY;
7284 }
7285
7286 if(s_Operation == EEnvelopeEditorOp::OP_SCALE)
7287 {
7288 str_copy(dst&: m_aTooltip, src: "Press shift to scale the time. Press alt to scale along midpoint. Press ctrl to be more precise.");
7289
7290 if(Input()->ShiftIsPressed())
7291 {
7292 s_ScaleFactorX += Ui()->MouseDeltaX() / Graphics()->ScreenWidth() * (Input()->ModifierIsPressed() ? 0.5f : 10.0f);
7293 float Midpoint = Input()->AltIsPressed() ? s_MidpointX : 0.0f;
7294 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
7295 {
7296 int SelectedIndex = m_vSelectedEnvelopePoints[k].first;
7297 int BoundLow = f2fxt(t: ScreenToEnvelopeX(View, x: View.x));
7298 int BoundHigh = f2fxt(t: ScreenToEnvelopeX(View, x: View.x + View.w));
7299 for(int j = 0; j < SelectedIndex; j++)
7300 {
7301 if(!IsEnvPointSelected(Index: j))
7302 BoundLow = maximum(a: pEnvelope->m_vPoints[j].m_Time + 1, b: BoundLow);
7303 }
7304 for(int j = SelectedIndex + 1; j < (int)pEnvelope->m_vPoints.size(); j++)
7305 {
7306 if(!IsEnvPointSelected(Index: j))
7307 BoundHigh = minimum(a: pEnvelope->m_vPoints[j].m_Time - 1, b: BoundHigh);
7308 }
7309
7310 float Value = s_vInitialPositionsX[k];
7311 float ScaleBoundLow = (BoundLow - Midpoint) / (Value - Midpoint);
7312 float ScaleBoundHigh = (BoundHigh - Midpoint) / (Value - Midpoint);
7313 float ScaleBoundMin = minimum(a: ScaleBoundLow, b: ScaleBoundHigh);
7314 float ScaleBoundMax = maximum(a: ScaleBoundLow, b: ScaleBoundHigh);
7315 s_ScaleFactorX = clamp(val: s_ScaleFactorX, lo: ScaleBoundMin, hi: ScaleBoundMax);
7316 }
7317
7318 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
7319 {
7320 int SelectedIndex = m_vSelectedEnvelopePoints[k].first;
7321 float ScaleMinimum = s_vInitialPositionsX[k] - Midpoint > fxt2f(t: 1) ? fxt2f(t: 1) / (s_vInitialPositionsX[k] - Midpoint) : 0.0f;
7322 float ScaleFactor = maximum(a: ScaleMinimum, b: s_ScaleFactorX);
7323 pEnvelope->m_vPoints[SelectedIndex].m_Time = std::round(x: (s_vInitialPositionsX[k] - Midpoint) * ScaleFactor + Midpoint);
7324 }
7325 for(size_t k = 1; k < pEnvelope->m_vPoints.size(); k++)
7326 {
7327 if(pEnvelope->m_vPoints[k].m_Time <= pEnvelope->m_vPoints[k - 1].m_Time)
7328 pEnvelope->m_vPoints[k].m_Time = pEnvelope->m_vPoints[k - 1].m_Time + 1;
7329 }
7330 for(auto [SelectedIndex, _] : m_vSelectedEnvelopePoints)
7331 {
7332 if(SelectedIndex == 0 && pEnvelope->m_vPoints[SelectedIndex].m_Time != 0)
7333 {
7334 float Offset = pEnvelope->m_vPoints[0].m_Time;
7335 RemoveTimeOffsetEnvelope(pEnvelope);
7336 s_MidpointX -= Offset;
7337 for(auto &Value : s_vInitialPositionsX)
7338 Value -= Offset;
7339 break;
7340 }
7341 }
7342 }
7343 else
7344 {
7345 s_ScaleFactorY -= Ui()->MouseDeltaY() / Graphics()->ScreenHeight() * (Input()->ModifierIsPressed() ? 0.5f : 10.0f);
7346 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
7347 {
7348 auto [SelectedIndex, SelectedChannel] = m_vSelectedEnvelopePoints[k];
7349 if(Input()->AltIsPressed())
7350 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: (s_vInitialPositionsY[k] - s_MidpointY) * s_ScaleFactorY + s_MidpointY);
7351 else
7352 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: s_vInitialPositionsY[k] * s_ScaleFactorY);
7353
7354 if(pEnvelope->GetChannels() == 1 || pEnvelope->GetChannels() == 4)
7355 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = clamp(val: pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel], lo: 0, hi: 1024);
7356 }
7357 }
7358
7359 if(Ui()->MouseButton(Index: 0))
7360 {
7361 s_Operation = EEnvelopeEditorOp::OP_NONE;
7362 m_EnvOpTracker.Stop(Switch: false);
7363 }
7364 else if(Ui()->MouseButton(Index: 1) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
7365 {
7366 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
7367 {
7368 int SelectedIndex = m_vSelectedEnvelopePoints[k].first;
7369 pEnvelope->m_vPoints[SelectedIndex].m_Time = std::round(x: s_vInitialPositionsX[k]);
7370 }
7371 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
7372 {
7373 auto [SelectedIndex, SelectedChannel] = m_vSelectedEnvelopePoints[k];
7374 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: s_vInitialPositionsY[k]);
7375 }
7376 RemoveTimeOffsetEnvelope(pEnvelope);
7377 s_Operation = EEnvelopeEditorOp::OP_NONE;
7378 }
7379 }
7380
7381 // handle box selection
7382 if(s_Operation == EEnvelopeEditorOp::OP_BOX_SELECT)
7383 {
7384 IGraphics::CLineItem aLines[4] = {
7385 {s_MouseXStart, s_MouseYStart, Ui()->MouseX(), s_MouseYStart},
7386 {s_MouseXStart, s_MouseYStart, s_MouseXStart, Ui()->MouseY()},
7387 {s_MouseXStart, Ui()->MouseY(), Ui()->MouseX(), Ui()->MouseY()},
7388 {Ui()->MouseX(), s_MouseYStart, Ui()->MouseX(), Ui()->MouseY()}};
7389 Ui()->ClipEnable(pRect: &View);
7390 Graphics()->LinesBegin();
7391 Graphics()->LinesDraw(pArray: aLines, Num: std::size(aLines));
7392 Graphics()->LinesEnd();
7393 Ui()->ClipDisable();
7394
7395 if(!Ui()->MouseButton(Index: 0))
7396 {
7397 s_Operation = EEnvelopeEditorOp::OP_NONE;
7398 Ui()->SetActiveItem(nullptr);
7399
7400 float TimeStart = ScreenToEnvelopeX(View, x: s_MouseXStart);
7401 float TimeEnd = ScreenToEnvelopeX(View, x: Ui()->MouseX());
7402 float ValueStart = ScreenToEnvelopeY(View, y: s_MouseYStart);
7403 float ValueEnd = ScreenToEnvelopeY(View, y: Ui()->MouseY());
7404
7405 float TimeMin = minimum(a: TimeStart, b: TimeEnd);
7406 float TimeMax = maximum(a: TimeStart, b: TimeEnd);
7407 float ValueMin = minimum(a: ValueStart, b: ValueEnd);
7408 float ValueMax = maximum(a: ValueStart, b: ValueEnd);
7409
7410 if(!Input()->ShiftIsPressed())
7411 DeselectEnvPoints();
7412
7413 for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++)
7414 {
7415 for(int c = 0; c < CEnvPoint::MAX_CHANNELS; c++)
7416 {
7417 if(!(s_ActiveChannels & (1 << c)))
7418 continue;
7419
7420 float Time = fxt2f(t: pEnvelope->m_vPoints[i].m_Time);
7421 float Value = fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]);
7422
7423 if(in_range(a: Time, lower: TimeMin, upper: TimeMax) && in_range(a: Value, lower: ValueMin, upper: ValueMax))
7424 ToggleEnvPoint(Index: i, Channel: c);
7425 }
7426 }
7427 }
7428 }
7429 }
7430}
7431
7432void CEditor::RenderEditorHistory(CUIRect View)
7433{
7434 enum EHistoryType
7435 {
7436 EDITOR_HISTORY,
7437 ENVELOPE_HISTORY,
7438 SERVER_SETTINGS_HISTORY
7439 };
7440
7441 static EHistoryType s_HistoryType = EDITOR_HISTORY;
7442 static int s_ActionSelectedIndex = 0;
7443 static CListBox s_ListBox;
7444 s_ListBox.SetActive(m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen());
7445
7446 const bool GotSelection = s_ListBox.Active() && s_ActionSelectedIndex >= 0 && (size_t)s_ActionSelectedIndex < m_Map.m_vSettings.size();
7447
7448 CUIRect ToolBar, Button, Label, List, DragBar;
7449 View.HSplitTop(Cut: 22.0f, pTop: &DragBar, pBottom: nullptr);
7450 DragBar.y -= 2.0f;
7451 DragBar.w += 2.0f;
7452 DragBar.h += 4.0f;
7453 DoEditorDragBar(View, pDragBar: &DragBar, Side: EDragSide::SIDE_TOP, pValue: &m_aExtraEditorSplits[EXTRAEDITOR_HISTORY]);
7454 View.HSplitTop(Cut: 20.0f, pTop: &ToolBar, pBottom: &View);
7455 View.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &List);
7456 ToolBar.HMargin(Cut: 2.0f, pOtherRect: &ToolBar);
7457
7458 CUIRect TypeButtons, HistoryTypeButton;
7459 const int HistoryTypeBtnSize = 70.0f;
7460 ToolBar.VSplitLeft(Cut: 3 * HistoryTypeBtnSize, pLeft: &TypeButtons, pRight: &Label);
7461
7462 // history type buttons
7463 {
7464 TypeButtons.VSplitLeft(Cut: HistoryTypeBtnSize, pLeft: &HistoryTypeButton, pRight: &TypeButtons);
7465 static int s_EditorHistoryButton = 0;
7466 if(DoButton_Ex(pId: &s_EditorHistoryButton, pText: "Editor", Checked: s_HistoryType == EDITOR_HISTORY, pRect: &HistoryTypeButton, Flags: 0, pToolTip: "Show map editor history.", Corners: IGraphics::CORNER_L))
7467 {
7468 s_HistoryType = EDITOR_HISTORY;
7469 }
7470
7471 TypeButtons.VSplitLeft(Cut: HistoryTypeBtnSize, pLeft: &HistoryTypeButton, pRight: &TypeButtons);
7472 static int s_EnvelopeEditorHistoryButton = 0;
7473 if(DoButton_Ex(pId: &s_EnvelopeEditorHistoryButton, pText: "Envelope", Checked: s_HistoryType == ENVELOPE_HISTORY, pRect: &HistoryTypeButton, Flags: 0, pToolTip: "Show envelope editor history.", Corners: IGraphics::CORNER_NONE))
7474 {
7475 s_HistoryType = ENVELOPE_HISTORY;
7476 }
7477
7478 TypeButtons.VSplitLeft(Cut: HistoryTypeBtnSize, pLeft: &HistoryTypeButton, pRight: &TypeButtons);
7479 static int s_ServerSettingsHistoryButton = 0;
7480 if(DoButton_Ex(pId: &s_ServerSettingsHistoryButton, pText: "Settings", Checked: s_HistoryType == SERVER_SETTINGS_HISTORY, pRect: &HistoryTypeButton, Flags: 0, pToolTip: "Show server settings editor history.", Corners: IGraphics::CORNER_R))
7481 {
7482 s_HistoryType = SERVER_SETTINGS_HISTORY;
7483 }
7484 }
7485
7486 SLabelProperties InfoProps;
7487 InfoProps.m_MaxWidth = ToolBar.w - 60.f;
7488 InfoProps.m_EllipsisAtEnd = true;
7489 Label.VSplitLeft(Cut: 8.0f, pLeft: nullptr, pRight: &Label);
7490 Ui()->DoLabel(pRect: &Label, pText: "Editor history. Click on an action to undo all actions above.", Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: InfoProps);
7491
7492 CEditorHistory *pCurrentHistory;
7493 if(s_HistoryType == EDITOR_HISTORY)
7494 pCurrentHistory = &m_EditorHistory;
7495 else if(s_HistoryType == ENVELOPE_HISTORY)
7496 pCurrentHistory = &m_EnvelopeEditorHistory;
7497 else if(s_HistoryType == SERVER_SETTINGS_HISTORY)
7498 pCurrentHistory = &m_ServerSettingsHistory;
7499 else
7500 return;
7501
7502 // delete button
7503 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
7504 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
7505 static int s_DeleteButton = 0;
7506 if(DoButton_FontIcon(pId: &s_DeleteButton, pText: FONT_ICON_TRASH, Checked: (!pCurrentHistory->m_vpUndoActions.empty() || !pCurrentHistory->m_vpRedoActions.empty()) ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: "Clear the history.", Corners: IGraphics::CORNER_ALL, FontSize: 9.0f) == 1 || (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DELETE)))
7507 {
7508 pCurrentHistory->Clear();
7509 s_ActionSelectedIndex = 0;
7510 }
7511
7512 // actions list
7513 int RedoSize = (int)pCurrentHistory->m_vpRedoActions.size();
7514 int UndoSize = (int)pCurrentHistory->m_vpUndoActions.size();
7515 s_ActionSelectedIndex = RedoSize;
7516 s_ListBox.DoStart(RowHeight: 15.0f, NumItems: RedoSize + UndoSize, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: s_ActionSelectedIndex, pRect: &List);
7517
7518 for(int i = 0; i < RedoSize; i++)
7519 {
7520 const CListboxItem Item = s_ListBox.DoNextItem(pId: &pCurrentHistory->m_vpRedoActions[i], Selected: s_ActionSelectedIndex >= 0 && s_ActionSelectedIndex == i);
7521 if(!Item.m_Visible)
7522 continue;
7523
7524 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
7525
7526 SLabelProperties Props;
7527 Props.m_MaxWidth = Label.w;
7528 Props.m_EllipsisAtEnd = true;
7529 TextRender()->TextColor(rgb: {.5f, .5f, .5f});
7530 TextRender()->TextOutlineColor(rgb: TextRender()->DefaultTextOutlineColor());
7531 Ui()->DoLabel(pRect: &Label, pText: pCurrentHistory->m_vpRedoActions[i]->DisplayText(), Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
7532 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
7533 }
7534
7535 for(int i = 0; i < UndoSize; i++)
7536 {
7537 const CListboxItem Item = s_ListBox.DoNextItem(pId: &pCurrentHistory->m_vpUndoActions[UndoSize - i - 1], Selected: s_ActionSelectedIndex >= RedoSize && s_ActionSelectedIndex == (i + RedoSize));
7538 if(!Item.m_Visible)
7539 continue;
7540
7541 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
7542
7543 SLabelProperties Props;
7544 Props.m_MaxWidth = Label.w;
7545 Props.m_EllipsisAtEnd = true;
7546 Ui()->DoLabel(pRect: &Label, pText: pCurrentHistory->m_vpUndoActions[UndoSize - i - 1]->DisplayText(), Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
7547 }
7548
7549 { // Base action "Loaded map" that cannot be undone
7550 static int s_BaseAction;
7551 const CListboxItem Item = s_ListBox.DoNextItem(pId: &s_BaseAction, Selected: s_ActionSelectedIndex == RedoSize + UndoSize);
7552 if(Item.m_Visible)
7553 {
7554 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
7555
7556 Ui()->DoLabel(pRect: &Label, pText: "Loaded map", Size: 10.0f, Align: TEXTALIGN_ML);
7557 }
7558 }
7559
7560 const int NewSelected = s_ListBox.DoEnd();
7561 if(s_ActionSelectedIndex != NewSelected)
7562 {
7563 // Figure out if we should undo or redo some actions
7564 // Undo everything until the selected index
7565 if(NewSelected > s_ActionSelectedIndex)
7566 {
7567 for(int i = 0; i < (NewSelected - s_ActionSelectedIndex); i++)
7568 {
7569 pCurrentHistory->Undo();
7570 }
7571 }
7572 else
7573 {
7574 for(int i = 0; i < (s_ActionSelectedIndex - NewSelected); i++)
7575 {
7576 pCurrentHistory->Redo();
7577 }
7578 }
7579 s_ActionSelectedIndex = NewSelected;
7580 }
7581}
7582
7583void CEditor::DoEditorDragBar(CUIRect View, CUIRect *pDragBar, EDragSide Side, float *pValue, float MinValue, float MaxValue)
7584{
7585 enum EDragOperation
7586 {
7587 OP_NONE,
7588 OP_DRAGGING,
7589 OP_CLICKED
7590 };
7591 static EDragOperation s_Operation = OP_NONE;
7592 static float s_InitialMouseY = 0.0f;
7593 static float s_InitialMouseOffsetY = 0.0f;
7594 static float s_InitialMouseX = 0.0f;
7595 static float s_InitialMouseOffsetX = 0.0f;
7596
7597 bool IsVertical = Side == EDragSide::SIDE_TOP || Side == EDragSide::SIDE_BOTTOM;
7598
7599 if(Ui()->MouseInside(pRect: pDragBar) && Ui()->HotItem() == pDragBar)
7600 m_CursorType = IsVertical ? CURSOR_RESIZE_V : CURSOR_RESIZE_H;
7601
7602 bool Clicked;
7603 bool Abrupted;
7604 if(int Result = DoButton_DraggableEx(pId: pDragBar, pText: "", Checked: 8, pRect: pDragBar, pClicked: &Clicked, pAbrupted: &Abrupted, Flags: 0, pToolTip: "Change the size of the editor by dragging."))
7605 {
7606 if(s_Operation == OP_NONE && Result == 1)
7607 {
7608 s_InitialMouseY = Ui()->MouseY();
7609 s_InitialMouseOffsetY = Ui()->MouseY() - pDragBar->y;
7610 s_InitialMouseX = Ui()->MouseX();
7611 s_InitialMouseOffsetX = Ui()->MouseX() - pDragBar->x;
7612 s_Operation = OP_CLICKED;
7613 }
7614
7615 if(Clicked || Abrupted)
7616 s_Operation = OP_NONE;
7617
7618 if(s_Operation == OP_CLICKED && absolute(a: IsVertical ? Ui()->MouseY() - s_InitialMouseY : Ui()->MouseX() - s_InitialMouseX) > 5.0f)
7619 s_Operation = OP_DRAGGING;
7620
7621 if(s_Operation == OP_DRAGGING)
7622 {
7623 if(Side == EDragSide::SIDE_TOP)
7624 *pValue = clamp(val: s_InitialMouseOffsetY + View.y + View.h - Ui()->MouseY(), lo: MinValue, hi: MaxValue);
7625 else if(Side == EDragSide::SIDE_RIGHT)
7626 *pValue = clamp(val: Ui()->MouseX() - s_InitialMouseOffsetX - View.x + pDragBar->w, lo: MinValue, hi: MaxValue);
7627 else if(Side == EDragSide::SIDE_BOTTOM)
7628 *pValue = clamp(val: Ui()->MouseY() - s_InitialMouseOffsetY - View.y + pDragBar->h, lo: MinValue, hi: MaxValue);
7629 else if(Side == EDragSide::SIDE_LEFT)
7630 *pValue = clamp(val: s_InitialMouseOffsetX + View.x + View.w - Ui()->MouseX(), lo: MinValue, hi: MaxValue);
7631
7632 m_CursorType = IsVertical ? CURSOR_RESIZE_V : CURSOR_RESIZE_H;
7633 }
7634 }
7635}
7636
7637void CEditor::RenderMenubar(CUIRect MenuBar)
7638{
7639 SPopupMenuProperties PopupProperties;
7640 PopupProperties.m_Corners = IGraphics::CORNER_R | IGraphics::CORNER_B;
7641
7642 CUIRect FileButton;
7643 static int s_FileButton = 0;
7644 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &FileButton, pRight: &MenuBar);
7645 if(DoButton_Ex(pId: &s_FileButton, pText: "File", Checked: 0, pRect: &FileButton, Flags: 0, pToolTip: nullptr, Corners: IGraphics::CORNER_T, FontSize: EditorFontSizes::MENU, Align: TEXTALIGN_ML))
7646 {
7647 static SPopupMenuId s_PopupMenuFileId;
7648 Ui()->DoPopupMenu(pId: &s_PopupMenuFileId, X: FileButton.x, Y: FileButton.y + FileButton.h - 1.0f, Width: 120.0f, Height: 174.0f, pContext: this, pfnFunc: PopupMenuFile, Props: PopupProperties);
7649 }
7650
7651 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
7652
7653 CUIRect ToolsButton;
7654 static int s_ToolsButton = 0;
7655 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &ToolsButton, pRight: &MenuBar);
7656 if(DoButton_Ex(pId: &s_ToolsButton, pText: "Tools", Checked: 0, pRect: &ToolsButton, Flags: 0, pToolTip: nullptr, Corners: IGraphics::CORNER_T, FontSize: EditorFontSizes::MENU, Align: TEXTALIGN_ML))
7657 {
7658 static SPopupMenuId s_PopupMenuToolsId;
7659 Ui()->DoPopupMenu(pId: &s_PopupMenuToolsId, X: ToolsButton.x, Y: ToolsButton.y + ToolsButton.h - 1.0f, Width: 200.0f, Height: 64.0f, pContext: this, pfnFunc: PopupMenuTools, Props: PopupProperties);
7660 }
7661
7662 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
7663
7664 CUIRect SettingsButton;
7665 static int s_SettingsButton = 0;
7666 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &SettingsButton, pRight: &MenuBar);
7667 if(DoButton_Ex(pId: &s_SettingsButton, pText: "Settings", Checked: 0, pRect: &SettingsButton, Flags: 0, pToolTip: nullptr, Corners: IGraphics::CORNER_T, FontSize: EditorFontSizes::MENU, Align: TEXTALIGN_ML))
7668 {
7669 static SPopupMenuId s_PopupMenuEntitiesId;
7670 Ui()->DoPopupMenu(pId: &s_PopupMenuEntitiesId, X: SettingsButton.x, Y: SettingsButton.y + SettingsButton.h - 1.0f, Width: 200.0f, Height: 92.0f, pContext: this, pfnFunc: PopupMenuSettings, Props: PopupProperties);
7671 }
7672
7673 CUIRect ChangedIndicator, Info, Close;
7674 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
7675 MenuBar.VSplitLeft(Cut: MenuBar.h, pLeft: &ChangedIndicator, pRight: &MenuBar);
7676 MenuBar.VSplitRight(Cut: 20.0f, pLeft: &MenuBar, pRight: &Close);
7677 Close.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Close);
7678 MenuBar.VSplitLeft(Cut: MenuBar.w * 0.6f, pLeft: &MenuBar, pRight: &Info);
7679 MenuBar.VSplitRight(Cut: 5.0f, pLeft: &MenuBar, pRight: nullptr);
7680
7681 if(m_Map.m_Modified)
7682 {
7683 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
7684 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
7685 Ui()->DoLabel(pRect: &ChangedIndicator, pText: FONT_ICON_CIRCLE, Size: 8.0f, Align: TEXTALIGN_MC);
7686 TextRender()->SetRenderFlags(0);
7687 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
7688 static int s_ChangedIndicator;
7689 DoButton_Editor_Common(pId: &s_ChangedIndicator, pText: "", Checked: 0, pRect: &ChangedIndicator, Flags: 0, pToolTip: "This map has unsaved changes"); // just for the tooltip, result unused
7690 }
7691
7692 char aBuf[IO_MAX_PATH_LENGTH + 32];
7693 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "File: %s", m_aFileName);
7694 SLabelProperties Props;
7695 Props.m_MaxWidth = MenuBar.w;
7696 Props.m_EllipsisAtEnd = true;
7697 Ui()->DoLabel(pRect: &MenuBar, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
7698
7699 char aTimeStr[6];
7700 str_timestamp_format(buffer: aTimeStr, buffer_size: sizeof(aTimeStr), format: "%H:%M");
7701
7702 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "X: %.1f, Y: %.1f, Z: %.1f, A: %.1f, G: %i %s", Ui()->MouseWorldX() / 32.0f, Ui()->MouseWorldY() / 32.0f, MapView()->Zoom()->GetValue(), m_AnimateSpeed, MapView()->MapGrid()->Factor(), aTimeStr);
7703 Ui()->DoLabel(pRect: &Info, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MR);
7704
7705 static int s_CloseButton = 0;
7706 if(DoButton_Editor(pId: &s_CloseButton, pText: "×", Checked: 0, pRect: &Close, Flags: 0, pToolTip: "Exits from the editor"))
7707 {
7708 OnClose();
7709 g_Config.m_ClEditor = 0;
7710 }
7711}
7712
7713void CEditor::Render()
7714{
7715 // basic start
7716 Graphics()->Clear(r: 0.0f, g: 0.0f, b: 0.0f);
7717 CUIRect View = *Ui()->Screen();
7718 Ui()->MapScreen();
7719 m_CursorType = CURSOR_NORMAL;
7720
7721 float Width = View.w;
7722 float Height = View.h;
7723
7724 // reset tip
7725 str_copy(dst&: m_aTooltip, src: "");
7726
7727 // render checker
7728 RenderBackground(View, Texture: m_CheckerTexture, Size: 32.0f, Brightness: 1.0f);
7729
7730 CUIRect MenuBar, ModeBar, ToolBar, StatusBar, ExtraEditor, ToolBox;
7731 m_ShowPicker = Input()->KeyIsPressed(Key: KEY_SPACE) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && m_vSelectedLayers.size() == 1;
7732
7733 if(m_GuiActive)
7734 {
7735 View.HSplitTop(Cut: 16.0f, pTop: &MenuBar, pBottom: &View);
7736 View.HSplitTop(Cut: 53.0f, pTop: &ToolBar, pBottom: &View);
7737 View.VSplitLeft(Cut: m_ToolBoxWidth, pLeft: &ToolBox, pRight: &View);
7738
7739 View.HSplitBottom(Cut: 16.0f, pTop: &View, pBottom: &StatusBar);
7740 if(!m_ShowPicker && m_ActiveExtraEditor != EXTRAEDITOR_NONE)
7741 View.HSplitBottom(Cut: m_aExtraEditorSplits[(int)m_ActiveExtraEditor], pTop: &View, pBottom: &ExtraEditor);
7742 }
7743 else
7744 {
7745 // hack to get keyboard inputs from toolbar even when GUI is not active
7746 ToolBar.x = -100;
7747 ToolBar.y = -100;
7748 ToolBar.w = 50;
7749 ToolBar.h = 50;
7750 }
7751
7752 // a little hack for now
7753 if(m_Mode == MODE_LAYERS)
7754 DoMapEditor(View);
7755
7756 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
7757 {
7758 // handle undo/redo hotkeys
7759 if(Input()->KeyPress(Key: KEY_Z) && Input()->ModifierIsPressed() && !Input()->ShiftIsPressed())
7760 UndoLastAction();
7761 if((Input()->KeyPress(Key: KEY_Y) && Input()->ModifierIsPressed()) || (Input()->KeyPress(Key: KEY_Z) && Input()->ModifierIsPressed() && Input()->ShiftIsPressed()))
7762 RedoLastAction();
7763
7764 // handle brush save/load hotkeys
7765 for(int i = KEY_1; i <= KEY_0; i++)
7766 {
7767 if(Input()->KeyPress(Key: i))
7768 {
7769 int Slot = i - KEY_1;
7770 if(Input()->ModifierIsPressed() && !m_pBrush->IsEmpty())
7771 {
7772 dbg_msg(sys: "editor", fmt: "saving current brush to %d", Slot);
7773 m_apSavedBrushes[Slot] = std::make_shared<CLayerGroup>(args&: *m_pBrush);
7774 }
7775 else if(m_apSavedBrushes[Slot])
7776 {
7777 dbg_msg(sys: "editor", fmt: "loading brush from slot %d", Slot);
7778 m_pBrush = std::make_shared<CLayerGroup>(args&: *m_apSavedBrushes[Slot]);
7779 }
7780 }
7781 }
7782 }
7783
7784 float Brightness = 0.25f;
7785
7786 if(m_GuiActive)
7787 {
7788 RenderBackground(View: MenuBar, Texture: m_BackgroundTexture, Size: 128.0f, Brightness: Brightness * 0);
7789 MenuBar.Margin(Cut: 2.0f, pOtherRect: &MenuBar);
7790
7791 RenderBackground(View: ToolBox, Texture: m_BackgroundTexture, Size: 128.0f, Brightness);
7792 ToolBox.Margin(Cut: 2.0f, pOtherRect: &ToolBox);
7793
7794 RenderBackground(View: ToolBar, Texture: m_BackgroundTexture, Size: 128.0f, Brightness);
7795 ToolBar.Margin(Cut: 2.0f, pOtherRect: &ToolBar);
7796 ToolBar.VSplitLeft(Cut: m_ToolBoxWidth, pLeft: &ModeBar, pRight: &ToolBar);
7797
7798 RenderBackground(View: StatusBar, Texture: m_BackgroundTexture, Size: 128.0f, Brightness);
7799 StatusBar.Margin(Cut: 2.0f, pOtherRect: &StatusBar);
7800 }
7801
7802 // do the toolbar
7803 if(m_Mode == MODE_LAYERS)
7804 DoToolbarLayers(ToolBar);
7805 else if(m_Mode == MODE_SOUNDS)
7806 DoToolbarSounds(ToolBar);
7807
7808 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
7809 {
7810 const bool ModPressed = Input()->ModifierIsPressed();
7811 const bool ShiftPressed = Input()->ShiftIsPressed();
7812 const bool AltPressed = Input()->AltIsPressed();
7813 // ctrl+n to create new map
7814 if(Input()->KeyPress(Key: KEY_N) && ModPressed)
7815 {
7816 if(HasUnsavedData())
7817 {
7818 if(!m_PopupEventWasActivated)
7819 {
7820 m_PopupEventType = POPEVENT_NEW;
7821 m_PopupEventActivated = true;
7822 }
7823 }
7824 else
7825 {
7826 Reset();
7827 m_aFileName[0] = 0;
7828 }
7829 }
7830 // ctrl+a to append map
7831 if(Input()->KeyPress(Key: KEY_A) && ModPressed)
7832 {
7833 InvokeFileDialog(StorageType: IStorage::TYPE_ALL, FileType: FILETYPE_MAP, pTitle: "Append map", pButtonText: "Append", pBasePath: "maps", FilenameAsDefault: false, pfnFunc: CallbackAppendMap, pUser: this);
7834 }
7835 // ctrl+o or ctrl+l to open
7836 if((Input()->KeyPress(Key: KEY_O) || Input()->KeyPress(Key: KEY_L)) && ModPressed)
7837 {
7838 if(ShiftPressed)
7839 {
7840 if(HasUnsavedData())
7841 {
7842 if(!m_PopupEventWasActivated)
7843 {
7844 m_PopupEventType = POPEVENT_LOADCURRENT;
7845 m_PopupEventActivated = true;
7846 }
7847 }
7848 else
7849 {
7850 LoadCurrentMap();
7851 }
7852 }
7853 else
7854 {
7855 if(HasUnsavedData())
7856 {
7857 if(!m_PopupEventWasActivated)
7858 {
7859 m_PopupEventType = POPEVENT_LOAD;
7860 m_PopupEventActivated = true;
7861 }
7862 }
7863 else
7864 {
7865 InvokeFileDialog(StorageType: IStorage::TYPE_ALL, FileType: FILETYPE_MAP, pTitle: "Load map", pButtonText: "Load", pBasePath: "maps", FilenameAsDefault: false, pfnFunc: CallbackOpenMap, pUser: this);
7866 }
7867 }
7868 }
7869
7870 // ctrl+shift+alt+s to save copy
7871 if(Input()->KeyPress(Key: KEY_S) && ModPressed && ShiftPressed && AltPressed)
7872 InvokeFileDialog(StorageType: IStorage::TYPE_SAVE, FileType: FILETYPE_MAP, pTitle: "Save map", pButtonText: "Save", pBasePath: "maps", FilenameAsDefault: true, pfnFunc: CallbackSaveCopyMap, pUser: this);
7873 // ctrl+shift+s to save as
7874 else if(Input()->KeyPress(Key: KEY_S) && ModPressed && ShiftPressed)
7875 InvokeFileDialog(StorageType: IStorage::TYPE_SAVE, FileType: FILETYPE_MAP, pTitle: "Save map", pButtonText: "Save", pBasePath: "maps", FilenameAsDefault: true, pfnFunc: CallbackSaveMap, pUser: this);
7876 // ctrl+s to save
7877 else if(Input()->KeyPress(Key: KEY_S) && ModPressed)
7878 {
7879 if(m_aFileName[0] && m_ValidSaveFilename)
7880 {
7881 if(!m_PopupEventWasActivated)
7882 {
7883 str_copy(dst&: m_aFileSaveName, src: m_aFileName);
7884 CallbackSaveMap(pFileName: m_aFileSaveName, StorageType: IStorage::TYPE_SAVE, pUser: this);
7885 }
7886 }
7887 else
7888 InvokeFileDialog(StorageType: IStorage::TYPE_SAVE, FileType: FILETYPE_MAP, pTitle: "Save map", pButtonText: "Save", pBasePath: "maps", FilenameAsDefault: false, pfnFunc: CallbackSaveMap, pUser: this);
7889 }
7890 }
7891
7892 if(m_GuiActive)
7893 {
7894 CUIRect DragBar;
7895 ToolBox.VSplitRight(Cut: 1.0f, pLeft: &ToolBox, pRight: &DragBar);
7896 DragBar.x -= 2.0f;
7897 DragBar.w += 4.0f;
7898 DoEditorDragBar(View: ToolBox, pDragBar: &DragBar, Side: EDragSide::SIDE_RIGHT, pValue: &m_ToolBoxWidth);
7899
7900 if(m_Mode == MODE_LAYERS)
7901 RenderLayers(LayersBox: ToolBox);
7902 else if(m_Mode == MODE_IMAGES)
7903 {
7904 RenderImagesList(ToolBox);
7905 RenderSelectedImage(View);
7906 }
7907 else if(m_Mode == MODE_SOUNDS)
7908 RenderSounds(ToolBox);
7909 }
7910
7911 Ui()->MapScreen();
7912
7913 CUIRect TooltipRect;
7914 if(m_GuiActive)
7915 {
7916 RenderMenubar(MenuBar);
7917 RenderModebar(View: ModeBar);
7918 if(!m_ShowPicker)
7919 {
7920 if(m_ActiveExtraEditor != EXTRAEDITOR_NONE)
7921 {
7922 RenderBackground(View: ExtraEditor, Texture: m_BackgroundTexture, Size: 128.0f, Brightness);
7923 ExtraEditor.HMargin(Cut: 2.0f, pOtherRect: &ExtraEditor);
7924 ExtraEditor.VSplitRight(Cut: 2.0f, pLeft: &ExtraEditor, pRight: nullptr);
7925 }
7926
7927 static bool s_ShowServerSettingsEditorLast = false;
7928 if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES)
7929 {
7930 RenderEnvelopeEditor(View: ExtraEditor);
7931 }
7932 else if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS)
7933 {
7934 RenderServerSettingsEditor(View: ExtraEditor, ShowServerSettingsEditorLast: s_ShowServerSettingsEditorLast);
7935 }
7936 else if(m_ActiveExtraEditor == EXTRAEDITOR_HISTORY)
7937 {
7938 RenderEditorHistory(View: ExtraEditor);
7939 }
7940 s_ShowServerSettingsEditorLast = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS;
7941 }
7942 RenderStatusbar(View: StatusBar, pTooltipRect: &TooltipRect);
7943 }
7944
7945 RenderPressedKeys(View);
7946 RenderSavingIndicator(View);
7947
7948 if(m_Dialog == DIALOG_FILE)
7949 {
7950 static int s_NullUiTarget = 0;
7951 Ui()->SetHotItem(&s_NullUiTarget);
7952 RenderFileDialog();
7953 }
7954 else if(m_Dialog == DIALOG_MAPSETTINGS_ERROR)
7955 {
7956 static int s_NullUiTarget = 0;
7957 Ui()->SetHotItem(&s_NullUiTarget);
7958 RenderMapSettingsErrorDialog();
7959 }
7960
7961 if(m_PopupEventActivated)
7962 {
7963 static SPopupMenuId s_PopupEventId;
7964 constexpr float PopupWidth = 400.0f;
7965 constexpr float PopupHeight = 150.0f;
7966 Ui()->DoPopupMenu(pId: &s_PopupEventId, X: Width / 2.0f - PopupWidth / 2.0f, Y: Height / 2.0f - PopupHeight / 2.0f, Width: PopupWidth, Height: PopupHeight, pContext: this, pfnFunc: PopupEvent);
7967 m_PopupEventActivated = false;
7968 m_PopupEventWasActivated = true;
7969 }
7970
7971 if(m_Dialog == DIALOG_NONE && !Ui()->IsPopupHovered() && Ui()->MouseInside(pRect: &View))
7972 {
7973 // handle zoom hotkeys
7974 if(Input()->KeyPress(Key: KEY_KP_MINUS))
7975 MapView()->Zoom()->ChangeValue(Amount: 50.0f);
7976 if(Input()->KeyPress(Key: KEY_KP_PLUS))
7977 MapView()->Zoom()->ChangeValue(Amount: -50.0f);
7978 if(Input()->KeyPress(Key: KEY_KP_MULTIPLY))
7979 MapView()->ResetZoom();
7980
7981 if(m_pBrush->IsEmpty() || !Input()->ShiftIsPressed())
7982 {
7983 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
7984 MapView()->Zoom()->ChangeValue(Amount: 20.0f);
7985 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
7986 MapView()->Zoom()->ChangeValue(Amount: -20.0f);
7987 }
7988 if(!m_pBrush->IsEmpty())
7989 {
7990 const bool HasTeleTiles = std::any_of(first: m_pBrush->m_vpLayers.begin(), last: m_pBrush->m_vpLayers.end(), pred: [](auto pLayer) {
7991 return pLayer->m_Type == LAYERTYPE_TILES && std::static_pointer_cast<CLayerTiles>(pLayer)->m_Tele;
7992 });
7993 if(HasTeleTiles)
7994 str_copy(dst&: m_aTooltip, src: "Use shift+mousewheel up/down to adjust the tele numbers. Use ctrl+f to change all tele numbers to the first unused number.");
7995
7996 if(Input()->ShiftIsPressed())
7997 {
7998 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
7999 AdjustBrushSpecialTiles(UseNextFree: false, Adjust: -1);
8000 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
8001 AdjustBrushSpecialTiles(UseNextFree: false, Adjust: 1);
8002 }
8003
8004 // Use ctrl+f to replace number in brush with next free
8005 if(Input()->ModifierIsPressed() && Input()->KeyPress(Key: KEY_F))
8006 AdjustBrushSpecialTiles(UseNextFree: true);
8007 }
8008 }
8009
8010 for(CEditorComponent &Component : m_vComponents)
8011 Component.OnRender(View);
8012
8013 MapView()->UpdateZoom();
8014
8015 // Cancel color pipette with escape before closing popup menus with escape
8016 if(m_ColorPipetteActive && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
8017 {
8018 m_ColorPipetteActive = false;
8019 }
8020
8021 Ui()->RenderPopupMenus();
8022 FreeDynamicPopupMenus();
8023
8024 UpdateColorPipette();
8025
8026 if(m_Dialog == DIALOG_NONE && !m_PopupEventActivated && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
8027 {
8028 OnClose();
8029 g_Config.m_ClEditor = 0;
8030 }
8031
8032 // The tooltip can be set in popup menus so we have to render the tooltip after the popup menus.
8033 if(m_GuiActive)
8034 RenderTooltip(TooltipRect);
8035
8036 RenderMousePointer();
8037}
8038
8039void CEditor::RenderPressedKeys(CUIRect View)
8040{
8041 if(!g_Config.m_EdShowkeys)
8042 return;
8043
8044 Ui()->MapScreen();
8045 CTextCursor Cursor;
8046 TextRender()->SetCursor(pCursor: &Cursor, x: View.x + 10, y: View.y + View.h - 24 - 10, FontSize: 24.0f, Flags: TEXTFLAG_RENDER);
8047
8048 int NKeys = 0;
8049 for(int i = 0; i < KEY_LAST; i++)
8050 {
8051 if(Input()->KeyIsPressed(Key: i))
8052 {
8053 if(NKeys)
8054 TextRender()->TextEx(pCursor: &Cursor, pText: " + ", Length: -1);
8055 TextRender()->TextEx(pCursor: &Cursor, pText: Input()->KeyName(Key: i), Length: -1);
8056 NKeys++;
8057 }
8058 }
8059}
8060
8061void CEditor::RenderSavingIndicator(CUIRect View)
8062{
8063 if(m_WriterFinishJobs.empty())
8064 return;
8065
8066 const char *pText = "Saving…";
8067 const float FontSize = 24.0f;
8068
8069 Ui()->MapScreen();
8070 CUIRect Label, Spinner;
8071 View.Margin(Cut: 20.0f, pOtherRect: &View);
8072 View.HSplitBottom(Cut: FontSize, pTop: nullptr, pBottom: &View);
8073 View.VSplitRight(Cut: TextRender()->TextWidth(Size: FontSize, pText) + 2.0f, pLeft: &Spinner, pRight: &Label);
8074 Spinner.VSplitRight(Cut: Spinner.h, pLeft: nullptr, pRight: &Spinner);
8075 Ui()->DoLabel(pRect: &Label, pText, Size: FontSize, Align: TEXTALIGN_MR);
8076 Ui()->RenderProgressSpinner(Center: Spinner.Center(), OuterRadius: 8.0f);
8077}
8078
8079void CEditor::FreeDynamicPopupMenus()
8080{
8081 auto Iterator = m_PopupMessageContexts.begin();
8082 while(Iterator != m_PopupMessageContexts.end())
8083 {
8084 if(!Ui()->IsPopupOpen(pId: Iterator->second))
8085 {
8086 CUi::SMessagePopupContext *pContext = Iterator->second;
8087 Iterator = m_PopupMessageContexts.erase(position: Iterator);
8088 delete pContext;
8089 }
8090 else
8091 ++Iterator;
8092 }
8093}
8094
8095void CEditor::UpdateColorPipette()
8096{
8097 if(!m_ColorPipetteActive)
8098 return;
8099
8100 static char s_PipetteScreenButton;
8101 if(Ui()->HotItem() == &s_PipetteScreenButton)
8102 {
8103 // Read color one pixel to the top and left as we would otherwise not read the correct
8104 // color due to the cursor sprite being rendered over the current mouse position.
8105 const int PixelX = clamp<int>(val: round_to_int(f: (Ui()->MouseX() - 1.0f) / Ui()->Screen()->w * Graphics()->ScreenWidth()), lo: 0, hi: Graphics()->ScreenWidth() - 1);
8106 const int PixelY = clamp<int>(val: round_to_int(f: (Ui()->MouseY() - 1.0f) / Ui()->Screen()->h * Graphics()->ScreenHeight()), lo: 0, hi: Graphics()->ScreenHeight() - 1);
8107 Graphics()->ReadPixel(Position: ivec2(PixelX, PixelY), pColor: &m_PipetteColor);
8108 }
8109
8110 // Simulate button overlaying the entire screen to intercept all clicks for color pipette.
8111 const int ButtonResult = DoButton_Editor_Common(pId: &s_PipetteScreenButton, pText: "", Checked: 0, pRect: Ui()->Screen(), Flags: 0, pToolTip: "Left click to pick a color from the screen. Right click to cancel pipette mode.");
8112 // Don't handle clicks if we are panning, so the pipette stays active while panning.
8113 // Checking m_pContainerPanned alone is not enough, as this variable is reset when
8114 // panning ends before this function is called.
8115 if(m_pContainerPanned == nullptr && m_pContainerPannedLast == nullptr)
8116 {
8117 if(ButtonResult == 1)
8118 {
8119 char aClipboard[9];
8120 str_format(buffer: aClipboard, buffer_size: sizeof(aClipboard), format: "%08X", m_PipetteColor.PackAlphaLast());
8121 Input()->SetClipboardText(aClipboard);
8122
8123 // Check if any of the saved colors is equal to the picked color and
8124 // bring it to the front of the list instead of adding a duplicate.
8125 int ShiftEnd = (int)std::size(m_aSavedColors) - 1;
8126 for(int i = 0; i < (int)std::size(m_aSavedColors); ++i)
8127 {
8128 if(m_aSavedColors[i].Pack() == m_PipetteColor.Pack())
8129 {
8130 ShiftEnd = i;
8131 break;
8132 }
8133 }
8134 for(int i = ShiftEnd; i > 0; --i)
8135 {
8136 m_aSavedColors[i] = m_aSavedColors[i - 1];
8137 }
8138 m_aSavedColors[0] = m_PipetteColor;
8139 }
8140 if(ButtonResult > 0)
8141 {
8142 m_ColorPipetteActive = false;
8143 }
8144 }
8145}
8146
8147void CEditor::RenderMousePointer()
8148{
8149 if(!m_ShowMousePointer)
8150 return;
8151
8152 constexpr float CursorSize = 16.0f;
8153
8154 // Cursor
8155 Graphics()->WrapClamp();
8156 Graphics()->TextureSet(Texture: m_aCursorTextures[m_CursorType]);
8157 Graphics()->QuadsBegin();
8158 if(m_CursorType == CURSOR_RESIZE_V)
8159 {
8160 Graphics()->QuadsSetRotation(Angle: pi / 2.0f);
8161 }
8162 if(ms_pUiGotContext == Ui()->HotItem())
8163 {
8164 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 1.0f);
8165 }
8166 const float CursorOffset = m_CursorType == CURSOR_RESIZE_V || m_CursorType == CURSOR_RESIZE_H ? -CursorSize / 2.0f : 0.0f;
8167 IGraphics::CQuadItem QuadItem(Ui()->MouseX() + CursorOffset, Ui()->MouseY() + CursorOffset, CursorSize, CursorSize);
8168 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
8169 Graphics()->QuadsEnd();
8170 Graphics()->WrapNormal();
8171
8172 // Pipette color
8173 if(m_ColorPipetteActive)
8174 {
8175 CUIRect PipetteRect = {.x: Ui()->MouseX() + CursorSize, .y: Ui()->MouseY() + CursorSize, .w: 80.0f, .h: 20.0f};
8176 if(PipetteRect.x + PipetteRect.w + 2.0f > Ui()->Screen()->w)
8177 {
8178 PipetteRect.x = Ui()->MouseX() - PipetteRect.w - CursorSize / 2.0f;
8179 }
8180 if(PipetteRect.y + PipetteRect.h + 2.0f > Ui()->Screen()->h)
8181 {
8182 PipetteRect.y = Ui()->MouseY() - PipetteRect.h - CursorSize / 2.0f;
8183 }
8184 PipetteRect.Draw(Color: ColorRGBA(0.2f, 0.2f, 0.2f, 0.7f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
8185
8186 CUIRect Pipette, Label;
8187 PipetteRect.VSplitLeft(Cut: PipetteRect.h, pLeft: &Pipette, pRight: &Label);
8188 Pipette.Margin(Cut: 2.0f, pOtherRect: &Pipette);
8189 Pipette.Draw(Color: m_PipetteColor, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
8190
8191 char aLabel[8];
8192 str_format(buffer: aLabel, buffer_size: sizeof(aLabel), format: "#%06X", m_PipetteColor.PackAlphaLast(Alpha: false));
8193 Ui()->DoLabel(pRect: &Label, pText: aLabel, Size: 10.0f, Align: TEXTALIGN_MC);
8194 }
8195}
8196
8197void CEditor::Reset(bool CreateDefault)
8198{
8199 Ui()->ClosePopupMenus();
8200 m_Map.Clean();
8201
8202 for(CEditorComponent &Component : m_vComponents)
8203 Component.OnReset();
8204
8205 // create default layers
8206 if(CreateDefault)
8207 {
8208 m_EditorWasUsedBefore = true;
8209 m_Map.CreateDefault(EntitiesTexture: GetEntitiesTexture());
8210 }
8211
8212 SelectGameLayer();
8213 DeselectQuads();
8214 DeselectQuadPoints();
8215 m_SelectedEnvelope = 0;
8216 m_SelectedImage = 0;
8217 m_SelectedSound = 0;
8218 m_SelectedSource = -1;
8219
8220 m_MouseDeltaX = 0;
8221 m_MouseDeltaY = 0;
8222 m_MouseDeltaWx = 0;
8223 m_MouseDeltaWy = 0;
8224 m_pContainerPanned = nullptr;
8225 m_pContainerPannedLast = nullptr;
8226
8227 m_Map.m_Modified = false;
8228 m_Map.m_ModifiedAuto = false;
8229 m_Map.m_LastModifiedTime = -1.0f;
8230 m_Map.m_LastSaveTime = Client()->GlobalTime();
8231 m_Map.m_LastAutosaveUpdateTime = -1.0f;
8232
8233 m_ShowEnvelopePreview = SHOWENV_NONE;
8234 m_ShiftBy = 1;
8235
8236 m_ResetZoomEnvelope = true;
8237 m_SettingsCommandInput.Clear();
8238
8239 m_EditorHistory.Clear();
8240 m_EnvelopeEditorHistory.Clear();
8241 m_ServerSettingsHistory.Clear();
8242
8243 m_QuadTracker.m_pEditor = this;
8244
8245 m_EnvOpTracker.m_pEditor = this;
8246 m_EnvOpTracker.Reset();
8247
8248 m_MapSettingsCommandContext.Reset();
8249}
8250
8251int CEditor::GetTextureUsageFlag() const
8252{
8253 return Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
8254}
8255
8256IGraphics::CTextureHandle CEditor::GetFrontTexture()
8257{
8258 if(!m_FrontTexture.IsValid())
8259 m_FrontTexture = Graphics()->LoadTexture(pFilename: "editor/front.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
8260 return m_FrontTexture;
8261}
8262
8263IGraphics::CTextureHandle CEditor::GetTeleTexture()
8264{
8265 if(!m_TeleTexture.IsValid())
8266 m_TeleTexture = Graphics()->LoadTexture(pFilename: "editor/tele.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
8267 return m_TeleTexture;
8268}
8269
8270IGraphics::CTextureHandle CEditor::GetSpeedupTexture()
8271{
8272 if(!m_SpeedupTexture.IsValid())
8273 m_SpeedupTexture = Graphics()->LoadTexture(pFilename: "editor/speedup.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
8274 return m_SpeedupTexture;
8275}
8276
8277IGraphics::CTextureHandle CEditor::GetSwitchTexture()
8278{
8279 if(!m_SwitchTexture.IsValid())
8280 m_SwitchTexture = Graphics()->LoadTexture(pFilename: "editor/switch.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
8281 return m_SwitchTexture;
8282}
8283
8284IGraphics::CTextureHandle CEditor::GetTuneTexture()
8285{
8286 if(!m_TuneTexture.IsValid())
8287 m_TuneTexture = Graphics()->LoadTexture(pFilename: "editor/tune.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
8288 return m_TuneTexture;
8289}
8290
8291IGraphics::CTextureHandle CEditor::GetEntitiesTexture()
8292{
8293 if(!m_EntitiesTexture.IsValid())
8294 m_EntitiesTexture = Graphics()->LoadTexture(pFilename: "editor/entities/DDNet.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
8295 return m_EntitiesTexture;
8296}
8297
8298void CEditor::Init()
8299{
8300 m_pInput = Kernel()->RequestInterface<IInput>();
8301 m_pClient = Kernel()->RequestInterface<IClient>();
8302 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
8303 m_pConfig = m_pConfigManager->Values();
8304 m_pConsole = Kernel()->RequestInterface<IConsole>();
8305 m_pEngine = Kernel()->RequestInterface<IEngine>();
8306 m_pGraphics = Kernel()->RequestInterface<IGraphics>();
8307 m_pTextRender = Kernel()->RequestInterface<ITextRender>();
8308 m_pStorage = Kernel()->RequestInterface<IStorage>();
8309 m_pSound = Kernel()->RequestInterface<ISound>();
8310 m_UI.Init(pKernel: Kernel());
8311 m_UI.SetPopupMenuClosedCallback([this]() {
8312 m_PopupEventWasActivated = false;
8313 });
8314 m_RenderTools.Init(pGraphics: m_pGraphics, pTextRender: m_pTextRender);
8315 m_ZoomEnvelopeX.Init(pEditor: this);
8316 m_ZoomEnvelopeY.Init(pEditor: this);
8317 m_Map.m_pEditor = this;
8318
8319 m_vComponents.emplace_back(args&: m_MapView);
8320 m_vComponents.emplace_back(args&: m_MapSettingsBackend);
8321 m_vComponents.emplace_back(args&: m_LayerSelector);
8322 for(CEditorComponent &Component : m_vComponents)
8323 Component.Init(pEditor: this);
8324
8325 m_CheckerTexture = Graphics()->LoadTexture(pFilename: "editor/checker.png", StorageType: IStorage::TYPE_ALL);
8326 m_BackgroundTexture = Graphics()->LoadTexture(pFilename: "editor/background.png", StorageType: IStorage::TYPE_ALL);
8327 m_aCursorTextures[CURSOR_NORMAL] = Graphics()->LoadTexture(pFilename: "editor/cursor.png", StorageType: IStorage::TYPE_ALL);
8328 m_aCursorTextures[CURSOR_RESIZE_H] = Graphics()->LoadTexture(pFilename: "editor/cursor_resize.png", StorageType: IStorage::TYPE_ALL);
8329 m_aCursorTextures[CURSOR_RESIZE_V] = m_aCursorTextures[CURSOR_RESIZE_H];
8330
8331 m_pTilesetPicker = std::make_shared<CLayerTiles>(args: this, args: 16, args: 16);
8332 m_pTilesetPicker->MakePalette();
8333 m_pTilesetPicker->m_Readonly = true;
8334
8335 m_pQuadsetPicker = std::make_shared<CLayerQuads>(args: this);
8336 m_pQuadsetPicker->NewQuad(x: 0, y: 0, Width: 64, Height: 64);
8337 m_pQuadsetPicker->m_Readonly = true;
8338
8339 m_pBrush = std::make_shared<CLayerGroup>();
8340 m_pBrush->m_pMap = &m_Map;
8341
8342 Reset(CreateDefault: false);
8343}
8344
8345void CEditor::PlaceBorderTiles()
8346{
8347 std::shared_ptr<CLayerTiles> pT = std::static_pointer_cast<CLayerTiles>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_TILES));
8348
8349 for(int i = 0; i < pT->m_Width * pT->m_Height; ++i)
8350 {
8351 if(i % pT->m_Width < 2 || i % pT->m_Width > pT->m_Width - 3 || i < pT->m_Width * 2 || i > pT->m_Width * (pT->m_Height - 2))
8352 {
8353 int x = i % pT->m_Width;
8354 int y = i / pT->m_Width;
8355
8356 CTile Current = pT->m_pTiles[i];
8357 Current.m_Index = 1;
8358 pT->SetTile(x, y, Tile: Current);
8359 }
8360 }
8361
8362 int GameGroupIndex = std::find(first: m_Map.m_vpGroups.begin(), last: m_Map.m_vpGroups.end(), val: m_Map.m_pGameGroup) - m_Map.m_vpGroups.begin();
8363 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorBrushDrawAction>(args: this, args&: GameGroupIndex), pDisplay: "Tool 'Make borders'");
8364
8365 m_Map.OnModify();
8366}
8367
8368void CEditor::HandleCursorMovement()
8369{
8370 static float s_MouseX = 0.0f;
8371 static float s_MouseY = 0.0f;
8372
8373 float MouseRelX = 0.0f, MouseRelY = 0.0f;
8374 IInput::ECursorType CursorType = Input()->CursorRelative(pX: &MouseRelX, pY: &MouseRelY);
8375 if(CursorType != IInput::CURSOR_NONE)
8376 Ui()->ConvertMouseMove(pX: &MouseRelX, pY: &MouseRelY, CursorType);
8377
8378 m_MouseDeltaX += MouseRelX;
8379 m_MouseDeltaY += MouseRelY;
8380
8381 if(!Ui()->CheckMouseLock())
8382 {
8383 s_MouseX = clamp(val: s_MouseX + MouseRelX, lo: 0.0f, hi: Graphics()->WindowWidth() - 1.0f);
8384 s_MouseY = clamp(val: s_MouseY + MouseRelY, lo: 0.0f, hi: Graphics()->WindowHeight() - 1.0f);
8385 }
8386
8387 // update positions for ui, but only update ui when rendering
8388 m_MouseX = Ui()->Screen()->w * ((float)s_MouseX / Graphics()->WindowWidth());
8389 m_MouseY = Ui()->Screen()->h * ((float)s_MouseY / Graphics()->WindowHeight());
8390
8391 // fix correct world x and y
8392 std::shared_ptr<CLayerGroup> pGroup = GetSelectedGroup();
8393 if(pGroup)
8394 {
8395 float aPoints[4];
8396 pGroup->Mapping(pPoints: aPoints);
8397
8398 float WorldWidth = aPoints[2] - aPoints[0];
8399 float WorldHeight = aPoints[3] - aPoints[1];
8400
8401 m_MouseWScale = WorldWidth / Graphics()->WindowWidth();
8402
8403 m_MouseWorldX = aPoints[0] + WorldWidth * (s_MouseX / Graphics()->WindowWidth());
8404 m_MouseWorldY = aPoints[1] + WorldHeight * (s_MouseY / Graphics()->WindowHeight());
8405 m_MouseDeltaWx = m_MouseDeltaX * (WorldWidth / Graphics()->WindowWidth());
8406 m_MouseDeltaWy = m_MouseDeltaY * (WorldHeight / Graphics()->WindowHeight());
8407 }
8408 else
8409 {
8410 m_MouseWorldX = 0.0f;
8411 m_MouseWorldY = 0.0f;
8412 }
8413
8414 for(const std::shared_ptr<CLayerGroup> &pGameGroup : m_Map.m_vpGroups)
8415 {
8416 if(!pGameGroup->m_GameGroup)
8417 continue;
8418
8419 float aPoints[4];
8420 pGameGroup->Mapping(pPoints: aPoints);
8421
8422 float WorldWidth = aPoints[2] - aPoints[0];
8423 float WorldHeight = aPoints[3] - aPoints[1];
8424
8425 m_MouseWorldNoParaX = aPoints[0] + WorldWidth * (s_MouseX / Graphics()->WindowWidth());
8426 m_MouseWorldNoParaY = aPoints[1] + WorldHeight * (s_MouseY / Graphics()->WindowHeight());
8427 }
8428
8429 OnMouseMove(MouseX: s_MouseX, MouseY: s_MouseY);
8430}
8431
8432void CEditor::OnMouseMove(float MouseX, float MouseY)
8433{
8434 m_vHoverTiles.clear();
8435 for(size_t g = 0; g < m_Map.m_vpGroups.size(); g++)
8436 {
8437 const std::shared_ptr<CLayerGroup> pGroup = m_Map.m_vpGroups[g];
8438 for(size_t l = 0; l < pGroup->m_vpLayers.size(); l++)
8439 {
8440 const std::shared_ptr<CLayer> pLayer = pGroup->m_vpLayers[l];
8441 int LayerType = pLayer->m_Type;
8442 if(LayerType != LAYERTYPE_TILES &&
8443 LayerType != LAYERTYPE_FRONT &&
8444 LayerType != LAYERTYPE_TELE &&
8445 LayerType != LAYERTYPE_SPEEDUP &&
8446 LayerType != LAYERTYPE_SWITCH &&
8447 LayerType != LAYERTYPE_TUNE)
8448 continue;
8449
8450 std::shared_ptr<CLayerTiles> pTiles = std::static_pointer_cast<CLayerTiles>(r: pLayer);
8451 pGroup->MapScreen();
8452 float aPoints[4];
8453 pGroup->Mapping(pPoints: aPoints);
8454 float WorldWidth = aPoints[2] - aPoints[0];
8455 float WorldHeight = aPoints[3] - aPoints[1];
8456 CUIRect Rect;
8457 Rect.x = aPoints[0] + WorldWidth * (MouseX / Graphics()->WindowWidth());
8458 Rect.y = aPoints[1] + WorldHeight * (MouseY / Graphics()->WindowHeight());
8459 Rect.w = 0;
8460 Rect.h = 0;
8461 RECTi r;
8462 pTiles->Convert(Rect, pOut: &r);
8463 pTiles->Clamp(pRect: &r);
8464 int x = r.x;
8465 int y = r.y;
8466
8467 if(x < 0 || x >= pTiles->m_Width)
8468 continue;
8469 if(y < 0 || y >= pTiles->m_Height)
8470 continue;
8471 CTile Tile = pTiles->GetTile(x, y);
8472 if(Tile.m_Index)
8473 m_vHoverTiles.emplace_back(args: CHoverTile(
8474 g, l, x, y, Tile));
8475 }
8476 }
8477 Ui()->MapScreen();
8478}
8479
8480void CEditor::DispatchInputEvents()
8481{
8482 for(size_t i = 0; i < Input()->NumEvents(); i++)
8483 {
8484 const IInput::CEvent &Event = Input()->GetEvent(Index: i);
8485 if(!Input()->IsEventValid(Event))
8486 continue;
8487 Ui()->OnInput(Event);
8488 }
8489}
8490
8491void CEditor::HandleAutosave()
8492{
8493 const float Time = Client()->GlobalTime();
8494 const float LastAutosaveUpdateTime = m_Map.m_LastAutosaveUpdateTime;
8495 m_Map.m_LastAutosaveUpdateTime = Time;
8496
8497 if(g_Config.m_EdAutosaveInterval == 0)
8498 return; // autosave disabled
8499 if(!m_Map.m_ModifiedAuto || m_Map.m_LastModifiedTime < 0.0f)
8500 return; // no unsaved changes
8501
8502 // Add time to autosave timer if the editor was disabled for more than 10 seconds,
8503 // to prevent autosave from immediately activating when the editor is activated
8504 // after being deactivated for some time.
8505 if(LastAutosaveUpdateTime >= 0.0f && Time - LastAutosaveUpdateTime > 10.0f)
8506 {
8507 m_Map.m_LastSaveTime += Time - LastAutosaveUpdateTime;
8508 }
8509
8510 // Check if autosave timer has expired.
8511 if(m_Map.m_LastSaveTime >= Time || Time - m_Map.m_LastSaveTime < 60 * g_Config.m_EdAutosaveInterval)
8512 return;
8513
8514 // Wait for 5 seconds of no modification before saving, to prevent autosave
8515 // from immediately activating when a map is first modified or while user is
8516 // modifying the map, but don't delay the autosave for more than 1 minute.
8517 if(Time - m_Map.m_LastModifiedTime < 5.0f && Time - m_Map.m_LastSaveTime < 60 * (g_Config.m_EdAutosaveInterval + 1))
8518 return;
8519
8520 PerformAutosave();
8521}
8522
8523bool CEditor::PerformAutosave()
8524{
8525 char aDate[20];
8526 char aAutosavePath[IO_MAX_PATH_LENGTH];
8527 str_timestamp(buffer: aDate, buffer_size: sizeof(aDate));
8528 char aFileNameNoExt[IO_MAX_PATH_LENGTH];
8529 if(m_aFileName[0] == '\0')
8530 {
8531 str_copy(dst&: aFileNameNoExt, src: "unnamed");
8532 }
8533 else
8534 {
8535 const char *pFileName = fs_filename(path: m_aFileName);
8536 str_truncate(dst: aFileNameNoExt, dst_size: sizeof(aFileNameNoExt), src: pFileName, truncation_len: str_length(str: pFileName) - str_length(str: ".map"));
8537 }
8538 str_format(buffer: aAutosavePath, buffer_size: sizeof(aAutosavePath), format: "maps/auto/%s_%s.map", aFileNameNoExt, aDate);
8539
8540 m_Map.m_LastSaveTime = Client()->GlobalTime();
8541 if(Save(pFilename: aAutosavePath))
8542 {
8543 m_Map.m_ModifiedAuto = false;
8544 // Clean up autosaves
8545 if(g_Config.m_EdAutosaveMax)
8546 {
8547 CFileCollection AutosavedMaps;
8548 AutosavedMaps.Init(pStorage: Storage(), pPath: "maps/auto", pFileDesc: aFileNameNoExt, pFileExt: ".map", MaxEntries: g_Config.m_EdAutosaveMax);
8549 }
8550 return true;
8551 }
8552 else
8553 {
8554 ShowFileDialogError(pFormat: "Failed to automatically save map to file '%s'.", aAutosavePath);
8555 return false;
8556 }
8557}
8558
8559void CEditor::HandleWriterFinishJobs()
8560{
8561 if(m_WriterFinishJobs.empty())
8562 return;
8563
8564 std::shared_ptr<CDataFileWriterFinishJob> pJob = m_WriterFinishJobs.front();
8565 if(!pJob->Done())
8566 return;
8567 m_WriterFinishJobs.pop_front();
8568
8569 char aBuf[2 * IO_MAX_PATH_LENGTH + 128];
8570 if(Storage()->FileExists(pFilename: pJob->GetRealFileName(), Type: IStorage::TYPE_SAVE) && !Storage()->RemoveFile(pFilename: pJob->GetRealFileName(), Type: IStorage::TYPE_SAVE))
8571 {
8572 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Saving failed: Could not remove old map file '%s'.", pJob->GetRealFileName());
8573 ShowFileDialogError(pFormat: "%s", aBuf);
8574 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "editor/save", pStr: aBuf);
8575 return;
8576 }
8577
8578 if(!Storage()->RenameFile(pOldFilename: pJob->GetTempFileName(), pNewFilename: pJob->GetRealFileName(), Type: IStorage::TYPE_SAVE))
8579 {
8580 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Saving failed: Could not move temporary map file '%s' to '%s'.", pJob->GetTempFileName(), pJob->GetRealFileName());
8581 ShowFileDialogError(pFormat: "%s", aBuf);
8582 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "editor/save", pStr: aBuf);
8583 return;
8584 }
8585
8586 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "saving '%s' done", pJob->GetRealFileName());
8587 Console()->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "editor/save", pStr: aBuf);
8588
8589 // send rcon.. if we can
8590 if(Client()->RconAuthed())
8591 {
8592 CServerInfo CurrentServerInfo;
8593 Client()->GetServerInfo(pServerInfo: &CurrentServerInfo);
8594 NETADDR ServerAddr = Client()->ServerAddress();
8595 const unsigned char aIpv4Localhost[16] = {127, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
8596 const unsigned char aIpv6Localhost[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1};
8597
8598 // and if we're on localhost
8599 if(!mem_comp(a: ServerAddr.ip, b: aIpv4Localhost, size: sizeof(aIpv4Localhost)) || !mem_comp(a: ServerAddr.ip, b: aIpv6Localhost, size: sizeof(aIpv6Localhost)))
8600 {
8601 char aMapName[128];
8602 IStorage::StripPathAndExtension(pFilename: pJob->GetRealFileName(), pBuffer: aMapName, BufferSize: sizeof(aMapName));
8603 if(!str_comp(a: aMapName, b: CurrentServerInfo.m_aMap))
8604 Client()->Rcon(pLine: "reload");
8605 }
8606 }
8607}
8608
8609void CEditor::OnUpdate()
8610{
8611 CUIElementBase::Init(pUI: Ui()); // update static pointer because game and editor use separate UI
8612
8613 if(!m_EditorWasUsedBefore)
8614 {
8615 m_EditorWasUsedBefore = true;
8616 Reset();
8617 }
8618
8619 m_pContainerPannedLast = m_pContainerPanned;
8620
8621 // handle key presses
8622 for(size_t i = 0; i < Input()->NumEvents(); i++)
8623 {
8624 const IInput::CEvent &Event = Input()->GetEvent(Index: i);
8625 if(!Input()->IsEventValid(Event))
8626 continue;
8627
8628 for(CEditorComponent &Component : m_vComponents)
8629 {
8630 if(Component.OnInput(Event))
8631 break;
8632 }
8633 }
8634
8635 HandleCursorMovement();
8636 DispatchInputEvents();
8637 HandleAutosave();
8638 HandleWriterFinishJobs();
8639
8640 for(CEditorComponent &Component : m_vComponents)
8641 Component.OnUpdate();
8642}
8643
8644void CEditor::OnRender()
8645{
8646 Ui()->ResetMouseSlow();
8647
8648 // toggle gui
8649 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_TAB))
8650 m_GuiActive = !m_GuiActive;
8651
8652 if(Input()->KeyPress(Key: KEY_F10))
8653 m_ShowMousePointer = false;
8654
8655 if(m_Animate)
8656 m_AnimateTime = (time_get() - m_AnimateStart) / (float)time_freq();
8657 else
8658 m_AnimateTime = 0;
8659
8660 ms_pUiGotContext = nullptr;
8661 Ui()->StartCheck();
8662
8663 Ui()->Update(MouseX: m_MouseX, MouseY: m_MouseY, MouseDeltaX: m_MouseDeltaX, MouseDeltaY: m_MouseDeltaY, MouseWorldX: m_MouseWorldX, MouseWorldY: m_MouseWorldY);
8664
8665 Render();
8666
8667 m_MouseDeltaX = 0.0f;
8668 m_MouseDeltaY = 0.0f;
8669 m_MouseDeltaWx = 0.0f;
8670 m_MouseDeltaWy = 0.0f;
8671
8672 if(Input()->KeyPress(Key: KEY_F10))
8673 {
8674 Graphics()->TakeScreenshot(pFilename: nullptr);
8675 m_ShowMousePointer = true;
8676 }
8677
8678 Ui()->FinishCheck();
8679 Ui()->ClearHotkeys();
8680 Input()->Clear();
8681
8682 CLineInput::RenderCandidates();
8683}
8684
8685void CEditor::OnActivate()
8686{
8687 ResetMentions();
8688 ResetIngameMoved();
8689}
8690
8691void CEditor::OnWindowResize()
8692{
8693 Ui()->OnWindowResize();
8694}
8695
8696void CEditor::OnClose()
8697{
8698 m_ColorPipetteActive = false;
8699
8700 if(m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
8701 Sound()->Pause(SampleId: m_ToolbarPreviewSound);
8702 if(m_FilePreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_FilePreviewSound))
8703 Sound()->Pause(SampleId: m_FilePreviewSound);
8704}
8705
8706void CEditor::OnDialogClose()
8707{
8708 if(m_FilePreviewSound >= 0)
8709 {
8710 Sound()->UnloadSample(SampleId: m_FilePreviewSound);
8711 m_FilePreviewSound = -1;
8712 }
8713}
8714
8715void CEditor::LoadCurrentMap()
8716{
8717 if(Load(pFilename: m_pClient->GetCurrentMapPath(), StorageType: IStorage::TYPE_SAVE))
8718 {
8719 m_ValidSaveFilename = true;
8720 }
8721 else
8722 {
8723 Load(pFilename: m_pClient->GetCurrentMapPath(), StorageType: IStorage::TYPE_ALL);
8724 m_ValidSaveFilename = false;
8725 }
8726
8727 CGameClient *pGameClient = (CGameClient *)Kernel()->RequestInterface<IGameClient>();
8728 vec2 Center = pGameClient->m_Camera.m_Center;
8729
8730 MapView()->SetWorldOffset(Center);
8731}
8732
8733bool CEditor::Save(const char *pFilename)
8734{
8735 // Check if file with this name is already being saved at the moment
8736 if(std::any_of(first: std::begin(cont&: m_WriterFinishJobs), last: std::end(cont&: m_WriterFinishJobs), pred: [pFilename](const std::shared_ptr<CDataFileWriterFinishJob> &Job) { return str_comp(a: pFilename, b: Job->GetRealFileName()) == 0; }))
8737 return false;
8738
8739 return m_Map.Save(pFilename);
8740}
8741
8742bool CEditor::HandleMapDrop(const char *pFileName, int StorageType)
8743{
8744 if(HasUnsavedData())
8745 {
8746 str_copy(dst&: m_aFileNamePending, src: pFileName);
8747 m_PopupEventType = CEditor::POPEVENT_LOADDROP;
8748 m_PopupEventActivated = true;
8749 return true;
8750 }
8751 else
8752 {
8753 return Load(pFilename: pFileName, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE);
8754 }
8755}
8756
8757bool CEditor::Load(const char *pFileName, int StorageType)
8758{
8759 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
8760 ShowFileDialogError(pFormat: "%s", pErrorMessage);
8761 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "editor/load", pStr: pErrorMessage);
8762 };
8763
8764 Reset();
8765 bool Result = m_Map.Load(pFilename: pFileName, StorageType, ErrorHandler: std::move(ErrorHandler));
8766 if(Result)
8767 {
8768 str_copy(dst&: m_aFileName, src: pFileName);
8769 SortImages();
8770 SelectGameLayer();
8771
8772 for(CEditorComponent &Component : m_vComponents)
8773 Component.OnMapLoad();
8774 }
8775 else
8776 {
8777 m_aFileName[0] = 0;
8778 }
8779 return Result;
8780}
8781
8782bool CEditor::Append(const char *pFileName, int StorageType, bool IgnoreHistory)
8783{
8784 CEditorMap NewMap;
8785 NewMap.m_pEditor = this;
8786
8787 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
8788 ShowFileDialogError(pFormat: "%s", pErrorMessage);
8789 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "editor/append", pStr: pErrorMessage);
8790 };
8791 if(!NewMap.Load(pFilename: pFileName, StorageType, ErrorHandler: std::move(ErrorHandler)))
8792 return false;
8793
8794 CEditorActionAppendMap::SPrevInfo Info{
8795 .m_Groups: (int)m_Map.m_vpGroups.size(),
8796 .m_Images: (int)m_Map.m_vpImages.size(),
8797 .m_Sounds: (int)m_Map.m_vpSounds.size(),
8798 .m_Envelopes: (int)m_Map.m_vpEnvelopes.size()};
8799
8800 // Keep a map to check if specific indices have already been replaced to prevent
8801 // replacing those indices again when transfering images
8802 static std::map<int *, bool> s_ReplacedMap;
8803 static const auto &&s_ReplaceIndex = [](int ToReplace, int ReplaceWith) {
8804 return [ToReplace, ReplaceWith](int *pIndex) {
8805 if(*pIndex == ToReplace && !s_ReplacedMap[pIndex])
8806 {
8807 *pIndex = ReplaceWith;
8808 s_ReplacedMap[pIndex] = true;
8809 }
8810 };
8811 };
8812
8813 const auto &&Rename = [&](const std::shared_ptr<CEditorImage> &pImage) {
8814 char aRenamed[IO_MAX_PATH_LENGTH];
8815 int DuplicateCount = 1;
8816 str_copy(dst&: aRenamed, src: pImage->m_aName);
8817 while(std::find_if(first: m_Map.m_vpImages.begin(), last: m_Map.m_vpImages.end(), pred: [aRenamed](const std::shared_ptr<CEditorImage> &OtherImage) { return str_comp(a: OtherImage->m_aName, b: aRenamed) == 0; }) != m_Map.m_vpImages.end())
8818 str_format(buffer: aRenamed, buffer_size: sizeof(aRenamed), format: "%s (%d)", pImage->m_aName, DuplicateCount++); // Rename to "image_name (%d)"
8819 str_copy(dst&: pImage->m_aName, src: aRenamed);
8820 };
8821
8822 // Transfer non-duplicate images
8823 s_ReplacedMap.clear();
8824 for(auto NewMapIt = NewMap.m_vpImages.begin(); NewMapIt != NewMap.m_vpImages.end(); ++NewMapIt)
8825 {
8826 auto pNewImage = *NewMapIt;
8827 auto NameIsTaken = [pNewImage](const std::shared_ptr<CEditorImage> &OtherImage) { return str_comp(a: pNewImage->m_aName, b: OtherImage->m_aName) == 0; };
8828 auto MatchInCurrentMap = std::find_if(first: m_Map.m_vpImages.begin(), last: m_Map.m_vpImages.end(), pred: NameIsTaken);
8829
8830 const bool IsDuplicate = MatchInCurrentMap != m_Map.m_vpImages.end();
8831 const int IndexToReplace = NewMapIt - NewMap.m_vpImages.begin();
8832
8833 if(IsDuplicate)
8834 {
8835 // Check for image data
8836 const bool ImageDataEquals = (*MatchInCurrentMap)->DataEquals(Other: *pNewImage);
8837
8838 if(ImageDataEquals)
8839 {
8840 const int IndexToReplaceWith = MatchInCurrentMap - m_Map.m_vpImages.begin();
8841
8842 dbg_msg(sys: "editor", fmt: "map already contains image %s with the same data, removing duplicate", pNewImage->m_aName);
8843
8844 // In the new map, replace the index of the duplicate image to the index of the same in the current map.
8845 NewMap.ModifyImageIndex(pfnFunc: s_ReplaceIndex(IndexToReplace, IndexToReplaceWith));
8846 }
8847 else
8848 {
8849 // Rename image and add it
8850 Rename(pNewImage);
8851
8852 dbg_msg(sys: "editor", fmt: "map already contains image %s but contents of appended image is different. Renaming to %s", (*MatchInCurrentMap)->m_aName, pNewImage->m_aName);
8853
8854 NewMap.ModifyImageIndex(pfnFunc: s_ReplaceIndex(IndexToReplace, m_Map.m_vpImages.size()));
8855 m_Map.m_vpImages.push_back(x: pNewImage);
8856 }
8857 }
8858 else
8859 {
8860 NewMap.ModifyImageIndex(pfnFunc: s_ReplaceIndex(IndexToReplace, m_Map.m_vpImages.size()));
8861 m_Map.m_vpImages.push_back(x: pNewImage);
8862 }
8863 }
8864 NewMap.m_vpImages.clear();
8865
8866 // modify indices
8867 static const auto &&s_ModifyAddIndex = [](int AddAmount) {
8868 return [AddAmount](int *pIndex) {
8869 if(*pIndex >= 0)
8870 *pIndex += AddAmount;
8871 };
8872 };
8873
8874 NewMap.ModifySoundIndex(pfnFunc: s_ModifyAddIndex(m_Map.m_vpSounds.size()));
8875 NewMap.ModifyEnvelopeIndex(pfnFunc: s_ModifyAddIndex(m_Map.m_vpEnvelopes.size()));
8876
8877 // transfer sounds
8878 for(const auto &pSound : NewMap.m_vpSounds)
8879 m_Map.m_vpSounds.push_back(x: pSound);
8880 NewMap.m_vpSounds.clear();
8881
8882 // transfer envelopes
8883 for(const auto &pEnvelope : NewMap.m_vpEnvelopes)
8884 m_Map.m_vpEnvelopes.push_back(x: pEnvelope);
8885 NewMap.m_vpEnvelopes.clear();
8886
8887 // transfer groups
8888 for(const auto &pGroup : NewMap.m_vpGroups)
8889 {
8890 if(pGroup != NewMap.m_pGameGroup)
8891 {
8892 pGroup->m_pMap = &m_Map;
8893 m_Map.m_vpGroups.push_back(x: pGroup);
8894 }
8895 }
8896 NewMap.m_vpGroups.clear();
8897
8898 auto IndexMap = SortImages();
8899
8900 if(!IgnoreHistory)
8901 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionAppendMap>(args: this, args&: pFileName, args&: Info, args&: IndexMap));
8902
8903 // all done \o/
8904 return true;
8905}
8906
8907void CEditor::UndoLastAction()
8908{
8909 if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS)
8910 m_ServerSettingsHistory.Undo();
8911 else if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES)
8912 m_EnvelopeEditorHistory.Undo();
8913 else
8914 m_EditorHistory.Undo();
8915}
8916
8917void CEditor::RedoLastAction()
8918{
8919 if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS)
8920 m_ServerSettingsHistory.Redo();
8921 else if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES)
8922 m_EnvelopeEditorHistory.Redo();
8923 else
8924 m_EditorHistory.Redo();
8925}
8926
8927void CEditor::AdjustBrushSpecialTiles(bool UseNextFree, int Adjust)
8928{
8929 // Adjust m_Number field of tune, switch and tele tiles by `Adjust` if `UseNextFree` is false
8930 // If true, then use the next free number instead
8931
8932 auto &&AdjustNumber = [Adjust](unsigned char &Number) {
8933 Number = ((Number + Adjust) - 1 + 255) % 255 + 1;
8934 };
8935
8936 for(auto &pLayer : m_pBrush->m_vpLayers)
8937 {
8938 if(pLayer->m_Type != LAYERTYPE_TILES)
8939 continue;
8940
8941 std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(r: pLayer);
8942
8943 // Only handle tele, switch and tune layers
8944 if(pLayerTiles->m_Tele)
8945 {
8946 int NextFreeTeleNumber = FindNextFreeTeleNumber();
8947 int NextFreeCPNumber = FindNextFreeTeleNumber(IsCheckpoint: true);
8948
8949 std::shared_ptr<CLayerTele> pTeleLayer = std::static_pointer_cast<CLayerTele>(r: pLayer);
8950 for(int y = 0; y < pTeleLayer->m_Height; y++)
8951 {
8952 for(int x = 0; x < pTeleLayer->m_Width; x++)
8953 {
8954 int i = y * pTeleLayer->m_Width + x;
8955 if(!IsValidTeleTile(Index: pTeleLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pTeleLayer->m_pTeleTile[i].m_Number))
8956 continue;
8957
8958 if(UseNextFree)
8959 {
8960 if(IsTeleTileCheckpoint(Index: pTeleLayer->m_pTiles[i].m_Index))
8961 pTeleLayer->m_pTeleTile[i].m_Number = NextFreeCPNumber;
8962 else
8963 pTeleLayer->m_pTeleTile[i].m_Number = NextFreeTeleNumber;
8964 }
8965 else
8966 AdjustNumber(pTeleLayer->m_pTeleTile[i].m_Number);
8967 }
8968 }
8969 }
8970 else if(pLayerTiles->m_Tune)
8971 {
8972 if(!UseNextFree)
8973 {
8974 std::shared_ptr<CLayerTune> pTuneLayer = std::static_pointer_cast<CLayerTune>(r: pLayer);
8975 for(int y = 0; y < pTuneLayer->m_Height; y++)
8976 {
8977 for(int x = 0; x < pTuneLayer->m_Width; x++)
8978 {
8979 int i = y * pTuneLayer->m_Width + x;
8980 if(!IsValidTuneTile(Index: pTuneLayer->m_pTiles[i].m_Index) || !pTuneLayer->m_pTuneTile[i].m_Number)
8981 continue;
8982
8983 AdjustNumber(pTuneLayer->m_pTuneTile[i].m_Number);
8984 }
8985 }
8986 }
8987 }
8988 else if(pLayerTiles->m_Switch)
8989 {
8990 int NextFreeNumber = FindNextFreeSwitchNumber();
8991
8992 std::shared_ptr<CLayerSwitch> pSwitchLayer = std::static_pointer_cast<CLayerSwitch>(r: pLayer);
8993 for(int y = 0; y < pSwitchLayer->m_Height; y++)
8994 {
8995 for(int x = 0; x < pSwitchLayer->m_Width; x++)
8996 {
8997 int i = y * pSwitchLayer->m_Width + x;
8998 if(!IsValidSwitchTile(Index: pSwitchLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pSwitchLayer->m_pSwitchTile[i].m_Number))
8999 continue;
9000
9001 if(UseNextFree)
9002 pSwitchLayer->m_pSwitchTile[i].m_Number = NextFreeNumber;
9003 else
9004 AdjustNumber(pSwitchLayer->m_pSwitchTile[i].m_Number);
9005 }
9006 }
9007 }
9008 }
9009}
9010
9011int CEditor::FindNextFreeSwitchNumber()
9012{
9013 int Number = -1;
9014
9015 for(int i = 1; i <= 255; i++)
9016 {
9017 if(!m_Map.m_pSwitchLayer->ContainsElementWithId(Id: i))
9018 {
9019 Number = i;
9020 break;
9021 }
9022 }
9023 return Number;
9024}
9025
9026int CEditor::FindNextFreeTeleNumber(bool IsCheckpoint)
9027{
9028 int Number = -1;
9029 for(int i = 1; i <= 255; i++)
9030 {
9031 if(!m_Map.m_pTeleLayer->ContainsElementWithId(Id: i, Checkpoint: IsCheckpoint))
9032 {
9033 Number = i;
9034 break;
9035 }
9036 }
9037 return Number;
9038}
9039
9040IEditor *CreateEditor() { return new CEditor; }
9041