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