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