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: ETimeFormat::HOURS, buffer: aCurrentTime, buffer_size: sizeof(aCurrentTime));
313 char aTotalTime[32];
314 str_time_float(secs: TotalTime, format: ETimeFormat::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 const 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()->InitMenuBackgroundPositions();
2806 const std::vector<vec2> &MenuBackgroundPositions = MapView()->ProofMode()->MenuBackgroundPositions();
2807 for(int i = 0; i < (int)MenuBackgroundPositions.size(); i++)
2808 {
2809 vec2 Pos = MenuBackgroundPositions[i];
2810 const void *pId = &MenuBackgroundPositions[i];
2811 Pos += MapView()->GetWorldOffset() - MenuBackgroundPositions[MapView()->ProofMode()->CurrentMenuProofIndex()];
2812 Pos.y -= 3.0f;
2813
2814 if(distance(a: Pos, b: m_MouseWorldNoParaPos) <= 20.0f)
2815 {
2816 Ui()->SetHotItem(pId);
2817
2818 if(i != MapView()->ProofMode()->CurrentMenuProofIndex() && Ui()->CheckActiveItem(pId))
2819 {
2820 if(!Ui()->MouseButton(Index: 0))
2821 {
2822 MapView()->ProofMode()->SetCurrentMenuProofIndex(i);
2823 MapView()->SetWorldOffset(MenuBackgroundPositions[i]);
2824 Ui()->SetActiveItem(nullptr);
2825 }
2826 }
2827 else if(Ui()->HotItem() == pId)
2828 {
2829 char aTooltipPrefix[32] = "Switch proof position to";
2830 if(i == MapView()->ProofMode()->CurrentMenuProofIndex())
2831 str_copy(dst&: aTooltipPrefix, src: "Current proof position at");
2832
2833 char aNumBuf[8];
2834 if(i < (TILE_TIME_CHECKPOINT_LAST - TILE_TIME_CHECKPOINT_FIRST))
2835 str_format(buffer: aNumBuf, buffer_size: sizeof(aNumBuf), format: "#%d", i + 1);
2836 else
2837 aNumBuf[0] = '\0';
2838
2839 char aTooltipPositions[128];
2840 str_format(buffer: aTooltipPositions, buffer_size: sizeof(aTooltipPositions), format: "%s %s", MapView()->ProofMode()->MenuBackgroundPositionName(MenuProofIndex: i), aNumBuf);
2841
2842 for(int k : MapView()->ProofMode()->MenuBackgroundCollisions(MenuProofIndex: i))
2843 {
2844 if(k == MapView()->ProofMode()->CurrentMenuProofIndex())
2845 str_copy(dst&: aTooltipPrefix, src: "Current proof position at");
2846
2847 Pos = MenuBackgroundPositions[k];
2848 Pos += MapView()->GetWorldOffset() - MenuBackgroundPositions[MapView()->ProofMode()->CurrentMenuProofIndex()];
2849 Pos.y -= 3.0f;
2850
2851 if(distance(a: Pos, b: m_MouseWorldNoParaPos) > 20.0f)
2852 continue;
2853
2854 if(i < (TILE_TIME_CHECKPOINT_LAST - TILE_TIME_CHECKPOINT_FIRST))
2855 str_format(buffer: aNumBuf, buffer_size: sizeof(aNumBuf), format: "#%d", k + 1);
2856 else
2857 aNumBuf[0] = '\0';
2858
2859 char aTooltipPositionsCopy[128];
2860 str_copy(dst&: aTooltipPositionsCopy, src: aTooltipPositions);
2861 str_format(buffer: aTooltipPositions, buffer_size: sizeof(aTooltipPositions), format: "%s, %s %s", aTooltipPositionsCopy, MapView()->ProofMode()->MenuBackgroundPositionName(MenuProofIndex: k), aNumBuf);
2862 }
2863 str_format(buffer: m_aTooltip, buffer_size: sizeof(m_aTooltip), format: "%s %s.", aTooltipPrefix, aTooltipPositions);
2864
2865 if(Ui()->MouseButton(Index: 0))
2866 Ui()->SetActiveItem(pId);
2867 }
2868 break;
2869 }
2870 }
2871 }
2872
2873 if(!Input()->ModifierIsPressed() && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
2874 {
2875 float PanSpeed = Input()->ShiftIsPressed() ? 200.0f : 64.0f;
2876 if(Input()->KeyPress(Key: KEY_A))
2877 MapView()->OffsetWorld(Offset: {-PanSpeed * m_MouseWorldScale, 0});
2878 else if(Input()->KeyPress(Key: KEY_D))
2879 MapView()->OffsetWorld(Offset: {PanSpeed * m_MouseWorldScale, 0});
2880 if(Input()->KeyPress(Key: KEY_W))
2881 MapView()->OffsetWorld(Offset: {0, -PanSpeed * m_MouseWorldScale});
2882 else if(Input()->KeyPress(Key: KEY_S))
2883 MapView()->OffsetWorld(Offset: {0, PanSpeed * m_MouseWorldScale});
2884 }
2885 }
2886
2887 if(Ui()->CheckActiveItem(pId: &m_MapEditorId) && m_pContainerPanned == nullptr)
2888 {
2889 // release mouse
2890 if(!Ui()->MouseButton(Index: 0))
2891 {
2892 if(s_Operation == OP_BRUSH_DRAW)
2893 {
2894 std::shared_ptr<IEditorAction> pAction = std::make_shared<CEditorBrushDrawAction>(args: Map(), args&: Map()->m_SelectedGroup);
2895
2896 if(!pAction->IsEmpty()) // Avoid recording tile draw action when placing quads only
2897 Map()->m_EditorHistory.RecordAction(pAction);
2898 }
2899
2900 s_Operation = OP_NONE;
2901 Ui()->SetActiveItem(nullptr);
2902 }
2903 }
2904
2905 if(!m_ShowPicker && Map()->SelectedGroup() && Map()->SelectedGroup()->m_UseClipping)
2906 {
2907 std::shared_ptr<CLayerGroup> pGameGroup = Map()->m_pGameGroup;
2908 pGameGroup->MapScreen();
2909
2910 CUIRect ClipRect;
2911 ClipRect.x = Map()->SelectedGroup()->m_ClipX;
2912 ClipRect.y = Map()->SelectedGroup()->m_ClipY;
2913 ClipRect.w = Map()->SelectedGroup()->m_ClipW;
2914 ClipRect.h = Map()->SelectedGroup()->m_ClipH;
2915 ClipRect.DrawOutline(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
2916 }
2917
2918 if(!m_ShowPicker)
2919 MapView()->ProofMode()->RenderScreenSizes();
2920
2921 if(!m_ShowPicker && m_ShowEnvelopePreview && m_ActiveEnvelopePreview != EEnvelopePreview::NONE)
2922 {
2923 const std::shared_ptr<CLayer> pSelectedLayer = Map()->SelectedLayer(Index: 0);
2924 if(pSelectedLayer != nullptr && pSelectedLayer->m_Type == LAYERTYPE_QUADS)
2925 {
2926 DoQuadEnvelopes(pLayerQuads: static_cast<const CLayerQuads *>(pSelectedLayer.get()));
2927 }
2928 m_ActiveEnvelopePreview = EEnvelopePreview::NONE;
2929 }
2930
2931 Ui()->MapScreen();
2932}
2933
2934void CEditor::UpdateHotQuadPoint(const CLayerQuads *pLayer)
2935{
2936 const vec2 MouseWorld = Ui()->MouseWorldPos();
2937
2938 float MinDist = 500.0f;
2939 const void *pMinPointId = nullptr;
2940
2941 const auto UpdateMinimum = [&](vec2 Position, const void *pId) {
2942 const float CurrDist = length_squared(a: (Position - MouseWorld) / m_MouseWorldScale);
2943 if(CurrDist < MinDist)
2944 {
2945 MinDist = CurrDist;
2946 pMinPointId = pId;
2947 return true;
2948 }
2949 return false;
2950 };
2951
2952 for(const CQuad &Quad : pLayer->m_vQuads)
2953 {
2954 if(m_ShowEnvelopePreview &&
2955 m_ActiveEnvelopePreview != EEnvelopePreview::NONE &&
2956 Quad.m_PosEnv >= 0 &&
2957 Quad.m_PosEnv < (int)Map()->m_vpEnvelopes.size())
2958 {
2959 for(const auto &EnvPoint : Map()->m_vpEnvelopes[Quad.m_PosEnv]->m_vPoints)
2960 {
2961 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]));
2962 if(UpdateMinimum(Position, &EnvPoint) && Ui()->ActiveItem() == nullptr)
2963 {
2964 Map()->m_CurrentQuadIndex = &Quad - pLayer->m_vQuads.data();
2965 }
2966 }
2967 }
2968
2969 for(const auto &Point : Quad.m_aPoints)
2970 {
2971 UpdateMinimum(vec2(fx2f(v: Point.x), fx2f(v: Point.y)), &Point);
2972 }
2973 }
2974
2975 if(pMinPointId != nullptr)
2976 {
2977 Ui()->SetHotItem(pMinPointId);
2978 }
2979}
2980
2981void CEditor::DoColorPickerButton(const void *pId, const CUIRect *pRect, ColorRGBA Color, const std::function<void(ColorRGBA Color)> &SetColor)
2982{
2983 CUIRect ColorRect;
2984 pRect->Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * Ui()->ButtonColorMul(pId)), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
2985 pRect->Margin(Cut: 1.0f, pOtherRect: &ColorRect);
2986 ColorRect.Draw(Color, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
2987
2988 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.");
2989 if(Input()->ShiftIsPressed())
2990 {
2991 if(ButtonResult == 1)
2992 {
2993 std::string Clipboard = Input()->GetClipboardText();
2994 if(Clipboard[0] == '#' || Clipboard[0] == '$') // ignore leading # (web color format) and $ (console color format)
2995 Clipboard = Clipboard.substr(pos: 1);
2996 if(str_isallnum_hex(str: Clipboard.c_str()))
2997 {
2998 std::optional<ColorRGBA> ParsedColor = color_parse<ColorRGBA>(pStr: Clipboard.c_str());
2999 if(ParsedColor)
3000 {
3001 m_ColorPickerPopupContext.m_State = EEditState::ONE_GO;
3002 SetColor(ParsedColor.value());
3003 }
3004 }
3005 }
3006 else if(ButtonResult == 2)
3007 {
3008 char aClipboard[9];
3009 str_format(buffer: aClipboard, buffer_size: sizeof(aClipboard), format: "%08X", Color.PackAlphaLast());
3010 Input()->SetClipboardText(aClipboard);
3011 }
3012 }
3013 else if(ButtonResult > 0)
3014 {
3015 if(m_ColorPickerPopupContext.m_ColorMode == CUi::SColorPickerPopupContext::MODE_UNSET)
3016 m_ColorPickerPopupContext.m_ColorMode = CUi::SColorPickerPopupContext::MODE_RGBA;
3017 m_ColorPickerPopupContext.m_RgbaColor = Color;
3018 m_ColorPickerPopupContext.m_HslaColor = color_cast<ColorHSLA>(rgb: Color);
3019 m_ColorPickerPopupContext.m_HsvaColor = color_cast<ColorHSVA>(hsl: m_ColorPickerPopupContext.m_HslaColor);
3020 m_ColorPickerPopupContext.m_Alpha = true;
3021 m_pColorPickerPopupActiveId = pId;
3022 Ui()->ShowPopupColorPicker(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext: &m_ColorPickerPopupContext);
3023 }
3024
3025 if(Ui()->IsPopupOpen(pId: &m_ColorPickerPopupContext))
3026 {
3027 if(m_pColorPickerPopupActiveId == pId)
3028 SetColor(m_ColorPickerPopupContext.m_RgbaColor);
3029 }
3030 else
3031 {
3032 m_pColorPickerPopupActiveId = nullptr;
3033 if(m_ColorPickerPopupContext.m_State == EEditState::EDITING)
3034 {
3035 m_ColorPickerPopupContext.m_State = EEditState::END;
3036 SetColor(m_ColorPickerPopupContext.m_RgbaColor);
3037 m_ColorPickerPopupContext.m_State = EEditState::NONE;
3038 }
3039 }
3040}
3041
3042bool CEditor::IsAllowPlaceUnusedTiles() const
3043{
3044 // explicit allow and implicit allow
3045 return m_AllowPlaceUnusedTiles != EUnusedEntities::NOT_ALLOWED;
3046}
3047
3048void CEditor::RenderLayers(CUIRect LayersBox)
3049{
3050 const float RowHeight = 12.0f;
3051 char aBuf[64];
3052
3053 CUIRect UnscrolledLayersBox = LayersBox;
3054
3055 static CScrollRegion s_ScrollRegion;
3056 vec2 ScrollOffset(0.0f, 0.0f);
3057 CScrollRegionParams ScrollParams;
3058 ScrollParams.m_ScrollbarWidth = 10.0f;
3059 ScrollParams.m_ScrollbarMargin = 3.0f;
3060 ScrollParams.m_ScrollUnit = RowHeight * 5.0f;
3061 s_ScrollRegion.Begin(pClipRect: &LayersBox, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
3062 LayersBox.y += ScrollOffset.y;
3063
3064 enum
3065 {
3066 OP_NONE = 0,
3067 OP_CLICK,
3068 OP_LAYER_DRAG,
3069 OP_GROUP_DRAG
3070 };
3071 static int s_Operation = OP_NONE;
3072 static int s_PreviousOperation = OP_NONE;
3073 static const void *s_pDraggedButton = nullptr;
3074 static float s_InitialMouseY = 0;
3075 static float s_InitialCutHeight = 0;
3076 constexpr float MinDragDistance = 5.0f;
3077 int GroupAfterDraggedLayer = -1;
3078 int LayerAfterDraggedLayer = -1;
3079 bool DraggedPositionFound = false;
3080 bool MoveLayers = false;
3081 bool MoveGroup = false;
3082 bool StartDragLayer = false;
3083 bool StartDragGroup = false;
3084 std::vector<int> vButtonsPerGroup;
3085
3086 auto SetOperation = [](int Operation) {
3087 if(Operation != s_Operation)
3088 {
3089 s_PreviousOperation = s_Operation;
3090 s_Operation = Operation;
3091 if(Operation == OP_NONE)
3092 {
3093 s_pDraggedButton = nullptr;
3094 }
3095 }
3096 };
3097
3098 vButtonsPerGroup.reserve(n: Map()->m_vpGroups.size());
3099 for(const std::shared_ptr<CLayerGroup> &pGroup : Map()->m_vpGroups)
3100 {
3101 vButtonsPerGroup.push_back(x: pGroup->m_vpLayers.size() + 1);
3102 }
3103
3104 if(s_pDraggedButton != nullptr && Ui()->ActiveItem() != s_pDraggedButton)
3105 {
3106 SetOperation(OP_NONE);
3107 }
3108
3109 if(s_Operation == OP_LAYER_DRAG || s_Operation == OP_GROUP_DRAG)
3110 {
3111 float MinDraggableValue = UnscrolledLayersBox.y;
3112 float MaxDraggableValue = MinDraggableValue;
3113 for(int NumButtons : vButtonsPerGroup)
3114 {
3115 MaxDraggableValue += NumButtons * (RowHeight + 2.0f) + 5.0f;
3116 }
3117 MaxDraggableValue += ScrollOffset.y;
3118
3119 if(s_Operation == OP_GROUP_DRAG)
3120 {
3121 MaxDraggableValue -= vButtonsPerGroup[Map()->m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f;
3122 }
3123 else if(s_Operation == OP_LAYER_DRAG)
3124 {
3125 MinDraggableValue += RowHeight + 2.0f;
3126 MaxDraggableValue -= Map()->m_vSelectedLayers.size() * (RowHeight + 2.0f) + 5.0f;
3127 }
3128
3129 UnscrolledLayersBox.HSplitTop(Cut: s_InitialCutHeight, pTop: nullptr, pBottom: &UnscrolledLayersBox);
3130 UnscrolledLayersBox.y -= s_InitialMouseY - Ui()->MouseY();
3131
3132 UnscrolledLayersBox.y = std::clamp(val: UnscrolledLayersBox.y, lo: MinDraggableValue, hi: MaxDraggableValue);
3133
3134 UnscrolledLayersBox.w = LayersBox.w;
3135 }
3136
3137 static bool s_ScrollToSelectionNext = false;
3138 const bool ScrollToSelection = LayerSelector()->SelectByTile() || s_ScrollToSelectionNext;
3139 s_ScrollToSelectionNext = false;
3140
3141 // render layers
3142 for(int g = 0; g < (int)Map()->m_vpGroups.size(); g++)
3143 {
3144 if(s_Operation == OP_LAYER_DRAG && g > 0 && !DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight / 2)
3145 {
3146 DraggedPositionFound = true;
3147 GroupAfterDraggedLayer = g;
3148
3149 LayerAfterDraggedLayer = Map()->m_vpGroups[g - 1]->m_vpLayers.size();
3150
3151 CUIRect Slot;
3152 LayersBox.HSplitTop(Cut: Map()->m_vSelectedLayers.size() * (RowHeight + 2.0f), pTop: &Slot, pBottom: &LayersBox);
3153 s_ScrollRegion.AddRect(Rect: Slot);
3154 }
3155
3156 CUIRect Slot, VisibleToggle;
3157 if(s_Operation == OP_GROUP_DRAG)
3158 {
3159 if(g == Map()->m_SelectedGroup)
3160 {
3161 UnscrolledLayersBox.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: &UnscrolledLayersBox);
3162 UnscrolledLayersBox.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &UnscrolledLayersBox);
3163 }
3164 else if(!DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight * vButtonsPerGroup[g] / 2 + 3.0f)
3165 {
3166 DraggedPositionFound = true;
3167 GroupAfterDraggedLayer = g;
3168
3169 CUIRect TmpSlot;
3170 if(Map()->m_vpGroups[Map()->m_SelectedGroup]->m_Collapse)
3171 LayersBox.HSplitTop(Cut: RowHeight + 7.0f, pTop: &TmpSlot, pBottom: &LayersBox);
3172 else
3173 LayersBox.HSplitTop(Cut: vButtonsPerGroup[Map()->m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f, pTop: &TmpSlot, pBottom: &LayersBox);
3174 s_ScrollRegion.AddRect(Rect: TmpSlot, ShouldScrollHere: false);
3175 }
3176 }
3177 if(s_Operation != OP_GROUP_DRAG || g != Map()->m_SelectedGroup)
3178 {
3179 LayersBox.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: &LayersBox);
3180
3181 CUIRect TmpRect;
3182 LayersBox.HSplitTop(Cut: 2.0f, pTop: &TmpRect, pBottom: &LayersBox);
3183 s_ScrollRegion.AddRect(Rect: TmpRect);
3184 }
3185
3186 if(s_ScrollRegion.AddRect(Rect: Slot))
3187 {
3188 Slot.VSplitLeft(Cut: 15.0f, pLeft: &VisibleToggle, pRight: &Slot);
3189
3190 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);
3191 if(MouseClick == 1)
3192 {
3193 Map()->m_vpGroups[g]->m_Visible = !Map()->m_vpGroups[g]->m_Visible;
3194 }
3195 else if(MouseClick == 2)
3196 {
3197 if(Input()->ShiftIsPressed())
3198 {
3199 if(g != Map()->m_SelectedGroup)
3200 Map()->SelectLayer(LayerIndex: 0, GroupIndex: g);
3201 }
3202
3203 int NumActive = 0;
3204 for(auto &Group : Map()->m_vpGroups)
3205 {
3206 if(Group == Map()->m_vpGroups[g])
3207 {
3208 Group->m_Visible = true;
3209 continue;
3210 }
3211
3212 if(Group->m_Visible)
3213 {
3214 Group->m_Visible = false;
3215 NumActive++;
3216 }
3217 }
3218 if(NumActive == 0)
3219 {
3220 for(auto &Group : Map()->m_vpGroups)
3221 {
3222 Group->m_Visible = true;
3223 }
3224 }
3225 }
3226
3227 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "#%d %s", g, Map()->m_vpGroups[g]->m_aName);
3228
3229 bool Clicked;
3230 bool Abrupted;
3231 if(int Result = DoButton_DraggableEx(pId: Map()->m_vpGroups[g].get(), pText: aBuf, Checked: g == Map()->m_SelectedGroup, pRect: &Slot, pClicked: &Clicked, pAbrupted: &Abrupted,
3232 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))
3233 {
3234 if(s_Operation == OP_NONE)
3235 {
3236 s_InitialMouseY = Ui()->MouseY();
3237 s_InitialCutHeight = s_InitialMouseY - UnscrolledLayersBox.y;
3238 SetOperation(OP_CLICK);
3239
3240 if(g != Map()->m_SelectedGroup)
3241 Map()->SelectLayer(LayerIndex: 0, GroupIndex: g);
3242 }
3243
3244 if(Abrupted)
3245 {
3246 SetOperation(OP_NONE);
3247 }
3248
3249 if(s_Operation == OP_CLICK && absolute(a: Ui()->MouseY() - s_InitialMouseY) > MinDragDistance)
3250 {
3251 StartDragGroup = true;
3252 s_pDraggedButton = Map()->m_vpGroups[g].get();
3253 }
3254
3255 if(s_Operation == OP_CLICK && Clicked)
3256 {
3257 if(g != Map()->m_SelectedGroup)
3258 Map()->SelectLayer(LayerIndex: 0, GroupIndex: g);
3259
3260 if(Input()->ShiftIsPressed() && Map()->m_SelectedGroup == g)
3261 {
3262 Map()->m_vSelectedLayers.clear();
3263 for(size_t i = 0; i < Map()->m_vpGroups[g]->m_vpLayers.size(); i++)
3264 {
3265 Map()->AddSelectedLayer(LayerIndex: i);
3266 }
3267 }
3268
3269 if(Result == 2)
3270 {
3271 static SPopupMenuId s_PopupGroupId;
3272 Ui()->DoPopupMenu(pId: &s_PopupGroupId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 145, Height: 256, pContext: this, pfnFunc: PopupGroup);
3273 }
3274
3275 if(!Map()->m_vpGroups[g]->m_vpLayers.empty() && Ui()->DoDoubleClickLogic(pId: Map()->m_vpGroups[g].get()))
3276 Map()->m_vpGroups[g]->m_Collapse ^= 1;
3277
3278 SetOperation(OP_NONE);
3279 }
3280
3281 if(s_Operation == OP_GROUP_DRAG && Clicked)
3282 MoveGroup = true;
3283 }
3284 else if(s_pDraggedButton == Map()->m_vpGroups[g].get())
3285 {
3286 SetOperation(OP_NONE);
3287 }
3288 }
3289
3290 for(int i = 0; i < (int)Map()->m_vpGroups[g]->m_vpLayers.size(); i++)
3291 {
3292 if(Map()->m_vpGroups[g]->m_Collapse)
3293 continue;
3294
3295 bool IsLayerSelected = false;
3296 if(Map()->m_SelectedGroup == g)
3297 {
3298 for(const auto &Selected : Map()->m_vSelectedLayers)
3299 {
3300 if(Selected == i)
3301 {
3302 IsLayerSelected = true;
3303 break;
3304 }
3305 }
3306 }
3307
3308 if(s_Operation == OP_GROUP_DRAG && g == Map()->m_SelectedGroup)
3309 {
3310 UnscrolledLayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &UnscrolledLayersBox);
3311 }
3312 else if(s_Operation == OP_LAYER_DRAG)
3313 {
3314 if(IsLayerSelected)
3315 {
3316 UnscrolledLayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &UnscrolledLayersBox);
3317 }
3318 else
3319 {
3320 if(!DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight / 2)
3321 {
3322 DraggedPositionFound = true;
3323 GroupAfterDraggedLayer = g + 1;
3324 LayerAfterDraggedLayer = i;
3325 for(size_t j = 0; j < Map()->m_vSelectedLayers.size(); j++)
3326 {
3327 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: nullptr, pBottom: &LayersBox);
3328 s_ScrollRegion.AddRect(Rect: Slot);
3329 }
3330 }
3331 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &LayersBox);
3332 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: ScrollToSelection && IsLayerSelected))
3333 continue;
3334 }
3335 }
3336 else
3337 {
3338 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &LayersBox);
3339 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: ScrollToSelection && IsLayerSelected))
3340 continue;
3341 }
3342
3343 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
3344
3345 CUIRect Button;
3346 Slot.VSplitLeft(Cut: 12.0f, pLeft: nullptr, pRight: &Slot);
3347 Slot.VSplitLeft(Cut: 15.0f, pLeft: &VisibleToggle, pRight: &Button);
3348
3349 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);
3350 if(MouseClick == 1)
3351 {
3352 Map()->m_vpGroups[g]->m_vpLayers[i]->m_Visible = !Map()->m_vpGroups[g]->m_vpLayers[i]->m_Visible;
3353 }
3354 else if(MouseClick == 2)
3355 {
3356 if(Input()->ShiftIsPressed())
3357 {
3358 if(!IsLayerSelected)
3359 Map()->SelectLayer(LayerIndex: i, GroupIndex: g);
3360 }
3361
3362 int NumActive = 0;
3363 for(auto &Layer : Map()->m_vpGroups[g]->m_vpLayers)
3364 {
3365 if(Layer == Map()->m_vpGroups[g]->m_vpLayers[i])
3366 {
3367 Layer->m_Visible = true;
3368 continue;
3369 }
3370
3371 if(Layer->m_Visible)
3372 {
3373 Layer->m_Visible = false;
3374 NumActive++;
3375 }
3376 }
3377 if(NumActive == 0)
3378 {
3379 for(auto &Layer : Map()->m_vpGroups[g]->m_vpLayers)
3380 {
3381 Layer->m_Visible = true;
3382 }
3383 }
3384 }
3385
3386 if(Map()->m_vpGroups[g]->m_vpLayers[i]->m_aName[0])
3387 str_copy(dst&: aBuf, src: Map()->m_vpGroups[g]->m_vpLayers[i]->m_aName);
3388 else
3389 {
3390 if(Map()->m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_TILES)
3391 {
3392 std::shared_ptr<CLayerTiles> pTiles = std::static_pointer_cast<CLayerTiles>(r: Map()->m_vpGroups[g]->m_vpLayers[i]);
3393 str_copy(dst&: aBuf, src: pTiles->m_Image >= 0 ? Map()->m_vpImages[pTiles->m_Image]->m_aName : "Tiles");
3394 }
3395 else if(Map()->m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_QUADS)
3396 {
3397 std::shared_ptr<CLayerQuads> pQuads = std::static_pointer_cast<CLayerQuads>(r: Map()->m_vpGroups[g]->m_vpLayers[i]);
3398 str_copy(dst&: aBuf, src: pQuads->m_Image >= 0 ? Map()->m_vpImages[pQuads->m_Image]->m_aName : "Quads");
3399 }
3400 else if(Map()->m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_SOUNDS)
3401 {
3402 std::shared_ptr<CLayerSounds> pSounds = std::static_pointer_cast<CLayerSounds>(r: Map()->m_vpGroups[g]->m_vpLayers[i]);
3403 str_copy(dst&: aBuf, src: pSounds->m_Sound >= 0 ? Map()->m_vpSounds[pSounds->m_Sound]->m_aName : "Sounds");
3404 }
3405 }
3406
3407 int Checked = IsLayerSelected ? 1 : 0;
3408 if(Map()->m_vpGroups[g]->m_vpLayers[i]->IsEntitiesLayer())
3409 {
3410 Checked += 6;
3411 }
3412
3413 bool Clicked;
3414 bool Abrupted;
3415 if(int Result = DoButton_DraggableEx(pId: Map()->m_vpGroups[g]->m_vpLayers[i].get(), pText: aBuf, Checked, pRect: &Button, pClicked: &Clicked, pAbrupted: &Abrupted,
3416 Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Select layer. Hold shift to select multiple.", Corners: IGraphics::CORNER_R))
3417 {
3418 if(s_Operation == OP_NONE)
3419 {
3420 s_InitialMouseY = Ui()->MouseY();
3421 s_InitialCutHeight = s_InitialMouseY - UnscrolledLayersBox.y;
3422
3423 SetOperation(OP_CLICK);
3424
3425 if(!Input()->ShiftIsPressed() && !IsLayerSelected)
3426 {
3427 Map()->SelectLayer(LayerIndex: i, GroupIndex: g);
3428 }
3429 }
3430
3431 if(Abrupted)
3432 {
3433 SetOperation(OP_NONE);
3434 }
3435
3436 if(s_Operation == OP_CLICK && absolute(a: Ui()->MouseY() - s_InitialMouseY) > MinDragDistance)
3437 {
3438 bool EntitiesLayerSelected = false;
3439 for(int k : Map()->m_vSelectedLayers)
3440 {
3441 if(Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers[k]->IsEntitiesLayer())
3442 EntitiesLayerSelected = true;
3443 }
3444
3445 if(!EntitiesLayerSelected)
3446 StartDragLayer = true;
3447
3448 s_pDraggedButton = Map()->m_vpGroups[g]->m_vpLayers[i].get();
3449 }
3450
3451 if(s_Operation == OP_CLICK && Clicked)
3452 {
3453 static SLayerPopupContext s_LayerPopupContext = {};
3454 s_LayerPopupContext.m_pEditor = this;
3455 if(Result == 1)
3456 {
3457 if(Input()->ShiftIsPressed() && Map()->m_SelectedGroup == g)
3458 {
3459 auto Position = std::find(first: Map()->m_vSelectedLayers.begin(), last: Map()->m_vSelectedLayers.end(), val: i);
3460 if(Position != Map()->m_vSelectedLayers.end())
3461 Map()->m_vSelectedLayers.erase(position: Position);
3462 else
3463 Map()->AddSelectedLayer(LayerIndex: i);
3464 }
3465 else if(!Input()->ShiftIsPressed())
3466 {
3467 Map()->SelectLayer(LayerIndex: i, GroupIndex: g);
3468 }
3469 }
3470 else if(Result == 2)
3471 {
3472 s_LayerPopupContext.m_vpLayers.clear();
3473 s_LayerPopupContext.m_vLayerIndices.clear();
3474
3475 if(!IsLayerSelected)
3476 {
3477 Map()->SelectLayer(LayerIndex: i, GroupIndex: g);
3478 }
3479
3480 if(Map()->m_vSelectedLayers.size() > 1)
3481 {
3482 // move right clicked layer to first index to render correct popup
3483 if(Map()->m_vSelectedLayers[0] != i)
3484 {
3485 auto Position = std::find(first: Map()->m_vSelectedLayers.begin(), last: Map()->m_vSelectedLayers.end(), val: i);
3486 std::swap(a&: Map()->m_vSelectedLayers[0], b&: *Position);
3487 }
3488
3489 bool AllTile = true;
3490 for(size_t j = 0; AllTile && j < Map()->m_vSelectedLayers.size(); j++)
3491 {
3492 int LayerIndex = Map()->m_vSelectedLayers[j];
3493 if(Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers[LayerIndex]->m_Type == LAYERTYPE_TILES)
3494 {
3495 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]]));
3496 s_LayerPopupContext.m_vLayerIndices.push_back(x: LayerIndex);
3497 }
3498 else
3499 AllTile = false;
3500 }
3501
3502 // Don't allow editing if all selected layers are not tile layers
3503 if(!AllTile)
3504 {
3505 s_LayerPopupContext.m_vpLayers.clear();
3506 s_LayerPopupContext.m_vLayerIndices.clear();
3507 }
3508 }
3509
3510 Ui()->DoPopupMenu(pId: &s_LayerPopupContext, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 150, Height: 300, pContext: &s_LayerPopupContext, pfnFunc: PopupLayer);
3511 }
3512
3513 SetOperation(OP_NONE);
3514 }
3515
3516 if(s_Operation == OP_LAYER_DRAG && Clicked)
3517 {
3518 MoveLayers = true;
3519 }
3520 }
3521 else if(s_pDraggedButton == Map()->m_vpGroups[g]->m_vpLayers[i].get())
3522 {
3523 SetOperation(OP_NONE);
3524 }
3525 }
3526
3527 if(s_Operation != OP_GROUP_DRAG || g != Map()->m_SelectedGroup)
3528 {
3529 LayersBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &LayersBox);
3530 s_ScrollRegion.AddRect(Rect: Slot);
3531 }
3532 }
3533
3534 if(!DraggedPositionFound && s_Operation == OP_LAYER_DRAG)
3535 {
3536 GroupAfterDraggedLayer = Map()->m_vpGroups.size();
3537 LayerAfterDraggedLayer = Map()->m_vpGroups[GroupAfterDraggedLayer - 1]->m_vpLayers.size();
3538
3539 CUIRect TmpSlot;
3540 LayersBox.HSplitTop(Cut: Map()->m_vSelectedLayers.size() * (RowHeight + 2.0f), pTop: &TmpSlot, pBottom: &LayersBox);
3541 s_ScrollRegion.AddRect(Rect: TmpSlot);
3542 }
3543
3544 if(!DraggedPositionFound && s_Operation == OP_GROUP_DRAG)
3545 {
3546 GroupAfterDraggedLayer = Map()->m_vpGroups.size();
3547
3548 CUIRect TmpSlot;
3549 if(Map()->m_vpGroups[Map()->m_SelectedGroup]->m_Collapse)
3550 LayersBox.HSplitTop(Cut: RowHeight + 7.0f, pTop: &TmpSlot, pBottom: &LayersBox);
3551 else
3552 LayersBox.HSplitTop(Cut: vButtonsPerGroup[Map()->m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f, pTop: &TmpSlot, pBottom: &LayersBox);
3553 s_ScrollRegion.AddRect(Rect: TmpSlot, ShouldScrollHere: false);
3554 }
3555
3556 if(MoveLayers && 1 <= GroupAfterDraggedLayer && GroupAfterDraggedLayer <= (int)Map()->m_vpGroups.size())
3557 {
3558 std::vector<std::shared_ptr<CLayer>> &vpNewGroupLayers = Map()->m_vpGroups[GroupAfterDraggedLayer - 1]->m_vpLayers;
3559 if(0 <= LayerAfterDraggedLayer && LayerAfterDraggedLayer <= (int)vpNewGroupLayers.size())
3560 {
3561 std::vector<std::shared_ptr<CLayer>> vpSelectedLayers;
3562 std::vector<std::shared_ptr<CLayer>> &vpSelectedGroupLayers = Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers;
3563 std::shared_ptr<CLayer> pNextLayer = nullptr;
3564 if(LayerAfterDraggedLayer < (int)vpNewGroupLayers.size())
3565 pNextLayer = vpNewGroupLayers[LayerAfterDraggedLayer];
3566
3567 std::sort(first: Map()->m_vSelectedLayers.begin(), last: Map()->m_vSelectedLayers.end(), comp: std::greater<>());
3568 for(int k : Map()->m_vSelectedLayers)
3569 {
3570 vpSelectedLayers.insert(position: vpSelectedLayers.begin(), x: vpSelectedGroupLayers[k]);
3571 }
3572 for(int k : Map()->m_vSelectedLayers)
3573 {
3574 vpSelectedGroupLayers.erase(position: vpSelectedGroupLayers.begin() + k);
3575 }
3576
3577 auto InsertPosition = std::find(first: vpNewGroupLayers.begin(), last: vpNewGroupLayers.end(), val: pNextLayer);
3578 int InsertPositionIndex = InsertPosition - vpNewGroupLayers.begin();
3579 vpNewGroupLayers.insert(position: InsertPosition, first: vpSelectedLayers.begin(), last: vpSelectedLayers.end());
3580
3581 int NumSelectedLayers = Map()->m_vSelectedLayers.size();
3582 Map()->m_vSelectedLayers.clear();
3583 for(int i = 0; i < NumSelectedLayers; i++)
3584 Map()->m_vSelectedLayers.push_back(x: InsertPositionIndex + i);
3585
3586 Map()->m_SelectedGroup = GroupAfterDraggedLayer - 1;
3587 Map()->OnModify();
3588 }
3589 }
3590
3591 if(MoveGroup && 0 <= GroupAfterDraggedLayer && GroupAfterDraggedLayer <= (int)Map()->m_vpGroups.size())
3592 {
3593 std::shared_ptr<CLayerGroup> pSelectedGroup = Map()->m_vpGroups[Map()->m_SelectedGroup];
3594 std::shared_ptr<CLayerGroup> pNextGroup = nullptr;
3595 if(GroupAfterDraggedLayer < (int)Map()->m_vpGroups.size())
3596 pNextGroup = Map()->m_vpGroups[GroupAfterDraggedLayer];
3597
3598 Map()->m_vpGroups.erase(position: Map()->m_vpGroups.begin() + Map()->m_SelectedGroup);
3599
3600 auto InsertPosition = std::find(first: Map()->m_vpGroups.begin(), last: Map()->m_vpGroups.end(), val: pNextGroup);
3601 Map()->m_vpGroups.insert(position: InsertPosition, x: pSelectedGroup);
3602
3603 auto Pos = std::find(first: Map()->m_vpGroups.begin(), last: Map()->m_vpGroups.end(), val: pSelectedGroup);
3604 Map()->m_SelectedGroup = Pos - Map()->m_vpGroups.begin();
3605
3606 Map()->OnModify();
3607 }
3608
3609 static int s_InitialGroupIndex;
3610 static std::vector<int> s_vInitialLayerIndices;
3611
3612 if(MoveLayers || MoveGroup)
3613 {
3614 SetOperation(OP_NONE);
3615 }
3616 if(StartDragLayer)
3617 {
3618 SetOperation(OP_LAYER_DRAG);
3619 s_InitialGroupIndex = Map()->m_SelectedGroup;
3620 s_vInitialLayerIndices = std::vector(Map()->m_vSelectedLayers);
3621 }
3622 if(StartDragGroup)
3623 {
3624 s_InitialGroupIndex = Map()->m_SelectedGroup;
3625 SetOperation(OP_GROUP_DRAG);
3626 }
3627
3628 if(s_Operation == OP_LAYER_DRAG || s_Operation == OP_GROUP_DRAG)
3629 {
3630 if(s_pDraggedButton == nullptr)
3631 {
3632 SetOperation(OP_NONE);
3633 }
3634 else
3635 {
3636 s_ScrollRegion.DoEdgeScrolling();
3637 Ui()->SetActiveItem(s_pDraggedButton);
3638 }
3639 }
3640
3641 if(Input()->KeyPress(Key: KEY_DOWN) && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && CLineInput::GetActiveInput() == nullptr && s_Operation == OP_NONE)
3642 {
3643 if(Input()->ShiftIsPressed())
3644 {
3645 if(Map()->m_vSelectedLayers[Map()->m_vSelectedLayers.size() - 1] < (int)Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers.size() - 1)
3646 Map()->AddSelectedLayer(LayerIndex: Map()->m_vSelectedLayers[Map()->m_vSelectedLayers.size() - 1] + 1);
3647 }
3648 else
3649 {
3650 Map()->SelectNextLayer();
3651 }
3652 s_ScrollToSelectionNext = true;
3653 }
3654 if(Input()->KeyPress(Key: KEY_UP) && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && CLineInput::GetActiveInput() == nullptr && s_Operation == OP_NONE)
3655 {
3656 if(Input()->ShiftIsPressed())
3657 {
3658 if(Map()->m_vSelectedLayers[Map()->m_vSelectedLayers.size() - 1] > 0)
3659 Map()->AddSelectedLayer(LayerIndex: Map()->m_vSelectedLayers[Map()->m_vSelectedLayers.size() - 1] - 1);
3660 }
3661 else
3662 {
3663 Map()->SelectPreviousLayer();
3664 }
3665
3666 s_ScrollToSelectionNext = true;
3667 }
3668
3669 CUIRect AddGroupButton, CollapseAllButton;
3670 LayersBox.HSplitTop(Cut: RowHeight + 1.0f, pTop: &AddGroupButton, pBottom: &LayersBox);
3671 if(s_ScrollRegion.AddRect(Rect: AddGroupButton))
3672 {
3673 AddGroupButton.HSplitTop(Cut: RowHeight, pTop: &AddGroupButton, pBottom: nullptr);
3674 if(DoButton_Editor(pId: &m_QuickActionAddGroup, pText: m_QuickActionAddGroup.Label(), Checked: 0, pRect: &AddGroupButton, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionAddGroup.Description()))
3675 {
3676 m_QuickActionAddGroup.Call();
3677 }
3678 }
3679
3680 LayersBox.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &LayersBox);
3681 LayersBox.HSplitTop(Cut: RowHeight + 1.0f, pTop: &CollapseAllButton, pBottom: &LayersBox);
3682 if(s_ScrollRegion.AddRect(Rect: CollapseAllButton))
3683 {
3684 size_t TotalCollapsed = 0;
3685 for(const auto &pGroup : Map()->m_vpGroups)
3686 {
3687 if(pGroup->m_vpLayers.empty() || pGroup->m_Collapse)
3688 {
3689 TotalCollapsed++;
3690 }
3691 }
3692
3693 const char *pActionText = TotalCollapsed == Map()->m_vpGroups.size() ? "Expand all" : "Collapse all";
3694
3695 CollapseAllButton.HSplitTop(Cut: RowHeight, pTop: &CollapseAllButton, pBottom: nullptr);
3696 static int s_CollapseAllButton = 0;
3697 if(DoButton_Editor(pId: &s_CollapseAllButton, pText: pActionText, Checked: 0, pRect: &CollapseAllButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Expand or collapse all groups."))
3698 {
3699 for(const auto &pGroup : Map()->m_vpGroups)
3700 {
3701 if(TotalCollapsed == Map()->m_vpGroups.size())
3702 pGroup->m_Collapse = false;
3703 else if(!pGroup->m_vpLayers.empty())
3704 pGroup->m_Collapse = true;
3705 }
3706 }
3707 }
3708
3709 s_ScrollRegion.End();
3710
3711 if(s_Operation == OP_NONE)
3712 {
3713 if(s_PreviousOperation == OP_GROUP_DRAG)
3714 {
3715 s_PreviousOperation = OP_NONE;
3716 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));
3717 }
3718 else if(s_PreviousOperation == OP_LAYER_DRAG)
3719 {
3720 if(s_InitialGroupIndex != Map()->m_SelectedGroup)
3721 {
3722 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));
3723 }
3724 else
3725 {
3726 std::vector<std::shared_ptr<IEditorAction>> vpActions;
3727 std::vector<int> vLayerIndices = Map()->m_vSelectedLayers;
3728 std::sort(first: vLayerIndices.begin(), last: vLayerIndices.end());
3729 std::sort(first: s_vInitialLayerIndices.begin(), last: s_vInitialLayerIndices.end());
3730 for(int k = 0; k < (int)vLayerIndices.size(); k++)
3731 {
3732 int LayerIndex = vLayerIndices[k];
3733 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));
3734 }
3735 Map()->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionBulk>(args: Map(), args&: vpActions, args: nullptr, args: true));
3736 }
3737 s_PreviousOperation = OP_NONE;
3738 }
3739 }
3740}
3741
3742bool CEditor::ReplaceImage(const char *pFilename, int StorageType, bool CheckDuplicate)
3743{
3744 // check if we have that image already
3745 char aBuf[128];
3746 IStorage::StripPathAndExtension(pFilename, pBuffer: aBuf, BufferSize: sizeof(aBuf));
3747 if(CheckDuplicate)
3748 {
3749 for(const auto &pImage : Map()->m_vpImages)
3750 {
3751 if(!str_comp(a: pImage->m_aName, b: aBuf))
3752 {
3753 ShowFileDialogError(pFormat: "Image named '%s' was already added.", pImage->m_aName);
3754 return false;
3755 }
3756 }
3757 }
3758
3759 CImageInfo ImgInfo;
3760 if(!Graphics()->LoadPng(Image&: ImgInfo, pFilename, StorageType))
3761 {
3762 ShowFileDialogError(pFormat: "Failed to load image from file '%s'.", pFilename);
3763 return false;
3764 }
3765
3766 std::shared_ptr<CEditorImage> pImg = Map()->SelectedImage();
3767 pImg->CEditorImage::Free();
3768 pImg->m_Width = ImgInfo.m_Width;
3769 pImg->m_Height = ImgInfo.m_Height;
3770 pImg->m_Format = ImgInfo.m_Format;
3771 pImg->m_pData = ImgInfo.m_pData;
3772 str_copy(dst&: pImg->m_aName, src: aBuf);
3773 pImg->m_External = IsVanillaImage(pImage: pImg->m_aName);
3774
3775 ConvertToRgba(Image&: *pImg);
3776 DilateImage(Image: *pImg);
3777
3778 pImg->m_AutoMapper.Load(pTileName: pImg->m_aName);
3779 int TextureLoadFlag = Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
3780 if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0)
3781 TextureLoadFlag = 0;
3782 pImg->m_Texture = Graphics()->LoadTextureRaw(Image: *pImg, Flags: TextureLoadFlag, pTexName: pFilename);
3783
3784 Map()->SortImages();
3785 Map()->SelectImage(pImage: pImg);
3786 OnDialogClose();
3787 return true;
3788}
3789
3790bool CEditor::ReplaceImageCallback(const char *pFilename, int StorageType, void *pUser)
3791{
3792 return static_cast<CEditor *>(pUser)->ReplaceImage(pFilename, StorageType, CheckDuplicate: true);
3793}
3794
3795bool CEditor::AddImage(const char *pFilename, int StorageType, void *pUser)
3796{
3797 CEditor *pEditor = (CEditor *)pUser;
3798
3799 // check if we have that image already
3800 char aBuf[128];
3801 IStorage::StripPathAndExtension(pFilename, pBuffer: aBuf, BufferSize: sizeof(aBuf));
3802 for(const auto &pImage : pEditor->Map()->m_vpImages)
3803 {
3804 if(!str_comp(a: pImage->m_aName, b: aBuf))
3805 {
3806 pEditor->ShowFileDialogError(pFormat: "Image named '%s' was already added.", pImage->m_aName);
3807 return false;
3808 }
3809 }
3810
3811 if(pEditor->Map()->m_vpImages.size() >= MAX_MAPIMAGES)
3812 {
3813 pEditor->m_PopupEventType = POPEVENT_IMAGE_MAX;
3814 pEditor->m_PopupEventActivated = true;
3815 return false;
3816 }
3817
3818 CImageInfo ImgInfo;
3819 if(!pEditor->Graphics()->LoadPng(Image&: ImgInfo, pFilename, StorageType))
3820 {
3821 pEditor->ShowFileDialogError(pFormat: "Failed to load image from file '%s'.", pFilename);
3822 return false;
3823 }
3824
3825 std::shared_ptr<CEditorImage> pImg = std::make_shared<CEditorImage>(args: pEditor->Map());
3826 pImg->m_Width = ImgInfo.m_Width;
3827 pImg->m_Height = ImgInfo.m_Height;
3828 pImg->m_Format = ImgInfo.m_Format;
3829 pImg->m_pData = ImgInfo.m_pData;
3830 pImg->m_External = IsVanillaImage(pImage: aBuf);
3831
3832 ConvertToRgba(Image&: *pImg);
3833 DilateImage(Image: *pImg);
3834
3835 int TextureLoadFlag = pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
3836 if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0)
3837 TextureLoadFlag = 0;
3838 pImg->m_Texture = pEditor->Graphics()->LoadTextureRaw(Image: *pImg, Flags: TextureLoadFlag, pTexName: pFilename);
3839 str_copy(dst&: pImg->m_aName, src: aBuf);
3840 pImg->m_AutoMapper.Load(pTileName: pImg->m_aName);
3841 pEditor->Map()->m_vpImages.push_back(x: pImg);
3842 pEditor->Map()->SortImages();
3843 pEditor->Map()->SelectImage(pImage: pImg);
3844 pEditor->OnDialogClose();
3845 return true;
3846}
3847
3848bool CEditor::AddSound(const char *pFilename, int StorageType, void *pUser)
3849{
3850 CEditor *pEditor = (CEditor *)pUser;
3851
3852 // check if we have that sound already
3853 char aBuf[128];
3854 IStorage::StripPathAndExtension(pFilename, pBuffer: aBuf, BufferSize: sizeof(aBuf));
3855 for(const auto &pSound : pEditor->Map()->m_vpSounds)
3856 {
3857 if(!str_comp(a: pSound->m_aName, b: aBuf))
3858 {
3859 pEditor->ShowFileDialogError(pFormat: "Sound named '%s' was already added.", pSound->m_aName);
3860 return false;
3861 }
3862 }
3863
3864 if(pEditor->Map()->m_vpSounds.size() >= MAX_MAPSOUNDS)
3865 {
3866 pEditor->m_PopupEventType = POPEVENT_SOUND_MAX;
3867 pEditor->m_PopupEventActivated = true;
3868 return false;
3869 }
3870
3871 // load external
3872 void *pData;
3873 unsigned DataSize;
3874 if(!pEditor->Storage()->ReadFile(pFilename, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
3875 {
3876 pEditor->ShowFileDialogError(pFormat: "Failed to open sound file '%s'.", pFilename);
3877 return false;
3878 }
3879
3880 // load sound
3881 const int SoundId = pEditor->Sound()->LoadOpusFromMem(pData, DataSize, ForceLoad: true, pContextName: pFilename);
3882 if(SoundId == -1)
3883 {
3884 free(ptr: pData);
3885 pEditor->ShowFileDialogError(pFormat: "Failed to load sound from file '%s'.", pFilename);
3886 return false;
3887 }
3888
3889 // add sound
3890 std::shared_ptr<CEditorSound> pSound = std::make_shared<CEditorSound>(args: pEditor->Map());
3891 pSound->m_SoundId = SoundId;
3892 pSound->m_DataSize = DataSize;
3893 pSound->m_pData = pData;
3894 str_copy(dst&: pSound->m_aName, src: aBuf);
3895 pEditor->Map()->m_vpSounds.push_back(x: pSound);
3896
3897 pEditor->Map()->SelectSound(pSound);
3898 pEditor->OnDialogClose();
3899 return true;
3900}
3901
3902bool CEditor::ReplaceSound(const char *pFilename, int StorageType, bool CheckDuplicate)
3903{
3904 // check if we have that sound already
3905 char aBuf[128];
3906 IStorage::StripPathAndExtension(pFilename, pBuffer: aBuf, BufferSize: sizeof(aBuf));
3907 if(CheckDuplicate)
3908 {
3909 for(const auto &pSound : Map()->m_vpSounds)
3910 {
3911 if(!str_comp(a: pSound->m_aName, b: aBuf))
3912 {
3913 ShowFileDialogError(pFormat: "Sound named '%s' was already added.", pSound->m_aName);
3914 return false;
3915 }
3916 }
3917 }
3918
3919 // load external
3920 void *pData;
3921 unsigned DataSize;
3922 if(!Storage()->ReadFile(pFilename, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
3923 {
3924 ShowFileDialogError(pFormat: "Failed to open sound file '%s'.", pFilename);
3925 return false;
3926 }
3927
3928 // load sound
3929 const int SoundId = Sound()->LoadOpusFromMem(pData, DataSize, ForceLoad: true, pContextName: pFilename);
3930 if(SoundId == -1)
3931 {
3932 free(ptr: pData);
3933 ShowFileDialogError(pFormat: "Failed to load sound from file '%s'.", pFilename);
3934 return false;
3935 }
3936
3937 std::shared_ptr<CEditorSound> pSound = Map()->SelectedSound();
3938
3939 if(m_ToolbarPreviewSound == pSound->m_SoundId)
3940 {
3941 m_ToolbarPreviewSound = SoundId;
3942 }
3943
3944 // unload sample
3945 Sound()->UnloadSample(SampleId: pSound->m_SoundId);
3946 free(ptr: pSound->m_pData);
3947
3948 // replace sound
3949 str_copy(dst&: pSound->m_aName, src: aBuf);
3950 pSound->m_SoundId = SoundId;
3951 pSound->m_pData = pData;
3952 pSound->m_DataSize = DataSize;
3953
3954 Map()->SelectSound(pSound);
3955 OnDialogClose();
3956 return true;
3957}
3958
3959bool CEditor::ReplaceSoundCallback(const char *pFilename, int StorageType, void *pUser)
3960{
3961 return static_cast<CEditor *>(pUser)->ReplaceSound(pFilename, StorageType, CheckDuplicate: true);
3962}
3963
3964void CEditor::RenderImagesList(CUIRect ToolBox)
3965{
3966 const float RowHeight = 12.0f;
3967
3968 static CScrollRegion s_ScrollRegion;
3969 vec2 ScrollOffset(0.0f, 0.0f);
3970 CScrollRegionParams ScrollParams;
3971 ScrollParams.m_ScrollbarWidth = 10.0f;
3972 ScrollParams.m_ScrollbarMargin = 3.0f;
3973 ScrollParams.m_ScrollUnit = RowHeight * 5;
3974 s_ScrollRegion.Begin(pClipRect: &ToolBox, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
3975 ToolBox.y += ScrollOffset.y;
3976
3977 bool ScrollToSelection = false;
3978 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Map()->m_vpImages.empty())
3979 {
3980 if(Input()->KeyPress(Key: KEY_DOWN))
3981 {
3982 const int OldImage = Map()->m_SelectedImage;
3983 Map()->SelectNextImage();
3984 ScrollToSelection = OldImage != Map()->m_SelectedImage;
3985 }
3986 else if(Input()->KeyPress(Key: KEY_UP))
3987 {
3988 const int OldImage = Map()->m_SelectedImage;
3989 Map()->SelectPreviousImage();
3990 ScrollToSelection = OldImage != Map()->m_SelectedImage;
3991 }
3992 }
3993
3994 for(int e = 0; e < 2; e++) // two passes, first embedded, then external
3995 {
3996 CUIRect Slot;
3997 ToolBox.HSplitTop(Cut: RowHeight + 3.0f, pTop: &Slot, pBottom: &ToolBox);
3998 if(s_ScrollRegion.AddRect(Rect: Slot))
3999 Ui()->DoLabel(pRect: &Slot, pText: e == 0 ? "Embedded" : "External", Size: 12.0f, Align: TEXTALIGN_MC);
4000
4001 for(int i = 0; i < (int)Map()->m_vpImages.size(); i++)
4002 {
4003 if((e && !Map()->m_vpImages[i]->m_External) ||
4004 (!e && Map()->m_vpImages[i]->m_External))
4005 {
4006 continue;
4007 }
4008
4009 ToolBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &ToolBox);
4010 int Selected = Map()->m_SelectedImage == i;
4011 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: Selected && ScrollToSelection))
4012 continue;
4013 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
4014
4015 const bool ImageUsed = std::any_of(first: Map()->m_vpGroups.cbegin(), last: Map()->m_vpGroups.cend(), pred: [i](const auto &pGroup) {
4016 return std::any_of(pGroup->m_vpLayers.cbegin(), pGroup->m_vpLayers.cend(), [i](const auto &pLayer) {
4017 if(pLayer->m_Type == LAYERTYPE_QUADS)
4018 return std::static_pointer_cast<CLayerQuads>(pLayer)->m_Image == i;
4019 else if(pLayer->m_Type == LAYERTYPE_TILES)
4020 return std::static_pointer_cast<CLayerTiles>(pLayer)->m_Image == i;
4021 return false;
4022 });
4023 });
4024
4025 if(!ImageUsed)
4026 Selected += 2; // Image is unused
4027
4028 if(Selected < 2 && e == 1)
4029 {
4030 if(!IsVanillaImage(pImage: Map()->m_vpImages[i]->m_aName))
4031 {
4032 Selected += 4; // Image should be embedded
4033 }
4034 }
4035
4036 if(int Result = DoButton_Ex(pId: &Map()->m_vpImages[i], pText: Map()->m_vpImages[i]->m_aName, Checked: Selected, pRect: &Slot,
4037 Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Select image.", Corners: IGraphics::CORNER_ALL))
4038 {
4039 Map()->m_SelectedImage = i;
4040
4041 if(Result == 2)
4042 {
4043 const int Height = Map()->SelectedImage()->m_External ? 73 : 107;
4044 static SPopupMenuId s_PopupImageId;
4045 Ui()->DoPopupMenu(pId: &s_PopupImageId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 140, Height, pContext: this, pfnFunc: PopupImage);
4046 }
4047 }
4048 }
4049
4050 // separator
4051 ToolBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &ToolBox);
4052 if(s_ScrollRegion.AddRect(Rect: Slot))
4053 {
4054 IGraphics::CLineItem LineItem(Slot.x, Slot.y + Slot.h / 2, Slot.x + Slot.w, Slot.y + Slot.h / 2);
4055 Graphics()->TextureClear();
4056 Graphics()->LinesBegin();
4057 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
4058 Graphics()->LinesEnd();
4059 }
4060 }
4061
4062 // new image
4063 static int s_AddImageButton = 0;
4064 CUIRect AddImageButton;
4065 ToolBox.HSplitTop(Cut: 5.0f + RowHeight + 1.0f, pTop: &AddImageButton, pBottom: &ToolBox);
4066 if(s_ScrollRegion.AddRect(Rect: AddImageButton))
4067 {
4068 AddImageButton.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &AddImageButton);
4069 AddImageButton.HSplitTop(Cut: RowHeight, pTop: &AddImageButton, pBottom: nullptr);
4070 if(DoButton_Editor(pId: &s_AddImageButton, pText: m_QuickActionAddImage.Label(), Checked: 0, pRect: &AddImageButton, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionAddImage.Description()))
4071 m_QuickActionAddImage.Call();
4072 }
4073 s_ScrollRegion.End();
4074}
4075
4076void CEditor::RenderSelectedImage(CUIRect View) const
4077{
4078 std::shared_ptr<CEditorImage> pSelectedImage = Map()->SelectedImage();
4079 if(pSelectedImage == nullptr)
4080 return;
4081
4082 View.Margin(Cut: 10.0f, pOtherRect: &View);
4083 if(View.h < View.w)
4084 View.w = View.h;
4085 else
4086 View.h = View.w;
4087 float Max = maximum<float>(a: pSelectedImage->m_Width, b: pSelectedImage->m_Height);
4088 View.w *= pSelectedImage->m_Width / Max;
4089 View.h *= pSelectedImage->m_Height / Max;
4090 Graphics()->TextureSet(Texture: pSelectedImage->m_Texture);
4091 Graphics()->BlendNormal();
4092 Graphics()->WrapClamp();
4093 Graphics()->QuadsBegin();
4094 IGraphics::CQuadItem QuadItem(View.x, View.y, View.w, View.h);
4095 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
4096 Graphics()->QuadsEnd();
4097 Graphics()->WrapNormal();
4098}
4099
4100void CEditor::RenderSounds(CUIRect ToolBox)
4101{
4102 const float RowHeight = 12.0f;
4103
4104 static CScrollRegion s_ScrollRegion;
4105 vec2 ScrollOffset(0.0f, 0.0f);
4106 CScrollRegionParams ScrollParams;
4107 ScrollParams.m_ScrollbarWidth = 10.0f;
4108 ScrollParams.m_ScrollbarMargin = 3.0f;
4109 ScrollParams.m_ScrollUnit = RowHeight * 5;
4110 s_ScrollRegion.Begin(pClipRect: &ToolBox, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
4111 ToolBox.y += ScrollOffset.y;
4112
4113 bool ScrollToSelection = false;
4114 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Map()->m_vpSounds.empty())
4115 {
4116 if(Input()->KeyPress(Key: KEY_DOWN))
4117 {
4118 Map()->SelectNextSound();
4119 ScrollToSelection = true;
4120 }
4121 else if(Input()->KeyPress(Key: KEY_UP))
4122 {
4123 Map()->SelectPreviousSound();
4124 ScrollToSelection = true;
4125 }
4126 }
4127
4128 CUIRect Slot;
4129 ToolBox.HSplitTop(Cut: RowHeight + 3.0f, pTop: &Slot, pBottom: &ToolBox);
4130 if(s_ScrollRegion.AddRect(Rect: Slot))
4131 Ui()->DoLabel(pRect: &Slot, pText: "Embedded", Size: 12.0f, Align: TEXTALIGN_MC);
4132
4133 for(int i = 0; i < (int)Map()->m_vpSounds.size(); i++)
4134 {
4135 ToolBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &ToolBox);
4136 int Selected = Map()->m_SelectedSound == i;
4137 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: Selected && ScrollToSelection))
4138 continue;
4139 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
4140
4141 const bool SoundUsed = std::any_of(first: Map()->m_vpGroups.cbegin(), last: Map()->m_vpGroups.cend(), pred: [i](const auto &pGroup) {
4142 return std::any_of(pGroup->m_vpLayers.cbegin(), pGroup->m_vpLayers.cend(), [i](const auto &pLayer) {
4143 if(pLayer->m_Type == LAYERTYPE_SOUNDS)
4144 return std::static_pointer_cast<CLayerSounds>(pLayer)->m_Sound == i;
4145 return false;
4146 });
4147 });
4148
4149 if(!SoundUsed)
4150 Selected += 2; // Sound is unused
4151
4152 if(int Result = DoButton_Ex(pId: &Map()->m_vpSounds[i], pText: Map()->m_vpSounds[i]->m_aName, Checked: Selected, pRect: &Slot,
4153 Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Select sound.", Corners: IGraphics::CORNER_ALL))
4154 {
4155 Map()->m_SelectedSound = i;
4156
4157 if(Result == 2)
4158 {
4159 static SPopupMenuId s_PopupSoundId;
4160 Ui()->DoPopupMenu(pId: &s_PopupSoundId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 140, Height: 90, pContext: this, pfnFunc: PopupSound);
4161 }
4162 }
4163 }
4164
4165 // separator
4166 ToolBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &ToolBox);
4167 if(s_ScrollRegion.AddRect(Rect: Slot))
4168 {
4169 IGraphics::CLineItem LineItem(Slot.x, Slot.y + Slot.h / 2, Slot.x + Slot.w, Slot.y + Slot.h / 2);
4170 Graphics()->TextureClear();
4171 Graphics()->LinesBegin();
4172 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
4173 Graphics()->LinesEnd();
4174 }
4175
4176 // new sound
4177 static int s_AddSoundButton = 0;
4178 CUIRect AddSoundButton;
4179 ToolBox.HSplitTop(Cut: 5.0f + RowHeight + 1.0f, pTop: &AddSoundButton, pBottom: &ToolBox);
4180 if(s_ScrollRegion.AddRect(Rect: AddSoundButton))
4181 {
4182 AddSoundButton.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &AddSoundButton);
4183 AddSoundButton.HSplitTop(Cut: RowHeight, pTop: &AddSoundButton, pBottom: nullptr);
4184 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."))
4185 m_FileBrowser.ShowFileDialog(StorageType: IStorage::TYPE_ALL, FileType: CFileBrowser::EFileType::SOUND, pTitle: "Add sound", pButtonText: "Add", pInitialPath: "mapres", pInitialFilename: "", pfnOpenCallback: AddSound, pOpenCallbackUser: this);
4186 }
4187 s_ScrollRegion.End();
4188}
4189
4190bool CEditor::CStringKeyComparator::operator()(const char *pLhs, const char *pRhs) const
4191{
4192 return str_comp(a: pLhs, b: pRhs) < 0;
4193}
4194
4195void CEditor::ShowFileDialogError(const char *pFormat, ...)
4196{
4197 char aMessage[1024];
4198 va_list VarArgs;
4199 va_start(VarArgs, pFormat);
4200 str_format_v(buffer: aMessage, buffer_size: sizeof(aMessage), format: pFormat, args: VarArgs);
4201 va_end(VarArgs);
4202
4203 auto ContextIterator = m_PopupMessageContexts.find(x: aMessage);
4204 CUi::SMessagePopupContext *pContext;
4205 if(ContextIterator != m_PopupMessageContexts.end())
4206 {
4207 pContext = ContextIterator->second;
4208 Ui()->ClosePopupMenu(pId: pContext);
4209 }
4210 else
4211 {
4212 pContext = new CUi::SMessagePopupContext();
4213 pContext->ErrorColor();
4214 str_copy(dst&: pContext->m_aMessage, src: aMessage);
4215 m_PopupMessageContexts[pContext->m_aMessage] = pContext;
4216 }
4217 Ui()->ShowPopupMessage(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext);
4218}
4219
4220void CEditor::RenderModebar(CUIRect View)
4221{
4222 CUIRect Mentions, IngameMoved, ModeButtons, ModeButton;
4223 View.HSplitTop(Cut: 12.0f, pTop: &Mentions, pBottom: &View);
4224 View.HSplitTop(Cut: 12.0f, pTop: &IngameMoved, pBottom: &View);
4225 View.HSplitTop(Cut: 8.0f, pTop: nullptr, pBottom: &ModeButtons);
4226 const float Width = m_ToolBoxWidth - 5.0f;
4227 ModeButtons.VSplitLeft(Cut: Width, pLeft: &ModeButtons, pRight: nullptr);
4228 const float ButtonWidth = Width / 3;
4229
4230 // mentions
4231 if(m_Mentions)
4232 {
4233 char aBuf[64];
4234 if(m_Mentions == 1)
4235 str_copy(dst&: aBuf, src: Localize(pStr: "1 new mention"));
4236 else if(m_Mentions <= 9)
4237 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d new mentions"), m_Mentions);
4238 else
4239 str_copy(dst&: aBuf, src: Localize(pStr: "9+ new mentions"));
4240
4241 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
4242 Ui()->DoLabel(pRect: &Mentions, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MC);
4243 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
4244 }
4245
4246 // ingame moved warning
4247 if(m_IngameMoved)
4248 {
4249 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
4250 Ui()->DoLabel(pRect: &IngameMoved, pText: Localize(pStr: "Moved ingame"), Size: 10.0f, Align: TEXTALIGN_MC);
4251 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
4252 }
4253
4254 // mode buttons
4255 {
4256 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
4257 static int s_LayersButton = 0;
4258 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))
4259 {
4260 m_Mode = MODE_LAYERS;
4261 }
4262
4263 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
4264 static int s_ImagesButton = 0;
4265 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))
4266 {
4267 m_Mode = MODE_IMAGES;
4268 }
4269
4270 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
4271 static int s_SoundsButton = 0;
4272 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))
4273 {
4274 m_Mode = MODE_SOUNDS;
4275 }
4276
4277 if(Input()->KeyPress(Key: KEY_LEFT) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
4278 {
4279 m_Mode = (m_Mode + NUM_MODES - 1) % NUM_MODES;
4280 }
4281 else if(Input()->KeyPress(Key: KEY_RIGHT) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
4282 {
4283 m_Mode = (m_Mode + 1) % NUM_MODES;
4284 }
4285 }
4286}
4287
4288void CEditor::RenderStatusbar(CUIRect View, CUIRect *pTooltipRect)
4289{
4290 CUIRect Button;
4291 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
4292 if(DoButton_Editor(pId: &m_QuickActionEnvelopes, pText: m_QuickActionEnvelopes.Label(), Checked: m_QuickActionEnvelopes.Color(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionEnvelopes.Description()))
4293 {
4294 m_QuickActionEnvelopes.Call();
4295 }
4296
4297 View.VSplitRight(Cut: 10.0f, pLeft: &View, pRight: nullptr);
4298 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
4299 if(DoButton_Editor(pId: &m_QuickActionServerSettings, pText: m_QuickActionServerSettings.Label(), Checked: m_QuickActionServerSettings.Color(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionServerSettings.Description()))
4300 {
4301 m_QuickActionServerSettings.Call();
4302 }
4303
4304 View.VSplitRight(Cut: 10.0f, pLeft: &View, pRight: nullptr);
4305 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
4306 if(DoButton_Editor(pId: &m_QuickActionHistory, pText: m_QuickActionHistory.Label(), Checked: m_QuickActionHistory.Color(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionHistory.Description()))
4307 {
4308 m_QuickActionHistory.Call();
4309 }
4310
4311 View.VSplitRight(Cut: 10.0f, pLeft: pTooltipRect, pRight: nullptr);
4312}
4313
4314void CEditor::RenderTooltip(CUIRect TooltipRect)
4315{
4316 if(str_comp(a: m_aTooltip, b: "") == 0)
4317 return;
4318
4319 char aBuf[256];
4320 if(m_pUiGotContext && m_pUiGotContext == Ui()->HotItem())
4321 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s Right click for context menu.", m_aTooltip);
4322 else
4323 str_copy(dst&: aBuf, src: m_aTooltip);
4324
4325 SLabelProperties Props;
4326 Props.m_MaxWidth = TooltipRect.w;
4327 Props.m_EllipsisAtEnd = true;
4328 Ui()->DoLabel(pRect: &TooltipRect, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
4329}
4330
4331void CEditor::ZoomAdaptOffsetX(float ZoomFactor, const CUIRect &View)
4332{
4333 float PosX = g_Config.m_EdZoomTarget ? (Ui()->MouseX() - View.x) / View.w : 0.5f;
4334 m_OffsetEnvelopeX = PosX - (PosX - m_OffsetEnvelopeX) * ZoomFactor;
4335}
4336
4337void CEditor::UpdateZoomEnvelopeX(const CUIRect &View)
4338{
4339 float OldZoom = m_ZoomEnvelopeX.GetValue();
4340 if(m_ZoomEnvelopeX.UpdateValue())
4341 ZoomAdaptOffsetX(ZoomFactor: OldZoom / m_ZoomEnvelopeX.GetValue(), View);
4342}
4343
4344void CEditor::ZoomAdaptOffsetY(float ZoomFactor, const CUIRect &View)
4345{
4346 float PosY = g_Config.m_EdZoomTarget ? 1.0f - (Ui()->MouseY() - View.y) / View.h : 0.5f;
4347 m_OffsetEnvelopeY = PosY - (PosY - m_OffsetEnvelopeY) * ZoomFactor;
4348}
4349
4350void CEditor::UpdateZoomEnvelopeY(const CUIRect &View)
4351{
4352 float OldZoom = m_ZoomEnvelopeY.GetValue();
4353 if(m_ZoomEnvelopeY.UpdateValue())
4354 ZoomAdaptOffsetY(ZoomFactor: OldZoom / m_ZoomEnvelopeY.GetValue(), View);
4355}
4356
4357void CEditor::ResetZoomEnvelope(const std::shared_ptr<CEnvelope> &pEnvelope, int ActiveChannels)
4358{
4359 auto [Bottom, Top] = pEnvelope->GetValueRange(ChannelMask: ActiveChannels);
4360 float EndTime = pEnvelope->EndTime();
4361 float ValueRange = absolute(a: Top - Bottom);
4362
4363 if(ValueRange < m_ZoomEnvelopeY.GetMinValue())
4364 {
4365 // Set view to some sane default if range is too small
4366 m_OffsetEnvelopeY = 0.5f - ValueRange / m_ZoomEnvelopeY.GetMinValue() / 2.0f - Bottom / m_ZoomEnvelopeY.GetMinValue();
4367 m_ZoomEnvelopeY.SetValueInstant(m_ZoomEnvelopeY.GetMinValue());
4368 }
4369 else if(ValueRange > m_ZoomEnvelopeY.GetMaxValue())
4370 {
4371 m_OffsetEnvelopeY = -Bottom / m_ZoomEnvelopeY.GetMaxValue();
4372 m_ZoomEnvelopeY.SetValueInstant(m_ZoomEnvelopeY.GetMaxValue());
4373 }
4374 else
4375 {
4376 // calculate biggest possible spacing
4377 float SpacingFactor = minimum(a: 1.25f, b: m_ZoomEnvelopeY.GetMaxValue() / ValueRange);
4378 m_ZoomEnvelopeY.SetValueInstant(SpacingFactor * ValueRange);
4379 float Space = 1.0f / SpacingFactor;
4380 float Spacing = (1.0f - Space) / 2.0f;
4381
4382 if(Top >= 0 && Bottom >= 0)
4383 m_OffsetEnvelopeY = Spacing - Bottom / m_ZoomEnvelopeY.GetValue();
4384 else if(Top <= 0 && Bottom <= 0)
4385 m_OffsetEnvelopeY = Spacing - Bottom / m_ZoomEnvelopeY.GetValue();
4386 else
4387 m_OffsetEnvelopeY = Spacing + Space * absolute(a: Bottom) / ValueRange;
4388 }
4389
4390 if(EndTime < m_ZoomEnvelopeX.GetMinValue())
4391 {
4392 m_OffsetEnvelopeX = 0.5f - EndTime / m_ZoomEnvelopeX.GetMinValue();
4393 m_ZoomEnvelopeX.SetValueInstant(m_ZoomEnvelopeX.GetMinValue());
4394 }
4395 else if(EndTime > m_ZoomEnvelopeX.GetMaxValue())
4396 {
4397 m_OffsetEnvelopeX = 0.0f;
4398 m_ZoomEnvelopeX.SetValueInstant(m_ZoomEnvelopeX.GetMaxValue());
4399 }
4400 else
4401 {
4402 float SpacingFactor = minimum(a: 1.25f, b: m_ZoomEnvelopeX.GetMaxValue() / EndTime);
4403 m_ZoomEnvelopeX.SetValueInstant(SpacingFactor * EndTime);
4404 float Space = 1.0f / SpacingFactor;
4405 float Spacing = (1.0f - Space) / 2.0f;
4406
4407 m_OffsetEnvelopeX = Spacing;
4408 }
4409}
4410
4411float CEditor::ScreenToEnvelopeX(const CUIRect &View, float x) const
4412{
4413 return (x - View.x - View.w * m_OffsetEnvelopeX) / View.w * m_ZoomEnvelopeX.GetValue();
4414}
4415
4416float CEditor::EnvelopeToScreenX(const CUIRect &View, float x) const
4417{
4418 return View.x + View.w * m_OffsetEnvelopeX + x / m_ZoomEnvelopeX.GetValue() * View.w;
4419}
4420
4421float CEditor::ScreenToEnvelopeY(const CUIRect &View, float y) const
4422{
4423 return (View.h - y + View.y) / View.h * m_ZoomEnvelopeY.GetValue() - m_OffsetEnvelopeY * m_ZoomEnvelopeY.GetValue();
4424}
4425
4426float CEditor::EnvelopeToScreenY(const CUIRect &View, float y) const
4427{
4428 return View.y + View.h - y / m_ZoomEnvelopeY.GetValue() * View.h - m_OffsetEnvelopeY * View.h;
4429}
4430
4431float CEditor::ScreenToEnvelopeDX(const CUIRect &View, float DeltaX)
4432{
4433 return DeltaX / Graphics()->ScreenWidth() * Ui()->Screen()->w / View.w * m_ZoomEnvelopeX.GetValue();
4434}
4435
4436float CEditor::ScreenToEnvelopeDY(const CUIRect &View, float DeltaY)
4437{
4438 return DeltaY / Graphics()->ScreenHeight() * Ui()->Screen()->h / View.h * m_ZoomEnvelopeY.GetValue();
4439}
4440
4441void CEditor::RemoveTimeOffsetEnvelope(const std::shared_ptr<CEnvelope> &pEnvelope)
4442{
4443 CFixedTime TimeOffset = pEnvelope->m_vPoints[0].m_Time;
4444 for(auto &Point : pEnvelope->m_vPoints)
4445 Point.m_Time -= TimeOffset;
4446
4447 m_OffsetEnvelopeX += TimeOffset.AsSeconds() / m_ZoomEnvelopeX.GetValue();
4448}
4449
4450static float ClampDelta(float Val, float Delta, float Min, float Max)
4451{
4452 if(Val + Delta <= Min)
4453 return Min - Val;
4454 if(Val + Delta >= Max)
4455 return Max - Val;
4456 return Delta;
4457}
4458
4459class CTimeStep
4460{
4461public:
4462 template<class T>
4463 CTimeStep(T t)
4464 {
4465 if constexpr(std::is_same_v<T, std::chrono::milliseconds>)
4466 m_Unit = ETimeUnit::MILLISECONDS;
4467 else if constexpr(std::is_same_v<T, std::chrono::seconds>)
4468 m_Unit = ETimeUnit::SECONDS;
4469 else
4470 m_Unit = ETimeUnit::MINUTES;
4471
4472 m_Value = t;
4473 }
4474
4475 CTimeStep operator*(int k) const
4476 {
4477 return CTimeStep(m_Value * k, m_Unit);
4478 }
4479
4480 CTimeStep operator-(const CTimeStep &Other)
4481 {
4482 return CTimeStep(m_Value - Other.m_Value, m_Unit);
4483 }
4484
4485 void Format(char *pBuffer, size_t BufferSize)
4486 {
4487 int Milliseconds = m_Value.count() % 1000;
4488 int Seconds = std::chrono::duration_cast<std::chrono::seconds>(d: m_Value).count() % 60;
4489 int Minutes = std::chrono::duration_cast<std::chrono::minutes>(d: m_Value).count();
4490
4491 switch(m_Unit)
4492 {
4493 case ETimeUnit::MILLISECONDS:
4494 if(Minutes != 0)
4495 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%d:%02d.%03dmin", Minutes, Seconds, Milliseconds);
4496 else if(Seconds != 0)
4497 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%d.%03ds", Seconds, Milliseconds);
4498 else
4499 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%dms", Milliseconds);
4500 break;
4501 case ETimeUnit::SECONDS:
4502 if(Minutes != 0)
4503 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%d:%02dmin", Minutes, Seconds);
4504 else
4505 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%ds", Seconds);
4506 break;
4507 case ETimeUnit::MINUTES:
4508 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%dmin", Minutes);
4509 break;
4510 }
4511 }
4512
4513 float AsSeconds() const
4514 {
4515 return std::chrono::duration_cast<std::chrono::duration<float>>(d: m_Value).count();
4516 }
4517
4518private:
4519 enum class ETimeUnit
4520 {
4521 MILLISECONDS,
4522 SECONDS,
4523 MINUTES,
4524 } m_Unit;
4525 std::chrono::milliseconds m_Value;
4526
4527 CTimeStep(std::chrono::milliseconds Value, ETimeUnit Unit)
4528 {
4529 m_Value = Value;
4530 m_Unit = Unit;
4531 }
4532};
4533
4534void CEditor::UpdateHotEnvelopePoint(const CUIRect &View, const CEnvelope *pEnvelope, int ActiveChannels)
4535{
4536 if(!Ui()->MouseInside(pRect: &View))
4537 return;
4538
4539 const vec2 MousePos = Ui()->MousePos();
4540
4541 float MinDist = 200.0f;
4542 const void *pMinPointId = nullptr;
4543
4544 const auto UpdateMinimum = [&](vec2 Position, const void *pId) {
4545 const float CurrDist = length_squared(a: Position - MousePos);
4546 if(CurrDist < MinDist)
4547 {
4548 MinDist = CurrDist;
4549 pMinPointId = pId;
4550 }
4551 };
4552
4553 for(size_t i = 0; i < pEnvelope->m_vPoints.size(); i++)
4554 {
4555 for(int c = pEnvelope->GetChannels() - 1; c >= 0; c--)
4556 {
4557 if(!(ActiveChannels & (1 << c)))
4558 continue;
4559
4560 if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER)
4561 {
4562 vec2 Position;
4563 Position.x = EnvelopeToScreenX(View, x: (pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]).AsSeconds());
4564 Position.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]));
4565 UpdateMinimum(Position, &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]);
4566 }
4567
4568 if(i < pEnvelope->m_vPoints.size() - 1 && pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER)
4569 {
4570 vec2 Position;
4571 Position.x = EnvelopeToScreenX(View, x: (pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]).AsSeconds());
4572 Position.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]));
4573 UpdateMinimum(Position, &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]);
4574 }
4575
4576 vec2 Position;
4577 Position.x = EnvelopeToScreenX(View, x: pEnvelope->m_vPoints[i].m_Time.AsSeconds());
4578 Position.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]));
4579 UpdateMinimum(Position, &pEnvelope->m_vPoints[i].m_aValues[c]);
4580 }
4581 }
4582
4583 if(pMinPointId != nullptr)
4584 {
4585 Ui()->SetHotItem(pMinPointId);
4586 }
4587}
4588
4589void CEditor::RenderEnvelopeEditor(CUIRect View)
4590{
4591 Map()->m_SelectedEnvelope = Map()->m_vpEnvelopes.empty() ? -1 : std::clamp(val: Map()->m_SelectedEnvelope, lo: 0, hi: (int)Map()->m_vpEnvelopes.size() - 1);
4592 std::shared_ptr<CEnvelope> pEnvelope = Map()->m_vpEnvelopes.empty() ? nullptr : Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
4593
4594 static EEnvelopeEditorOp s_Operation = EEnvelopeEditorOp::NONE;
4595 static std::vector<float> s_vAccurateDragValuesX = {};
4596 static std::vector<float> s_vAccurateDragValuesY = {};
4597 static float s_MouseXStart = 0.0f;
4598 static float s_MouseYStart = 0.0f;
4599
4600 static CLineInput s_NameInput;
4601
4602 CUIRect ToolBar, CurveBar, ColorBar, DragBar;
4603 View.HSplitTop(Cut: 30.0f, pTop: &DragBar, pBottom: nullptr);
4604 DragBar.y -= 2.0f;
4605 DragBar.w += 2.0f;
4606 DragBar.h += 4.0f;
4607 DoEditorDragBar(View, pDragBar: &DragBar, Side: EDragSide::TOP, pValue: &m_aExtraEditorSplits[EXTRAEDITOR_ENVELOPES]);
4608 View.HSplitTop(Cut: 15.0f, pTop: &ToolBar, pBottom: &View);
4609 View.HSplitTop(Cut: 15.0f, pTop: &CurveBar, pBottom: &View);
4610 ToolBar.Margin(Cut: 2.0f, pOtherRect: &ToolBar);
4611 CurveBar.Margin(Cut: 2.0f, pOtherRect: &CurveBar);
4612
4613 bool CurrentEnvelopeSwitched = false;
4614
4615 // do the toolbar
4616 static int s_ActiveChannels = 0xf;
4617 {
4618 CUIRect Button;
4619
4620 // redo button
4621 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
4622 static int s_RedoButton = 0;
4623 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)
4624 {
4625 Map()->m_EnvelopeEditorHistory.Redo();
4626 }
4627
4628 // undo button
4629 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
4630 ToolBar.VSplitRight(Cut: 10.0f, pLeft: &ToolBar, pRight: nullptr);
4631 static int s_UndoButton = 0;
4632 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)
4633 {
4634 Map()->m_EnvelopeEditorHistory.Undo();
4635 }
4636
4637 ToolBar.VSplitRight(Cut: 50.0f, pLeft: &ToolBar, pRight: &Button);
4638 static int s_NewSoundButton = 0;
4639 if(DoButton_Editor(pId: &s_NewSoundButton, pText: "Sound+", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Create a new sound envelope."))
4640 {
4641 Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionEnvelopeAdd>(args: Map(), args: CEnvelope::EType::SOUND));
4642 pEnvelope = Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
4643 CurrentEnvelopeSwitched = true;
4644 }
4645
4646 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
4647 ToolBar.VSplitRight(Cut: 50.0f, pLeft: &ToolBar, pRight: &Button);
4648 static int s_New4dButton = 0;
4649 if(DoButton_Editor(pId: &s_New4dButton, pText: "Color+", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Create a new color envelope."))
4650 {
4651 Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionEnvelopeAdd>(args: Map(), args: CEnvelope::EType::COLOR));
4652 pEnvelope = Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
4653 CurrentEnvelopeSwitched = true;
4654 }
4655
4656 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
4657 ToolBar.VSplitRight(Cut: 50.0f, pLeft: &ToolBar, pRight: &Button);
4658 static int s_New2dButton = 0;
4659 if(DoButton_Editor(pId: &s_New2dButton, pText: "Pos.+", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Create a new position envelope."))
4660 {
4661 Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionEnvelopeAdd>(args: Map(), args: CEnvelope::EType::POSITION));
4662 pEnvelope = Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
4663 CurrentEnvelopeSwitched = true;
4664 }
4665
4666 if(Map()->m_SelectedEnvelope >= 0)
4667 {
4668 // Delete button
4669 ToolBar.VSplitRight(Cut: 10.0f, pLeft: &ToolBar, pRight: nullptr);
4670 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
4671 static int s_DeleteButton = 0;
4672 if(DoButton_Editor(pId: &s_DeleteButton, pText: "✗", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Delete this envelope."))
4673 {
4674 auto vpObjectReferences = Map()->DeleteEnvelope(Index: Map()->m_SelectedEnvelope);
4675 Map()->m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeDelete>(args: Map(), args&: Map()->m_SelectedEnvelope, args&: vpObjectReferences, args&: pEnvelope));
4676
4677 Map()->m_SelectedEnvelope = Map()->m_vpEnvelopes.empty() ? -1 : std::clamp(val: Map()->m_SelectedEnvelope, lo: 0, hi: (int)Map()->m_vpEnvelopes.size() - 1);
4678 pEnvelope = Map()->m_vpEnvelopes.empty() ? nullptr : Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
4679 Map()->OnModify();
4680 }
4681 }
4682
4683 // check again, because the last envelope might has been deleted
4684 if(Map()->m_SelectedEnvelope >= 0)
4685 {
4686 // Move right button
4687 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
4688 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
4689 static int s_MoveRightButton = 0;
4690 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))
4691 {
4692 int MoveTo = Map()->m_SelectedEnvelope + 1;
4693 int MoveFrom = Map()->m_SelectedEnvelope;
4694 Map()->m_SelectedEnvelope = Map()->MoveEnvelope(IndexFrom: MoveFrom, IndexTo: MoveTo);
4695 if(Map()->m_SelectedEnvelope != MoveFrom)
4696 {
4697 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));
4698 pEnvelope = Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
4699 Map()->OnModify();
4700 }
4701 }
4702
4703 // Move left button
4704 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
4705 static int s_MoveLeftButton = 0;
4706 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))
4707 {
4708 int MoveTo = Map()->m_SelectedEnvelope - 1;
4709 int MoveFrom = Map()->m_SelectedEnvelope;
4710 Map()->m_SelectedEnvelope = Map()->MoveEnvelope(IndexFrom: MoveFrom, IndexTo: MoveTo);
4711 if(Map()->m_SelectedEnvelope != MoveFrom)
4712 {
4713 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));
4714 pEnvelope = Map()->m_vpEnvelopes[Map()->m_SelectedEnvelope];
4715 Map()->OnModify();
4716 }
4717 }
4718
4719 if(pEnvelope)
4720 {
4721 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
4722 ToolBar.VSplitRight(Cut: 20.0f, pLeft: &ToolBar, pRight: &Button);
4723 static int s_ZoomOutButton = 0;
4724 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))
4725 {
4726 if(Input()->ShiftIsPressed())
4727 m_ZoomEnvelopeY.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeY.GetValue());
4728 else
4729 m_ZoomEnvelopeX.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeX.GetValue());
4730 }
4731
4732 ToolBar.VSplitRight(Cut: 20.0f, pLeft: &ToolBar, pRight: &Button);
4733 static int s_ResetZoomButton = 0;
4734 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))
4735 ResetZoomEnvelope(pEnvelope, ActiveChannels: s_ActiveChannels);
4736
4737 ToolBar.VSplitRight(Cut: 20.0f, pLeft: &ToolBar, pRight: &Button);
4738 static int s_ZoomInButton = 0;
4739 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))
4740 {
4741 if(Input()->ShiftIsPressed())
4742 m_ZoomEnvelopeY.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeY.GetValue());
4743 else
4744 m_ZoomEnvelopeX.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeX.GetValue());
4745 }
4746 }
4747
4748 // Margin on the right side
4749 ToolBar.VSplitRight(Cut: 7.0f, pLeft: &ToolBar, pRight: nullptr);
4750 }
4751
4752 CUIRect Shifter, Inc, Dec;
4753 ToolBar.VSplitLeft(Cut: 60.0f, pLeft: &Shifter, pRight: &ToolBar);
4754 Shifter.VSplitRight(Cut: 15.0f, pLeft: &Shifter, pRight: &Inc);
4755 Shifter.VSplitLeft(Cut: 15.0f, pLeft: &Dec, pRight: &Shifter);
4756 char aBuf[64];
4757 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d/%d", Map()->m_SelectedEnvelope + 1, (int)Map()->m_vpEnvelopes.size());
4758
4759 ColorRGBA EnvColor = ColorRGBA(1, 1, 1, 0.5f);
4760 if(!Map()->m_vpEnvelopes.empty())
4761 {
4762 EnvColor = Map()->IsEnvelopeUsed(EnvelopeIndex: Map()->m_SelectedEnvelope) ? ColorRGBA(1, 0.7f, 0.7f, 0.5f) : ColorRGBA(0.7f, 1, 0.7f, 0.5f);
4763 }
4764
4765 static int s_EnvelopeSelector = 0;
4766 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);
4767 int NewValue = NewValueRes.m_Value;
4768 if(NewValue - 1 != Map()->m_SelectedEnvelope)
4769 {
4770 Map()->m_SelectedEnvelope = NewValue - 1;
4771 CurrentEnvelopeSwitched = true;
4772 }
4773
4774 static int s_PrevButton = 0;
4775 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))
4776 {
4777 Map()->m_SelectedEnvelope--;
4778 if(Map()->m_SelectedEnvelope < 0)
4779 Map()->m_SelectedEnvelope = Map()->m_vpEnvelopes.size() - 1;
4780 CurrentEnvelopeSwitched = true;
4781 }
4782
4783 static int s_NextButton = 0;
4784 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))
4785 {
4786 Map()->m_SelectedEnvelope++;
4787 if(Map()->m_SelectedEnvelope >= (int)Map()->m_vpEnvelopes.size())
4788 Map()->m_SelectedEnvelope = 0;
4789 CurrentEnvelopeSwitched = true;
4790 }
4791
4792 if(pEnvelope)
4793 {
4794 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: nullptr, pRight: &ToolBar);
4795 ToolBar.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolBar);
4796 Ui()->DoLabel(pRect: &Button, pText: "Name:", Size: 10.0f, Align: TEXTALIGN_MR);
4797
4798 ToolBar.VSplitLeft(Cut: 3.0f, pLeft: nullptr, pRight: &ToolBar);
4799 ToolBar.VSplitLeft(Cut: ToolBar.w > ToolBar.h * 40 ? 80.0f : 60.0f, pLeft: &Button, pRight: &ToolBar);
4800
4801 s_NameInput.SetBuffer(pStr: pEnvelope->m_aName, MaxSize: sizeof(pEnvelope->m_aName));
4802 if(DoEditBox(pLineInput: &s_NameInput, pRect: &Button, FontSize: 10.0f, Corners: IGraphics::CORNER_ALL, pToolTip: "The name of the selected envelope."))
4803 {
4804 Map()->OnModify();
4805 }
4806 }
4807 }
4808
4809 const bool ShowColorBar = pEnvelope && pEnvelope->GetChannels() == 4;
4810 if(ShowColorBar)
4811 {
4812 View.HSplitTop(Cut: 20.0f, pTop: &ColorBar, pBottom: &View);
4813 ColorBar.HMargin(Cut: 2.0f, pOtherRect: &ColorBar);
4814 }
4815
4816 RenderBackground(View, Texture: m_CheckerTexture, Size: 32.0f, Brightness: 0.1f);
4817
4818 if(pEnvelope)
4819 {
4820 if(m_ResetZoomEnvelope)
4821 {
4822 m_ResetZoomEnvelope = false;
4823 ResetZoomEnvelope(pEnvelope, ActiveChannels: s_ActiveChannels);
4824 }
4825
4826 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)};
4827
4828 CUIRect Button;
4829
4830 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: &Button, pRight: &ToolBar);
4831
4832 static const char *s_aapNames[4][CEnvPoint::MAX_CHANNELS] = {
4833 {"V", "", "", ""},
4834 {"", "", "", ""},
4835 {"X", "Y", "R", ""},
4836 {"R", "G", "B", "A"},
4837 };
4838
4839 static const char *s_aapDescriptions[4][CEnvPoint::MAX_CHANNELS] = {
4840 {"Volume of the envelope.", "", "", ""},
4841 {"", "", "", ""},
4842 {"X-axis of the envelope.", "Y-axis of the envelope.", "Rotation of the envelope.", ""},
4843 {"Red value of the envelope.", "Green value of the envelope.", "Blue value of the envelope.", "Alpha value of the envelope."},
4844 };
4845
4846 static int s_aChannelButtons[CEnvPoint::MAX_CHANNELS] = {0};
4847 int Bit = 1;
4848
4849 for(int i = 0; i < CEnvPoint::MAX_CHANNELS; i++, Bit <<= 1)
4850 {
4851 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: &Button, pRight: &ToolBar);
4852 if(i < pEnvelope->GetChannels())
4853 {
4854 int Corners = IGraphics::CORNER_NONE;
4855 if(pEnvelope->GetChannels() == 1)
4856 Corners = IGraphics::CORNER_ALL;
4857 else if(i == 0)
4858 Corners = IGraphics::CORNER_L;
4859 else if(i == pEnvelope->GetChannels() - 1)
4860 Corners = IGraphics::CORNER_R;
4861
4862 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))
4863 s_ActiveChannels ^= Bit;
4864 }
4865 }
4866
4867 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: nullptr, pRight: &ToolBar);
4868 ToolBar.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolBar);
4869
4870 static int s_EnvelopeEditorId = 0;
4871 static int s_EnvelopeEditorButtonUsed = -1;
4872 const bool ShouldPan = s_Operation == EEnvelopeEditorOp::NONE && (Ui()->MouseButton(Index: 2) || (Ui()->MouseButton(Index: 0) && Input()->ModifierIsPressed()));
4873 if(m_pContainerPanned == &s_EnvelopeEditorId)
4874 {
4875 if(!ShouldPan)
4876 m_pContainerPanned = nullptr;
4877 else
4878 {
4879 m_OffsetEnvelopeX += Ui()->MouseDeltaX() / Graphics()->ScreenWidth() * Ui()->Screen()->w / View.w;
4880 m_OffsetEnvelopeY -= Ui()->MouseDeltaY() / Graphics()->ScreenHeight() * Ui()->Screen()->h / View.h;
4881 }
4882 }
4883
4884 if(Ui()->MouseInside(pRect: &View) && m_Dialog == DIALOG_NONE)
4885 {
4886 Ui()->SetHotItem(&s_EnvelopeEditorId);
4887
4888 if(ShouldPan && m_pContainerPanned == nullptr)
4889 m_pContainerPanned = &s_EnvelopeEditorId;
4890
4891 if(Input()->KeyPress(Key: KEY_KP_MULTIPLY) && CLineInput::GetActiveInput() == nullptr)
4892 ResetZoomEnvelope(pEnvelope, ActiveChannels: s_ActiveChannels);
4893 if(Input()->ShiftIsPressed())
4894 {
4895 if(Input()->KeyPress(Key: KEY_KP_MINUS) && CLineInput::GetActiveInput() == nullptr)
4896 m_ZoomEnvelopeY.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeY.GetValue());
4897 if(Input()->KeyPress(Key: KEY_KP_PLUS) && CLineInput::GetActiveInput() == nullptr)
4898 m_ZoomEnvelopeY.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeY.GetValue());
4899 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
4900 m_ZoomEnvelopeY.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeY.GetValue());
4901 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
4902 m_ZoomEnvelopeY.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeY.GetValue());
4903 }
4904 else
4905 {
4906 if(Input()->KeyPress(Key: KEY_KP_MINUS) && CLineInput::GetActiveInput() == nullptr)
4907 m_ZoomEnvelopeX.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeX.GetValue());
4908 if(Input()->KeyPress(Key: KEY_KP_PLUS) && CLineInput::GetActiveInput() == nullptr)
4909 m_ZoomEnvelopeX.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeX.GetValue());
4910 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
4911 m_ZoomEnvelopeX.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeX.GetValue());
4912 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
4913 m_ZoomEnvelopeX.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeX.GetValue());
4914 }
4915 }
4916
4917 if(Ui()->HotItem() == &s_EnvelopeEditorId)
4918 {
4919 // do stuff
4920 if(Ui()->MouseButton(Index: 0))
4921 {
4922 s_EnvelopeEditorButtonUsed = 0;
4923 if(s_Operation != EEnvelopeEditorOp::BOX_SELECT && !Input()->ModifierIsPressed())
4924 {
4925 s_Operation = EEnvelopeEditorOp::BOX_SELECT;
4926 s_MouseXStart = Ui()->MouseX();
4927 s_MouseYStart = Ui()->MouseY();
4928 }
4929 }
4930 else if(s_EnvelopeEditorButtonUsed == 0)
4931 {
4932 if(Ui()->DoDoubleClickLogic(pId: &s_EnvelopeEditorId) && !Input()->ModifierIsPressed())
4933 {
4934 // add point
4935 float Time = ScreenToEnvelopeX(View, x: Ui()->MouseX());
4936 ColorRGBA Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
4937 pEnvelope->Eval(Time: std::clamp(val: Time, lo: 0.0f, hi: pEnvelope->EndTime()), Result&: Channels, Channels: 4);
4938
4939 const CFixedTime FixedTime = CFixedTime::FromSeconds(Seconds: Time);
4940 bool TimeFound = false;
4941 for(CEnvPoint &Point : pEnvelope->m_vPoints)
4942 {
4943 if(Point.m_Time == FixedTime)
4944 TimeFound = true;
4945 }
4946
4947 if(!TimeFound)
4948 Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionAddEnvelopePoint>(args: Map(), args&: Map()->m_SelectedEnvelope, args: FixedTime, args&: Channels));
4949
4950 if(FixedTime < CFixedTime(0))
4951 RemoveTimeOffsetEnvelope(pEnvelope);
4952 Map()->OnModify();
4953 }
4954 s_EnvelopeEditorButtonUsed = -1;
4955 }
4956
4957 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
4958 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.");
4959 }
4960
4961 UpdateZoomEnvelopeX(View);
4962 UpdateZoomEnvelopeY(View);
4963
4964 {
4965 float UnitsPerLineY = 0.001f;
4966 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};
4967 for(float Value : s_aUnitPerLineOptionsY)
4968 {
4969 if(Value / m_ZoomEnvelopeY.GetValue() * View.h < 40.0f)
4970 UnitsPerLineY = Value;
4971 }
4972 int NumLinesY = m_ZoomEnvelopeY.GetValue() / UnitsPerLineY + 1;
4973
4974 Ui()->ClipEnable(pRect: &View);
4975 Graphics()->TextureClear();
4976 Graphics()->LinesBegin();
4977 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.2f);
4978
4979 float BaseValue = static_cast<int>(m_OffsetEnvelopeY * m_ZoomEnvelopeY.GetValue() / UnitsPerLineY) * UnitsPerLineY;
4980 for(int i = 0; i <= NumLinesY; i++)
4981 {
4982 float Value = UnitsPerLineY * i - BaseValue;
4983 IGraphics::CLineItem LineItem(View.x, EnvelopeToScreenY(View, y: Value), View.x + View.w, EnvelopeToScreenY(View, y: Value));
4984 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
4985 }
4986
4987 Graphics()->LinesEnd();
4988
4989 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
4990 for(int i = 0; i <= NumLinesY; i++)
4991 {
4992 float Value = UnitsPerLineY * i - BaseValue;
4993 char aValueBuffer[16];
4994 if(UnitsPerLineY >= 1.0f)
4995 {
4996 str_format(buffer: aValueBuffer, buffer_size: sizeof(aValueBuffer), format: "%d", static_cast<int>(Value));
4997 }
4998 else
4999 {
5000 str_format(buffer: aValueBuffer, buffer_size: sizeof(aValueBuffer), format: "%.3f", Value);
5001 }
5002 Ui()->TextRender()->Text(x: View.x, y: EnvelopeToScreenY(View, y: Value) + 4.0f, Size: 8.0f, pText: aValueBuffer);
5003 }
5004 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
5005 Ui()->ClipDisable();
5006 }
5007
5008 {
5009 using namespace std::chrono_literals;
5010 CTimeStep UnitsPerLineX = 1ms;
5011 static const CTimeStep s_aUnitPerLineOptionsX[] = {5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2s, 5s, 10s, 15s, 30s, 1min};
5012 for(CTimeStep Value : s_aUnitPerLineOptionsX)
5013 {
5014 if(Value.AsSeconds() / m_ZoomEnvelopeX.GetValue() * View.w < 160.0f)
5015 UnitsPerLineX = Value;
5016 }
5017 int NumLinesX = m_ZoomEnvelopeX.GetValue() / UnitsPerLineX.AsSeconds() + 1;
5018
5019 Ui()->ClipEnable(pRect: &View);
5020 Graphics()->TextureClear();
5021 Graphics()->LinesBegin();
5022 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.2f);
5023
5024 CTimeStep BaseValue = UnitsPerLineX * static_cast<int>(m_OffsetEnvelopeX * m_ZoomEnvelopeX.GetValue() / UnitsPerLineX.AsSeconds());
5025 for(int i = 0; i <= NumLinesX; i++)
5026 {
5027 float Value = UnitsPerLineX.AsSeconds() * i - BaseValue.AsSeconds();
5028 IGraphics::CLineItem LineItem(EnvelopeToScreenX(View, x: Value), View.y, EnvelopeToScreenX(View, x: Value), View.y + View.h);
5029 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
5030 }
5031
5032 Graphics()->LinesEnd();
5033
5034 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
5035 for(int i = 0; i <= NumLinesX; i++)
5036 {
5037 CTimeStep Value = UnitsPerLineX * i - BaseValue;
5038 if(Value.AsSeconds() >= 0)
5039 {
5040 char aValueBuffer[16];
5041 Value.Format(pBuffer: aValueBuffer, BufferSize: sizeof(aValueBuffer));
5042
5043 Ui()->TextRender()->Text(x: EnvelopeToScreenX(View, x: Value.AsSeconds()) + 1.0f, y: View.y + View.h - 8.0f, Size: 8.0f, pText: aValueBuffer);
5044 }
5045 }
5046 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
5047 Ui()->ClipDisable();
5048 }
5049
5050 // render tangents for bezier curves
5051 {
5052 Ui()->ClipEnable(pRect: &View);
5053 Graphics()->TextureClear();
5054 Graphics()->LinesBegin();
5055 for(int c = 0; c < pEnvelope->GetChannels(); c++)
5056 {
5057 if(!(s_ActiveChannels & (1 << c)))
5058 continue;
5059
5060 for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++)
5061 {
5062 float PosX = EnvelopeToScreenX(View, x: pEnvelope->m_vPoints[i].m_Time.AsSeconds());
5063 float PosY = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]));
5064
5065 // Out-Tangent
5066 if(i < (int)pEnvelope->m_vPoints.size() - 1 && pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER)
5067 {
5068 float TangentX = EnvelopeToScreenX(View, x: (pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]).AsSeconds());
5069 float TangentY = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]));
5070
5071 if(Map()->IsTangentOutPointSelected(Index: i, Channel: c))
5072 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
5073 else
5074 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 0.4f);
5075
5076 IGraphics::CLineItem LineItem(TangentX, TangentY, PosX, PosY);
5077 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
5078 }
5079
5080 // In-Tangent
5081 if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER)
5082 {
5083 float TangentX = EnvelopeToScreenX(View, x: (pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]).AsSeconds());
5084 float TangentY = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]));
5085
5086 if(Map()->IsTangentInPointSelected(Index: i, Channel: c))
5087 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
5088 else
5089 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 0.4f);
5090
5091 IGraphics::CLineItem LineItem(TangentX, TangentY, PosX, PosY);
5092 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
5093 }
5094 }
5095 }
5096 Graphics()->LinesEnd();
5097 Ui()->ClipDisable();
5098 }
5099
5100 // render lines
5101 {
5102 float EndTimeTotal = maximum(a: 0.000001f, b: pEnvelope->EndTime());
5103 float EndX = std::clamp(val: EnvelopeToScreenX(View, x: EndTimeTotal), lo: View.x, hi: View.x + View.w);
5104 float StartX = std::clamp(val: View.x + View.w * m_OffsetEnvelopeX, lo: View.x, hi: View.x + View.w);
5105
5106 float EndTime = ScreenToEnvelopeX(View, x: EndX);
5107 float StartTime = ScreenToEnvelopeX(View, x: StartX);
5108
5109 Ui()->ClipEnable(pRect: &View);
5110 Graphics()->TextureClear();
5111 IGraphics::CLineItemBatch LineItemBatch;
5112 for(int c = 0; c < pEnvelope->GetChannels(); c++)
5113 {
5114 Graphics()->LinesBatchBegin(pBatch: &LineItemBatch);
5115 if(s_ActiveChannels & (1 << c))
5116 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1);
5117 else
5118 Graphics()->SetColor(r: aColors[c].r * 0.5f, g: aColors[c].g * 0.5f, b: aColors[c].b * 0.5f, a: 1);
5119
5120 const int Steps = static_cast<int>(((EndX - StartX) / Ui()->Screen()->w) * Graphics()->ScreenWidth());
5121 const float StepTime = (EndTime - StartTime) / static_cast<float>(Steps);
5122 const float StepSize = (EndX - StartX) / static_cast<float>(Steps);
5123
5124 ColorRGBA Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
5125 pEnvelope->Eval(Time: StartTime, Result&: Channels, Channels: c + 1);
5126 float PrevTime = StartTime;
5127 float PrevX = StartX;
5128 float PrevY = EnvelopeToScreenY(View, y: Channels[c]);
5129 for(int Step = 1; Step <= Steps; Step++)
5130 {
5131 float CurrentTime = StartTime + Step * StepTime;
5132 if(CurrentTime >= EndTime)
5133 {
5134 CurrentTime = EndTime - 0.001f;
5135 if(CurrentTime <= PrevTime)
5136 break;
5137 }
5138
5139 Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
5140 pEnvelope->Eval(Time: CurrentTime, Result&: Channels, Channels: c + 1);
5141 const float CurrentX = StartX + Step * StepSize;
5142 const float CurrentY = EnvelopeToScreenY(View, y: Channels[c]);
5143
5144 const IGraphics::CLineItem Item = IGraphics::CLineItem(PrevX, PrevY, CurrentX, CurrentY);
5145 Graphics()->LinesBatchDraw(pBatch: &LineItemBatch, pArray: &Item, Num: 1);
5146
5147 PrevTime = CurrentTime;
5148 PrevX = CurrentX;
5149 PrevY = CurrentY;
5150 }
5151 Graphics()->LinesBatchEnd(pBatch: &LineItemBatch);
5152 }
5153 Ui()->ClipDisable();
5154 }
5155
5156 // render curve options
5157 {
5158 for(int i = 0; i < (int)pEnvelope->m_vPoints.size() - 1; i++)
5159 {
5160 float t0 = pEnvelope->m_vPoints[i].m_Time.AsSeconds();
5161 float t1 = pEnvelope->m_vPoints[i + 1].m_Time.AsSeconds();
5162
5163 CUIRect CurveButton;
5164 CurveButton.x = EnvelopeToScreenX(View, x: t0 + (t1 - t0) * 0.5f);
5165 CurveButton.y = CurveBar.y;
5166 CurveButton.h = CurveBar.h;
5167 CurveButton.w = CurveBar.h;
5168 CurveButton.x -= CurveButton.w / 2.0f;
5169 const void *pId = &pEnvelope->m_vPoints[i].m_Curvetype;
5170 static const char *const TYPE_NAMES[NUM_CURVETYPES] = {"N", "L", "S", "F", "M", "B"};
5171 const char *pTypeName = "!?";
5172 if(0 <= pEnvelope->m_vPoints[i].m_Curvetype && pEnvelope->m_vPoints[i].m_Curvetype < (int)std::size(TYPE_NAMES))
5173 pTypeName = TYPE_NAMES[pEnvelope->m_vPoints[i].m_Curvetype];
5174
5175 if(CurveButton.x >= View.x)
5176 {
5177 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).");
5178 if(ButtonResult == 1)
5179 {
5180 const int PrevCurve = pEnvelope->m_vPoints[i].m_Curvetype;
5181 const int Direction = Input()->ShiftIsPressed() ? -1 : 1;
5182 pEnvelope->m_vPoints[i].m_Curvetype = (pEnvelope->m_vPoints[i].m_Curvetype + Direction + NUM_CURVETYPES) % NUM_CURVETYPES;
5183
5184 Map()->m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEditPoint>(args: Map(),
5185 args&: Map()->m_SelectedEnvelope, args&: i, args: 0, args: CEditorActionEnvelopeEditPoint::EEditType::CURVE_TYPE, args: PrevCurve, args&: pEnvelope->m_vPoints[i].m_Curvetype));
5186 Map()->OnModify();
5187 }
5188 else if(ButtonResult == 2)
5189 {
5190 m_PopupEnvelopeSelectedPoint = i;
5191 static SPopupMenuId s_PopupCurvetypeId;
5192 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);
5193 }
5194 }
5195 }
5196 }
5197
5198 // render colorbar
5199 if(ShowColorBar)
5200 {
5201 RenderEnvelopeEditorColorBar(ColorBar, pEnvelope);
5202 }
5203
5204 // render handles
5205 if(CurrentEnvelopeSwitched)
5206 {
5207 Map()->DeselectEnvPoints();
5208 m_ResetZoomEnvelope = true;
5209 }
5210
5211 {
5212 static SPopupMenuId s_PopupEnvPointId;
5213 const auto &&ShowPopupEnvPoint = [&]() {
5214 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);
5215 };
5216
5217 if(s_Operation == EEnvelopeEditorOp::NONE)
5218 {
5219 UpdateHotEnvelopePoint(View, pEnvelope: pEnvelope.get(), ActiveChannels: s_ActiveChannels);
5220 if(!Ui()->MouseButton(Index: 0))
5221 Map()->m_EnvOpTracker.Stop(Switch: false);
5222 }
5223 else
5224 {
5225 Map()->m_EnvOpTracker.Begin(Operation: s_Operation);
5226 }
5227
5228 Ui()->ClipEnable(pRect: &View);
5229 Graphics()->TextureClear();
5230 Graphics()->QuadsBegin();
5231 for(int c = 0; c < pEnvelope->GetChannels(); c++)
5232 {
5233 if(!(s_ActiveChannels & (1 << c)))
5234 continue;
5235
5236 for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++)
5237 {
5238 // point handle
5239 {
5240 CUIRect Final;
5241 Final.x = EnvelopeToScreenX(View, x: pEnvelope->m_vPoints[i].m_Time.AsSeconds());
5242 Final.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]));
5243 Final.x -= 2.0f;
5244 Final.y -= 2.0f;
5245 Final.w = 4.0f;
5246 Final.h = 4.0f;
5247
5248 const void *pId = &pEnvelope->m_vPoints[i].m_aValues[c];
5249
5250 if(Map()->IsEnvPointSelected(Index: i, Channel: c))
5251 {
5252 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
5253 CUIRect Background = {
5254 .x: Final.x - 0.2f * Final.w,
5255 .y: Final.y - 0.2f * Final.h,
5256 .w: Final.w * 1.4f,
5257 .h: Final.h * 1.4f};
5258 IGraphics::CQuadItem QuadItem(Background.x, Background.y, Background.w, Background.h);
5259 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
5260 }
5261
5262 if(Ui()->CheckActiveItem(pId))
5263 {
5264 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
5265
5266 if(s_Operation == EEnvelopeEditorOp::SELECT)
5267 {
5268 float dx = s_MouseXStart - Ui()->MouseX();
5269 float dy = s_MouseYStart - Ui()->MouseY();
5270
5271 if(dx * dx + dy * dy > 20.0f)
5272 {
5273 s_Operation = EEnvelopeEditorOp::DRAG_POINT;
5274
5275 if(!Map()->IsEnvPointSelected(Index: i, Channel: c))
5276 Map()->SelectEnvPoint(Index: i, Channel: c);
5277 }
5278 }
5279
5280 if(s_Operation == EEnvelopeEditorOp::DRAG_POINT || s_Operation == EEnvelopeEditorOp::DRAG_POINT_X || s_Operation == EEnvelopeEditorOp::DRAG_POINT_Y)
5281 {
5282 if(Input()->ShiftIsPressed())
5283 {
5284 if(s_Operation == EEnvelopeEditorOp::DRAG_POINT || s_Operation == EEnvelopeEditorOp::DRAG_POINT_Y)
5285 {
5286 s_Operation = EEnvelopeEditorOp::DRAG_POINT_X;
5287 s_vAccurateDragValuesX.clear();
5288 for(auto [SelectedIndex, _] : Map()->m_vSelectedEnvelopePoints)
5289 s_vAccurateDragValuesX.push_back(x: pEnvelope->m_vPoints[SelectedIndex].m_Time.GetInternal());
5290 }
5291 else
5292 {
5293 float DeltaX = ScreenToEnvelopeDX(View, DeltaX: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f);
5294
5295 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
5296 {
5297 int SelectedIndex = Map()->m_vSelectedEnvelopePoints[k].first;
5298 CFixedTime BoundLow = CFixedTime::FromSeconds(Seconds: ScreenToEnvelopeX(View, x: View.x));
5299 CFixedTime BoundHigh = CFixedTime::FromSeconds(Seconds: ScreenToEnvelopeX(View, x: View.x + View.w));
5300 for(int j = 0; j < SelectedIndex; j++)
5301 {
5302 if(!Map()->IsEnvPointSelected(Index: j))
5303 BoundLow = std::max(a: pEnvelope->m_vPoints[j].m_Time + CFixedTime(1), b: BoundLow);
5304 }
5305 for(int j = SelectedIndex + 1; j < (int)pEnvelope->m_vPoints.size(); j++)
5306 {
5307 if(!Map()->IsEnvPointSelected(Index: j))
5308 BoundHigh = std::min(a: pEnvelope->m_vPoints[j].m_Time - CFixedTime(1), b: BoundHigh);
5309 }
5310
5311 DeltaX = ClampDelta(Val: s_vAccurateDragValuesX[k], Delta: DeltaX, Min: BoundLow.GetInternal(), Max: BoundHigh.GetInternal());
5312 }
5313 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
5314 {
5315 int SelectedIndex = Map()->m_vSelectedEnvelopePoints[k].first;
5316 s_vAccurateDragValuesX[k] += DeltaX;
5317 pEnvelope->m_vPoints[SelectedIndex].m_Time = CFixedTime(std::round(x: s_vAccurateDragValuesX[k]));
5318 }
5319 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
5320 {
5321 int SelectedIndex = Map()->m_vSelectedEnvelopePoints[k].first;
5322 if(SelectedIndex == 0 && pEnvelope->m_vPoints[SelectedIndex].m_Time != CFixedTime(0))
5323 {
5324 RemoveTimeOffsetEnvelope(pEnvelope);
5325 float Offset = s_vAccurateDragValuesX[k];
5326 for(auto &Value : s_vAccurateDragValuesX)
5327 Value -= Offset;
5328 break;
5329 }
5330 }
5331 }
5332 }
5333 else
5334 {
5335 if(s_Operation == EEnvelopeEditorOp::DRAG_POINT || s_Operation == EEnvelopeEditorOp::DRAG_POINT_X)
5336 {
5337 s_Operation = EEnvelopeEditorOp::DRAG_POINT_Y;
5338 s_vAccurateDragValuesY.clear();
5339 for(auto [SelectedIndex, SelectedChannel] : Map()->m_vSelectedEnvelopePoints)
5340 s_vAccurateDragValuesY.push_back(x: pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]);
5341 }
5342 else
5343 {
5344 float DeltaY = ScreenToEnvelopeDY(View, DeltaY: Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f);
5345 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
5346 {
5347 auto [SelectedIndex, SelectedChannel] = Map()->m_vSelectedEnvelopePoints[k];
5348 s_vAccurateDragValuesY[k] -= DeltaY;
5349 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: s_vAccurateDragValuesY[k]);
5350
5351 if(pEnvelope->GetChannels() == 1 || pEnvelope->GetChannels() == 4)
5352 {
5353 pEnvelope->m_vPoints[i].m_aValues[c] = std::clamp(val: pEnvelope->m_vPoints[i].m_aValues[c], lo: 0, hi: 1024);
5354 s_vAccurateDragValuesY[k] = std::clamp<float>(val: s_vAccurateDragValuesY[k], lo: 0, hi: 1024);
5355 }
5356 }
5357 }
5358 }
5359 }
5360
5361 if(s_Operation == EEnvelopeEditorOp::CONTEXT_MENU)
5362 {
5363 if(!Ui()->MouseButton(Index: 1))
5364 {
5365 if(Map()->m_vSelectedEnvelopePoints.size() == 1)
5366 {
5367 Map()->m_UpdateEnvPointInfo = true;
5368 ShowPopupEnvPoint();
5369 }
5370 else if(Map()->m_vSelectedEnvelopePoints.size() > 1)
5371 {
5372 static SPopupMenuId s_PopupEnvPointMultiId;
5373 Ui()->DoPopupMenu(pId: &s_PopupEnvPointMultiId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 80, Height: 22, pContext: this, pfnFunc: PopupEnvPointMulti);
5374 }
5375 Ui()->SetActiveItem(nullptr);
5376 s_Operation = EEnvelopeEditorOp::NONE;
5377 }
5378 }
5379 else if(!Ui()->MouseButton(Index: 0))
5380 {
5381 Ui()->SetActiveItem(nullptr);
5382 Map()->m_SelectedQuadEnvelope = -1;
5383
5384 if(s_Operation == EEnvelopeEditorOp::SELECT)
5385 {
5386 if(Input()->ShiftIsPressed())
5387 Map()->ToggleEnvPoint(Index: i, Channel: c);
5388 else
5389 Map()->SelectEnvPoint(Index: i, Channel: c);
5390 }
5391
5392 s_Operation = EEnvelopeEditorOp::NONE;
5393 Map()->OnModify();
5394 }
5395
5396 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
5397 }
5398 else if(Ui()->HotItem() == pId)
5399 {
5400 if(Ui()->MouseButton(Index: 0))
5401 {
5402 Ui()->SetActiveItem(pId);
5403 s_Operation = EEnvelopeEditorOp::SELECT;
5404 Map()->m_SelectedQuadEnvelope = Map()->m_SelectedEnvelope;
5405
5406 s_MouseXStart = Ui()->MouseX();
5407 s_MouseYStart = Ui()->MouseY();
5408 }
5409 else if(Ui()->MouseButtonClicked(Index: 1))
5410 {
5411 if(Input()->ShiftIsPressed())
5412 {
5413 Map()->m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionDeleteEnvelopePoint>(args: Map(), args&: Map()->m_SelectedEnvelope, args&: i));
5414 }
5415 else
5416 {
5417 s_Operation = EEnvelopeEditorOp::CONTEXT_MENU;
5418 if(!Map()->IsEnvPointSelected(Index: i, Channel: c))
5419 Map()->SelectEnvPoint(Index: i, Channel: c);
5420 Ui()->SetActiveItem(pId);
5421 }
5422 }
5423
5424 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
5425 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
5426 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.");
5427 m_pUiGotContext = pId;
5428 }
5429 else
5430 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1.0f);
5431
5432 IGraphics::CQuadItem QuadItem(Final.x, Final.y, Final.w, Final.h);
5433 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
5434 }
5435
5436 // tangent handles for bezier curves
5437 if(i >= 0 && i < (int)pEnvelope->m_vPoints.size())
5438 {
5439 // Out-Tangent handle
5440 if(i < (int)pEnvelope->m_vPoints.size() - 1 && pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER)
5441 {
5442 CUIRect Final;
5443 Final.x = EnvelopeToScreenX(View, x: (pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]).AsSeconds());
5444 Final.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]));
5445 Final.x -= 2.0f;
5446 Final.y -= 2.0f;
5447 Final.w = 4.0f;
5448 Final.h = 4.0f;
5449
5450 // handle logic
5451 const void *pId = &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c];
5452
5453 if(Map()->IsTangentOutPointSelected(Index: i, Channel: c))
5454 {
5455 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
5456 IGraphics::CFreeformItem FreeformItem(
5457 Final.x + Final.w / 2.0f,
5458 Final.y - 1,
5459 Final.x + Final.w / 2.0f,
5460 Final.y - 1,
5461 Final.x + Final.w + 1,
5462 Final.y + Final.h + 1,
5463 Final.x - 1,
5464 Final.y + Final.h + 1);
5465 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
5466 }
5467
5468 if(Ui()->CheckActiveItem(pId))
5469 {
5470 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
5471
5472 if(s_Operation == EEnvelopeEditorOp::SELECT)
5473 {
5474 float dx = s_MouseXStart - Ui()->MouseX();
5475 float dy = s_MouseYStart - Ui()->MouseY();
5476
5477 if(dx * dx + dy * dy > 20.0f)
5478 {
5479 s_Operation = EEnvelopeEditorOp::DRAG_POINT;
5480
5481 s_vAccurateDragValuesX = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c].GetInternal())};
5482 s_vAccurateDragValuesY = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c])};
5483
5484 if(!Map()->IsTangentOutPointSelected(Index: i, Channel: c))
5485 Map()->SelectTangentOutPoint(Index: i, Channel: c);
5486 }
5487 }
5488
5489 if(s_Operation == EEnvelopeEditorOp::DRAG_POINT)
5490 {
5491 float DeltaX = ScreenToEnvelopeDX(View, DeltaX: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f);
5492 float DeltaY = ScreenToEnvelopeDY(View, DeltaY: Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f);
5493 s_vAccurateDragValuesX[0] += DeltaX;
5494 s_vAccurateDragValuesY[0] -= DeltaY;
5495
5496 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = CFixedTime(std::round(x: s_vAccurateDragValuesX[0]));
5497 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c] = std::round(x: s_vAccurateDragValuesY[0]);
5498
5499 // clamp time value
5500 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);
5501 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());
5502 }
5503
5504 if(s_Operation == EEnvelopeEditorOp::CONTEXT_MENU)
5505 {
5506 if(!Ui()->MouseButton(Index: 1))
5507 {
5508 if(Map()->IsTangentOutPointSelected(Index: i, Channel: c))
5509 {
5510 Map()->m_UpdateEnvPointInfo = true;
5511 ShowPopupEnvPoint();
5512 }
5513 Ui()->SetActiveItem(nullptr);
5514 s_Operation = EEnvelopeEditorOp::NONE;
5515 }
5516 }
5517 else if(!Ui()->MouseButton(Index: 0))
5518 {
5519 Ui()->SetActiveItem(nullptr);
5520 Map()->m_SelectedQuadEnvelope = -1;
5521
5522 if(s_Operation == EEnvelopeEditorOp::SELECT)
5523 Map()->SelectTangentOutPoint(Index: i, Channel: c);
5524
5525 s_Operation = EEnvelopeEditorOp::NONE;
5526 Map()->OnModify();
5527 }
5528
5529 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
5530 }
5531 else if(Ui()->HotItem() == pId)
5532 {
5533 if(Ui()->MouseButton(Index: 0))
5534 {
5535 Ui()->SetActiveItem(pId);
5536 s_Operation = EEnvelopeEditorOp::SELECT;
5537 Map()->m_SelectedQuadEnvelope = Map()->m_SelectedEnvelope;
5538
5539 s_MouseXStart = Ui()->MouseX();
5540 s_MouseYStart = Ui()->MouseY();
5541 }
5542 else if(Ui()->MouseButtonClicked(Index: 1))
5543 {
5544 if(Input()->ShiftIsPressed())
5545 {
5546 Map()->SelectTangentOutPoint(Index: i, Channel: c);
5547 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = CFixedTime(0);
5548 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c] = 0.0f;
5549 Map()->OnModify();
5550 }
5551 else
5552 {
5553 s_Operation = EEnvelopeEditorOp::CONTEXT_MENU;
5554 Map()->SelectTangentOutPoint(Index: i, Channel: c);
5555 Ui()->SetActiveItem(pId);
5556 }
5557 }
5558
5559 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
5560 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
5561 str_copy(dst&: m_aTooltip, src: "Bezier out-tangent. Left mouse to drag. Hold ctrl to be more precise. Shift+right click to reset.");
5562 m_pUiGotContext = pId;
5563 }
5564 else
5565 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1.0f);
5566
5567 // draw triangle
5568 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);
5569 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
5570 }
5571
5572 // In-Tangent handle
5573 if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER)
5574 {
5575 CUIRect Final;
5576 Final.x = EnvelopeToScreenX(View, x: (pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]).AsSeconds());
5577 Final.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]));
5578 Final.x -= 2.0f;
5579 Final.y -= 2.0f;
5580 Final.w = 4.0f;
5581 Final.h = 4.0f;
5582
5583 // handle logic
5584 const void *pId = &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c];
5585
5586 if(Map()->IsTangentInPointSelected(Index: i, Channel: c))
5587 {
5588 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
5589 IGraphics::CFreeformItem FreeformItem(
5590 Final.x + Final.w / 2.0f,
5591 Final.y - 1,
5592 Final.x + Final.w / 2.0f,
5593 Final.y - 1,
5594 Final.x + Final.w + 1,
5595 Final.y + Final.h + 1,
5596 Final.x - 1,
5597 Final.y + Final.h + 1);
5598 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
5599 }
5600
5601 if(Ui()->CheckActiveItem(pId))
5602 {
5603 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
5604
5605 if(s_Operation == EEnvelopeEditorOp::SELECT)
5606 {
5607 float dx = s_MouseXStart - Ui()->MouseX();
5608 float dy = s_MouseYStart - Ui()->MouseY();
5609
5610 if(dx * dx + dy * dy > 20.0f)
5611 {
5612 s_Operation = EEnvelopeEditorOp::DRAG_POINT;
5613
5614 s_vAccurateDragValuesX = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c].GetInternal())};
5615 s_vAccurateDragValuesY = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c])};
5616
5617 if(!Map()->IsTangentInPointSelected(Index: i, Channel: c))
5618 Map()->SelectTangentInPoint(Index: i, Channel: c);
5619 }
5620 }
5621
5622 if(s_Operation == EEnvelopeEditorOp::DRAG_POINT)
5623 {
5624 float DeltaX = ScreenToEnvelopeDX(View, DeltaX: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f);
5625 float DeltaY = ScreenToEnvelopeDY(View, DeltaY: Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f);
5626 s_vAccurateDragValuesX[0] += DeltaX;
5627 s_vAccurateDragValuesY[0] -= DeltaY;
5628
5629 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = CFixedTime(std::round(x: s_vAccurateDragValuesX[0]));
5630 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c] = std::round(x: s_vAccurateDragValuesY[0]);
5631
5632 // clamp time value
5633 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));
5634 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);
5635 }
5636
5637 if(s_Operation == EEnvelopeEditorOp::CONTEXT_MENU)
5638 {
5639 if(!Ui()->MouseButton(Index: 1))
5640 {
5641 if(Map()->IsTangentInPointSelected(Index: i, Channel: c))
5642 {
5643 Map()->m_UpdateEnvPointInfo = true;
5644 ShowPopupEnvPoint();
5645 }
5646 Ui()->SetActiveItem(nullptr);
5647 s_Operation = EEnvelopeEditorOp::NONE;
5648 }
5649 }
5650 else if(!Ui()->MouseButton(Index: 0))
5651 {
5652 Ui()->SetActiveItem(nullptr);
5653 Map()->m_SelectedQuadEnvelope = -1;
5654
5655 if(s_Operation == EEnvelopeEditorOp::SELECT)
5656 Map()->SelectTangentInPoint(Index: i, Channel: c);
5657
5658 s_Operation = EEnvelopeEditorOp::NONE;
5659 Map()->OnModify();
5660 }
5661
5662 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
5663 }
5664 else if(Ui()->HotItem() == pId)
5665 {
5666 if(Ui()->MouseButton(Index: 0))
5667 {
5668 Ui()->SetActiveItem(pId);
5669 s_Operation = EEnvelopeEditorOp::SELECT;
5670 Map()->m_SelectedQuadEnvelope = Map()->m_SelectedEnvelope;
5671
5672 s_MouseXStart = Ui()->MouseX();
5673 s_MouseYStart = Ui()->MouseY();
5674 }
5675 else if(Ui()->MouseButtonClicked(Index: 1))
5676 {
5677 if(Input()->ShiftIsPressed())
5678 {
5679 Map()->SelectTangentInPoint(Index: i, Channel: c);
5680 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = CFixedTime(0);
5681 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c] = 0.0f;
5682 Map()->OnModify();
5683 }
5684 else
5685 {
5686 s_Operation = EEnvelopeEditorOp::CONTEXT_MENU;
5687 Map()->SelectTangentInPoint(Index: i, Channel: c);
5688 Ui()->SetActiveItem(pId);
5689 }
5690 }
5691
5692 m_ActiveEnvelopePreview = EEnvelopePreview::SELECTED;
5693 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
5694 str_copy(dst&: m_aTooltip, src: "Bezier in-tangent. Left mouse to drag. Hold ctrl to be more precise. Shift+right click to reset.");
5695 m_pUiGotContext = pId;
5696 }
5697 else
5698 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1.0f);
5699
5700 // draw triangle
5701 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);
5702 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
5703 }
5704 }
5705 }
5706 }
5707 Graphics()->QuadsEnd();
5708 Ui()->ClipDisable();
5709 }
5710
5711 // handle scaling
5712 static float s_ScaleFactorX = 1.0f;
5713 static float s_ScaleFactorY = 1.0f;
5714 static float s_MidpointX = 0.0f;
5715 static float s_MidpointY = 0.0f;
5716 static std::vector<float> s_vInitialPositionsX;
5717 static std::vector<float> s_vInitialPositionsY;
5718 if(s_Operation == EEnvelopeEditorOp::NONE && !s_NameInput.IsActive() && Input()->KeyIsPressed(Key: KEY_S) && !Input()->ModifierIsPressed() && !Map()->m_vSelectedEnvelopePoints.empty())
5719 {
5720 s_Operation = EEnvelopeEditorOp::SCALE;
5721 s_ScaleFactorX = 1.0f;
5722 s_ScaleFactorY = 1.0f;
5723 auto [FirstPointIndex, FirstPointChannel] = Map()->m_vSelectedEnvelopePoints.front();
5724
5725 float MaximumX = pEnvelope->m_vPoints[FirstPointIndex].m_Time.GetInternal();
5726 float MinimumX = MaximumX;
5727 s_vInitialPositionsX.clear();
5728 for(auto [SelectedIndex, _] : Map()->m_vSelectedEnvelopePoints)
5729 {
5730 float Value = pEnvelope->m_vPoints[SelectedIndex].m_Time.GetInternal();
5731 s_vInitialPositionsX.push_back(x: Value);
5732 MaximumX = maximum(a: MaximumX, b: Value);
5733 MinimumX = minimum(a: MinimumX, b: Value);
5734 }
5735 s_MidpointX = (MaximumX - MinimumX) / 2.0f + MinimumX;
5736
5737 float MaximumY = pEnvelope->m_vPoints[FirstPointIndex].m_aValues[FirstPointChannel];
5738 float MinimumY = MaximumY;
5739 s_vInitialPositionsY.clear();
5740 for(auto [SelectedIndex, SelectedChannel] : Map()->m_vSelectedEnvelopePoints)
5741 {
5742 float Value = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel];
5743 s_vInitialPositionsY.push_back(x: Value);
5744 MaximumY = maximum(a: MaximumY, b: Value);
5745 MinimumY = minimum(a: MinimumY, b: Value);
5746 }
5747 s_MidpointY = (MaximumY - MinimumY) / 2.0f + MinimumY;
5748 }
5749
5750 if(s_Operation == EEnvelopeEditorOp::SCALE)
5751 {
5752 str_copy(dst&: m_aTooltip, src: "Press shift to scale the time. Press alt to scale along midpoint. Press ctrl to be more precise.");
5753
5754 if(Input()->ShiftIsPressed())
5755 {
5756 s_ScaleFactorX += Ui()->MouseDeltaX() / Graphics()->ScreenWidth() * (Input()->ModifierIsPressed() ? 0.5f : 10.0f);
5757 float Midpoint = Input()->AltIsPressed() ? s_MidpointX : 0.0f;
5758 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
5759 {
5760 int SelectedIndex = Map()->m_vSelectedEnvelopePoints[k].first;
5761 CFixedTime BoundLow = CFixedTime::FromSeconds(Seconds: ScreenToEnvelopeX(View, x: View.x));
5762 CFixedTime BoundHigh = CFixedTime::FromSeconds(Seconds: ScreenToEnvelopeX(View, x: View.x + View.w));
5763 for(int j = 0; j < SelectedIndex; j++)
5764 {
5765 if(!Map()->IsEnvPointSelected(Index: j))
5766 BoundLow = std::max(a: pEnvelope->m_vPoints[j].m_Time + CFixedTime(1), b: BoundLow);
5767 }
5768 for(int j = SelectedIndex + 1; j < (int)pEnvelope->m_vPoints.size(); j++)
5769 {
5770 if(!Map()->IsEnvPointSelected(Index: j))
5771 BoundHigh = std::min(a: pEnvelope->m_vPoints[j].m_Time - CFixedTime(1), b: BoundHigh);
5772 }
5773
5774 float Value = s_vInitialPositionsX[k];
5775 float ScaleBoundLow = (BoundLow.GetInternal() - Midpoint) / (Value - Midpoint);
5776 float ScaleBoundHigh = (BoundHigh.GetInternal() - Midpoint) / (Value - Midpoint);
5777 float ScaleBoundMin = minimum(a: ScaleBoundLow, b: ScaleBoundHigh);
5778 float ScaleBoundMax = maximum(a: ScaleBoundLow, b: ScaleBoundHigh);
5779 s_ScaleFactorX = std::clamp(val: s_ScaleFactorX, lo: ScaleBoundMin, hi: ScaleBoundMax);
5780 }
5781
5782 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
5783 {
5784 int SelectedIndex = Map()->m_vSelectedEnvelopePoints[k].first;
5785 float ScaleMinimum = s_vInitialPositionsX[k] - Midpoint > CFixedTime(1).AsSeconds() ? CFixedTime(1).AsSeconds() / (s_vInitialPositionsX[k] - Midpoint) : 0.0f;
5786 float ScaleFactor = maximum(a: ScaleMinimum, b: s_ScaleFactorX);
5787 pEnvelope->m_vPoints[SelectedIndex].m_Time = CFixedTime(std::round(x: (s_vInitialPositionsX[k] - Midpoint) * ScaleFactor + Midpoint));
5788 }
5789 for(size_t k = 1; k < pEnvelope->m_vPoints.size(); k++)
5790 {
5791 if(pEnvelope->m_vPoints[k].m_Time <= pEnvelope->m_vPoints[k - 1].m_Time)
5792 pEnvelope->m_vPoints[k].m_Time = pEnvelope->m_vPoints[k - 1].m_Time + CFixedTime(1);
5793 }
5794 for(auto [SelectedIndex, _] : Map()->m_vSelectedEnvelopePoints)
5795 {
5796 if(SelectedIndex == 0 && pEnvelope->m_vPoints[SelectedIndex].m_Time != CFixedTime(0))
5797 {
5798 float Offset = pEnvelope->m_vPoints[0].m_Time.GetInternal();
5799 RemoveTimeOffsetEnvelope(pEnvelope);
5800 s_MidpointX -= Offset;
5801 for(auto &Value : s_vInitialPositionsX)
5802 Value -= Offset;
5803 break;
5804 }
5805 }
5806 }
5807 else
5808 {
5809 s_ScaleFactorY -= Ui()->MouseDeltaY() / Graphics()->ScreenHeight() * (Input()->ModifierIsPressed() ? 0.5f : 10.0f);
5810 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
5811 {
5812 auto [SelectedIndex, SelectedChannel] = Map()->m_vSelectedEnvelopePoints[k];
5813 if(Input()->AltIsPressed())
5814 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: (s_vInitialPositionsY[k] - s_MidpointY) * s_ScaleFactorY + s_MidpointY);
5815 else
5816 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: s_vInitialPositionsY[k] * s_ScaleFactorY);
5817
5818 if(pEnvelope->GetChannels() == 1 || pEnvelope->GetChannels() == 4)
5819 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::clamp(val: pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel], lo: 0, hi: 1024);
5820 }
5821 }
5822
5823 if(Ui()->MouseButton(Index: 0))
5824 {
5825 s_Operation = EEnvelopeEditorOp::NONE;
5826 Map()->m_EnvOpTracker.Stop(Switch: false);
5827 }
5828 else if(Ui()->MouseButton(Index: 1) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
5829 {
5830 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
5831 {
5832 int SelectedIndex = Map()->m_vSelectedEnvelopePoints[k].first;
5833 pEnvelope->m_vPoints[SelectedIndex].m_Time = CFixedTime(std::round(x: s_vInitialPositionsX[k]));
5834 }
5835 for(size_t k = 0; k < Map()->m_vSelectedEnvelopePoints.size(); k++)
5836 {
5837 auto [SelectedIndex, SelectedChannel] = Map()->m_vSelectedEnvelopePoints[k];
5838 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: s_vInitialPositionsY[k]);
5839 }
5840 RemoveTimeOffsetEnvelope(pEnvelope);
5841 s_Operation = EEnvelopeEditorOp::NONE;
5842 }
5843 }
5844
5845 // handle box selection
5846 if(s_Operation == EEnvelopeEditorOp::BOX_SELECT)
5847 {
5848 Ui()->ClipEnable(pRect: &View);
5849 CUIRect SelectionRect;
5850 SelectionRect.x = s_MouseXStart;
5851 SelectionRect.y = s_MouseYStart;
5852 SelectionRect.w = Ui()->MouseX() - s_MouseXStart;
5853 SelectionRect.h = Ui()->MouseY() - s_MouseYStart;
5854 SelectionRect.DrawOutline(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
5855 Ui()->ClipDisable();
5856
5857 if(!Ui()->MouseButton(Index: 0))
5858 {
5859 s_Operation = EEnvelopeEditorOp::NONE;
5860 Ui()->SetActiveItem(nullptr);
5861
5862 float TimeStart = ScreenToEnvelopeX(View, x: s_MouseXStart);
5863 float TimeEnd = ScreenToEnvelopeX(View, x: Ui()->MouseX());
5864 float ValueStart = ScreenToEnvelopeY(View, y: s_MouseYStart);
5865 float ValueEnd = ScreenToEnvelopeY(View, y: Ui()->MouseY());
5866
5867 float TimeMin = minimum(a: TimeStart, b: TimeEnd);
5868 float TimeMax = maximum(a: TimeStart, b: TimeEnd);
5869 float ValueMin = minimum(a: ValueStart, b: ValueEnd);
5870 float ValueMax = maximum(a: ValueStart, b: ValueEnd);
5871
5872 if(!Input()->ShiftIsPressed())
5873 Map()->DeselectEnvPoints();
5874
5875 for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++)
5876 {
5877 for(int c = 0; c < CEnvPoint::MAX_CHANNELS; c++)
5878 {
5879 if(!(s_ActiveChannels & (1 << c)))
5880 continue;
5881
5882 float Time = pEnvelope->m_vPoints[i].m_Time.AsSeconds();
5883 float Value = fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]);
5884
5885 if(in_range(a: Time, lower: TimeMin, upper: TimeMax) && in_range(a: Value, lower: ValueMin, upper: ValueMax))
5886 Map()->ToggleEnvPoint(Index: i, Channel: c);
5887 }
5888 }
5889 }
5890 }
5891 }
5892}
5893
5894void CEditor::RenderEnvelopeEditorColorBar(CUIRect ColorBar, const std::shared_ptr<CEnvelope> &pEnvelope)
5895{
5896 if(pEnvelope->m_vPoints.size() < 2)
5897 {
5898 return;
5899 }
5900 const float ViewStartTime = ScreenToEnvelopeX(View: ColorBar, x: ColorBar.x);
5901 const float ViewEndTime = ScreenToEnvelopeX(View: ColorBar, x: ColorBar.x + ColorBar.w);
5902 if(ViewEndTime < 0.0f || ViewStartTime > pEnvelope->EndTime())
5903 {
5904 return;
5905 }
5906 const float StartX = maximum(a: EnvelopeToScreenX(View: ColorBar, x: 0.0f), b: ColorBar.x);
5907 const float TotalWidth = minimum(a: EnvelopeToScreenX(View: ColorBar, x: pEnvelope->EndTime()) - StartX, b: ColorBar.x + ColorBar.w - StartX);
5908
5909 Ui()->ClipEnable(pRect: &ColorBar);
5910 CUIRect ColorBarBackground = CUIRect{.x: StartX, .y: ColorBar.y, .w: TotalWidth, .h: ColorBar.h};
5911 RenderBackground(View: ColorBarBackground, Texture: m_CheckerTexture, Size: ColorBarBackground.h, Brightness: 1.0f);
5912 Graphics()->TextureClear();
5913 Graphics()->QuadsBegin();
5914
5915 int PointBeginIndex = pEnvelope->FindPointIndex(Time: CFixedTime::FromSeconds(Seconds: ViewStartTime));
5916 if(PointBeginIndex == -1)
5917 {
5918 PointBeginIndex = 0;
5919 }
5920 int PointEndIndex = pEnvelope->FindPointIndex(Time: CFixedTime::FromSeconds(Seconds: ViewEndTime));
5921 if(PointEndIndex == -1)
5922 {
5923 PointEndIndex = (int)pEnvelope->m_vPoints.size() - 2;
5924 }
5925 for(int PointIndex = PointBeginIndex; PointIndex <= PointEndIndex; PointIndex++)
5926 {
5927 const auto &PointStart = pEnvelope->m_vPoints[PointIndex];
5928 const auto &PointEnd = pEnvelope->m_vPoints[PointIndex + 1];
5929 const float PointStartTime = PointStart.m_Time.AsSeconds();
5930 const float PointEndTime = PointEnd.m_Time.AsSeconds();
5931
5932 int Steps;
5933 if(PointStart.m_Curvetype == CURVETYPE_LINEAR || PointStart.m_Curvetype == CURVETYPE_STEP)
5934 {
5935 Steps = 1; // let the GPU do the work
5936 }
5937 else
5938 {
5939 const float ClampedPointStartX = maximum(a: EnvelopeToScreenX(View: ColorBar, x: PointStartTime), b: ColorBar.x);
5940 const float ClampedPointEndX = minimum(a: EnvelopeToScreenX(View: ColorBar, x: PointEndTime), b: ColorBar.x + ColorBar.w);
5941 Steps = std::clamp(val: (int)std::sqrt(x: 5.0f * (ClampedPointEndX - ClampedPointStartX)), lo: 1, hi: 250);
5942 }
5943 const float OverallSectionStartTime = Steps == 1 ? PointStartTime : maximum(a: PointStartTime, b: ViewStartTime);
5944 const float OverallSectionEndTime = Steps == 1 ? PointEndTime : minimum(a: PointEndTime, b: ViewEndTime);
5945 float SectionStartTime = OverallSectionStartTime;
5946 float SectionStartX = EnvelopeToScreenX(View: ColorBar, x: SectionStartTime);
5947 for(int Step = 1; Step <= Steps; Step++)
5948 {
5949 const float SectionEndTime = OverallSectionStartTime + (OverallSectionEndTime - OverallSectionStartTime) * (Step / (float)Steps);
5950 const float SectionEndX = EnvelopeToScreenX(View: ColorBar, x: SectionEndTime);
5951
5952 ColorRGBA StartColor;
5953 if(Step == 1 && OverallSectionStartTime == PointStartTime)
5954 {
5955 StartColor = PointStart.ColorValue();
5956 }
5957 else
5958 {
5959 StartColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
5960 pEnvelope->Eval(Time: SectionStartTime, Result&: StartColor, Channels: 4);
5961 }
5962
5963 ColorRGBA EndColor;
5964 if(PointStart.m_Curvetype == CURVETYPE_STEP)
5965 {
5966 EndColor = StartColor;
5967 }
5968 else if(Step == Steps && OverallSectionEndTime == PointEndTime)
5969 {
5970 EndColor = PointEnd.ColorValue();
5971 }
5972 else
5973 {
5974 EndColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
5975 pEnvelope->Eval(Time: SectionEndTime, Result&: EndColor, Channels: 4);
5976 }
5977
5978 Graphics()->SetColor4(TopLeft: StartColor, TopRight: EndColor, BottomLeft: StartColor, BottomRight: EndColor);
5979 const IGraphics::CQuadItem QuadItem(SectionStartX, ColorBar.y, SectionEndX - SectionStartX, ColorBar.h);
5980 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
5981
5982 SectionStartTime = SectionEndTime;
5983 SectionStartX = SectionEndX;
5984 }
5985 }
5986 Graphics()->QuadsEnd();
5987 Ui()->ClipDisable();
5988 ColorBarBackground.h -= Ui()->Screen()->h / Graphics()->ScreenHeight(); // hack to fix alignment of bottom border
5989 ColorBarBackground.DrawOutline(Color: ColorRGBA(0.7f, 0.7f, 0.7f, 1.0f));
5990}
5991
5992void CEditor::RenderEditorHistory(CUIRect View)
5993{
5994 enum EHistoryType
5995 {
5996 EDITOR_HISTORY,
5997 ENVELOPE_HISTORY,
5998 SERVER_SETTINGS_HISTORY
5999 };
6000
6001 static EHistoryType s_HistoryType = EDITOR_HISTORY;
6002 static int s_ActionSelectedIndex = 0;
6003 static CListBox s_ListBox;
6004 s_ListBox.SetActive(m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen());
6005
6006 const bool GotSelection = s_ListBox.Active() && s_ActionSelectedIndex >= 0 && (size_t)s_ActionSelectedIndex < Map()->m_vSettings.size();
6007
6008 CUIRect ToolBar, Button, Label, List, DragBar;
6009 View.HSplitTop(Cut: 22.0f, pTop: &DragBar, pBottom: nullptr);
6010 DragBar.y -= 2.0f;
6011 DragBar.w += 2.0f;
6012 DragBar.h += 4.0f;
6013 DoEditorDragBar(View, pDragBar: &DragBar, Side: EDragSide::TOP, pValue: &m_aExtraEditorSplits[EXTRAEDITOR_HISTORY]);
6014 View.HSplitTop(Cut: 20.0f, pTop: &ToolBar, pBottom: &View);
6015 View.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &List);
6016 ToolBar.HMargin(Cut: 2.0f, pOtherRect: &ToolBar);
6017
6018 CUIRect TypeButtons, HistoryTypeButton;
6019 const int HistoryTypeBtnSize = 70.0f;
6020 ToolBar.VSplitLeft(Cut: 3 * HistoryTypeBtnSize, pLeft: &TypeButtons, pRight: &Label);
6021
6022 // history type buttons
6023 {
6024 TypeButtons.VSplitLeft(Cut: HistoryTypeBtnSize, pLeft: &HistoryTypeButton, pRight: &TypeButtons);
6025 static int s_EditorHistoryButton = 0;
6026 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))
6027 {
6028 s_HistoryType = EDITOR_HISTORY;
6029 }
6030
6031 TypeButtons.VSplitLeft(Cut: HistoryTypeBtnSize, pLeft: &HistoryTypeButton, pRight: &TypeButtons);
6032 static int s_EnvelopeEditorHistoryButton = 0;
6033 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))
6034 {
6035 s_HistoryType = ENVELOPE_HISTORY;
6036 }
6037
6038 TypeButtons.VSplitLeft(Cut: HistoryTypeBtnSize, pLeft: &HistoryTypeButton, pRight: &TypeButtons);
6039 static int s_ServerSettingsHistoryButton = 0;
6040 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))
6041 {
6042 s_HistoryType = SERVER_SETTINGS_HISTORY;
6043 }
6044 }
6045
6046 SLabelProperties InfoProps;
6047 InfoProps.m_MaxWidth = ToolBar.w - 60.f;
6048 InfoProps.m_EllipsisAtEnd = true;
6049 Label.VSplitLeft(Cut: 8.0f, pLeft: nullptr, pRight: &Label);
6050 Ui()->DoLabel(pRect: &Label, pText: "Editor history. Click on an action to undo all actions above.", Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: InfoProps);
6051
6052 CEditorHistory *pCurrentHistory;
6053 if(s_HistoryType == EDITOR_HISTORY)
6054 pCurrentHistory = &Map()->m_EditorHistory;
6055 else if(s_HistoryType == ENVELOPE_HISTORY)
6056 pCurrentHistory = &Map()->m_EnvelopeEditorHistory;
6057 else if(s_HistoryType == SERVER_SETTINGS_HISTORY)
6058 pCurrentHistory = &Map()->m_ServerSettingsHistory;
6059 else
6060 return;
6061
6062 // delete button
6063 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
6064 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
6065 static int s_DeleteButton = 0;
6066 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)))
6067 {
6068 pCurrentHistory->Clear();
6069 s_ActionSelectedIndex = 0;
6070 }
6071
6072 // actions list
6073 int RedoSize = (int)pCurrentHistory->m_vpRedoActions.size();
6074 int UndoSize = (int)pCurrentHistory->m_vpUndoActions.size();
6075 s_ActionSelectedIndex = RedoSize;
6076 s_ListBox.DoStart(RowHeight: 15.0f, NumItems: RedoSize + UndoSize, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: s_ActionSelectedIndex, pRect: &List);
6077
6078 for(int i = 0; i < RedoSize; i++)
6079 {
6080 const CListboxItem Item = s_ListBox.DoNextItem(pId: &pCurrentHistory->m_vpRedoActions[i], Selected: s_ActionSelectedIndex >= 0 && s_ActionSelectedIndex == i);
6081 if(!Item.m_Visible)
6082 continue;
6083
6084 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
6085
6086 SLabelProperties Props;
6087 Props.m_MaxWidth = Label.w;
6088 Props.m_EllipsisAtEnd = true;
6089 TextRender()->TextColor(Color: {.5f, .5f, .5f});
6090 TextRender()->TextOutlineColor(Color: TextRender()->DefaultTextOutlineColor());
6091 Ui()->DoLabel(pRect: &Label, pText: pCurrentHistory->m_vpRedoActions[i]->DisplayText(), Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
6092 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
6093 }
6094
6095 for(int i = 0; i < UndoSize; i++)
6096 {
6097 const CListboxItem Item = s_ListBox.DoNextItem(pId: &pCurrentHistory->m_vpUndoActions[UndoSize - i - 1], Selected: s_ActionSelectedIndex >= RedoSize && s_ActionSelectedIndex == (i + RedoSize));
6098 if(!Item.m_Visible)
6099 continue;
6100
6101 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
6102
6103 SLabelProperties Props;
6104 Props.m_MaxWidth = Label.w;
6105 Props.m_EllipsisAtEnd = true;
6106 Ui()->DoLabel(pRect: &Label, pText: pCurrentHistory->m_vpUndoActions[UndoSize - i - 1]->DisplayText(), Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
6107 }
6108
6109 { // Base action "Loaded map" that cannot be undone
6110 static int s_BaseAction;
6111 const CListboxItem Item = s_ListBox.DoNextItem(pId: &s_BaseAction, Selected: s_ActionSelectedIndex == RedoSize + UndoSize);
6112 if(Item.m_Visible)
6113 {
6114 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
6115
6116 Ui()->DoLabel(pRect: &Label, pText: "Loaded map", Size: 10.0f, Align: TEXTALIGN_ML);
6117 }
6118 }
6119
6120 const int NewSelected = s_ListBox.DoEnd();
6121 if(s_ActionSelectedIndex != NewSelected)
6122 {
6123 // Figure out if we should undo or redo some actions
6124 // Undo everything until the selected index
6125 if(NewSelected > s_ActionSelectedIndex)
6126 {
6127 for(int i = 0; i < (NewSelected - s_ActionSelectedIndex); i++)
6128 {
6129 pCurrentHistory->Undo();
6130 }
6131 }
6132 else
6133 {
6134 for(int i = 0; i < (s_ActionSelectedIndex - NewSelected); i++)
6135 {
6136 pCurrentHistory->Redo();
6137 }
6138 }
6139 s_ActionSelectedIndex = NewSelected;
6140 }
6141}
6142
6143void CEditor::DoEditorDragBar(CUIRect View, CUIRect *pDragBar, EDragSide Side, float *pValue, float MinValue, float MaxValue)
6144{
6145 enum EDragOperation
6146 {
6147 OP_NONE,
6148 OP_DRAGGING,
6149 OP_CLICKED
6150 };
6151 static EDragOperation s_Operation = OP_NONE;
6152 static float s_InitialMouseY = 0.0f;
6153 static float s_InitialMouseOffsetY = 0.0f;
6154 static float s_InitialMouseX = 0.0f;
6155 static float s_InitialMouseOffsetX = 0.0f;
6156
6157 bool IsVertical = Side == EDragSide::TOP || Side == EDragSide::BOTTOM;
6158
6159 if(Ui()->MouseInside(pRect: pDragBar) && Ui()->HotItem() == pDragBar)
6160 m_CursorType = IsVertical ? CURSOR_RESIZE_V : CURSOR_RESIZE_H;
6161
6162 bool Clicked;
6163 bool Abrupted;
6164 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."))
6165 {
6166 if(s_Operation == OP_NONE && Result == 1)
6167 {
6168 s_InitialMouseY = Ui()->MouseY();
6169 s_InitialMouseOffsetY = Ui()->MouseY() - pDragBar->y;
6170 s_InitialMouseX = Ui()->MouseX();
6171 s_InitialMouseOffsetX = Ui()->MouseX() - pDragBar->x;
6172 s_Operation = OP_CLICKED;
6173 }
6174
6175 if(Clicked || Abrupted)
6176 s_Operation = OP_NONE;
6177
6178 if(s_Operation == OP_CLICKED && absolute(a: IsVertical ? Ui()->MouseY() - s_InitialMouseY : Ui()->MouseX() - s_InitialMouseX) > 5.0f)
6179 s_Operation = OP_DRAGGING;
6180
6181 if(s_Operation == OP_DRAGGING)
6182 {
6183 if(Side == EDragSide::TOP)
6184 *pValue = std::clamp(val: s_InitialMouseOffsetY + View.y + View.h - Ui()->MouseY(), lo: MinValue, hi: MaxValue);
6185 else if(Side == EDragSide::RIGHT)
6186 *pValue = std::clamp(val: Ui()->MouseX() - s_InitialMouseOffsetX - View.x + pDragBar->w, lo: MinValue, hi: MaxValue);
6187 else if(Side == EDragSide::BOTTOM)
6188 *pValue = std::clamp(val: Ui()->MouseY() - s_InitialMouseOffsetY - View.y + pDragBar->h, lo: MinValue, hi: MaxValue);
6189 else if(Side == EDragSide::LEFT)
6190 *pValue = std::clamp(val: s_InitialMouseOffsetX + View.x + View.w - Ui()->MouseX(), lo: MinValue, hi: MaxValue);
6191
6192 m_CursorType = IsVertical ? CURSOR_RESIZE_V : CURSOR_RESIZE_H;
6193 }
6194 }
6195}
6196
6197void CEditor::RenderMenubar(CUIRect MenuBar)
6198{
6199 SPopupMenuProperties PopupProperties;
6200 PopupProperties.m_Corners = IGraphics::CORNER_R | IGraphics::CORNER_B;
6201
6202 CUIRect FileButton;
6203 static int s_FileButton = 0;
6204 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &FileButton, pRight: &MenuBar);
6205 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))
6206 {
6207 static SPopupMenuId s_PopupMenuFileId;
6208 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);
6209 }
6210
6211 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
6212
6213 CUIRect ToolsButton;
6214 static int s_ToolsButton = 0;
6215 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &ToolsButton, pRight: &MenuBar);
6216 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))
6217 {
6218 static SPopupMenuId s_PopupMenuToolsId;
6219 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);
6220 }
6221
6222 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
6223
6224 CUIRect SettingsButton;
6225 static int s_SettingsButton = 0;
6226 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &SettingsButton, pRight: &MenuBar);
6227 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))
6228 {
6229 static SPopupMenuId s_PopupMenuSettingsId;
6230 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);
6231 }
6232
6233 CUIRect ChangedIndicator, Info, Help, Close;
6234 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
6235 MenuBar.VSplitLeft(Cut: MenuBar.h, pLeft: &ChangedIndicator, pRight: &MenuBar);
6236 MenuBar.VSplitRight(Cut: 15.0f, pLeft: &MenuBar, pRight: &Close);
6237 MenuBar.VSplitRight(Cut: 5.0f, pLeft: &MenuBar, pRight: nullptr);
6238 MenuBar.VSplitRight(Cut: 15.0f, pLeft: &MenuBar, pRight: &Help);
6239 MenuBar.VSplitRight(Cut: 5.0f, pLeft: &MenuBar, pRight: nullptr);
6240 MenuBar.VSplitLeft(Cut: MenuBar.w * 0.6f, pLeft: &MenuBar, pRight: &Info);
6241 MenuBar.VSplitRight(Cut: 5.0f, pLeft: &MenuBar, pRight: nullptr);
6242
6243 if(Map()->m_Modified)
6244 {
6245 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
6246 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);
6247 Ui()->DoLabel(pRect: &ChangedIndicator, pText: FontIcon::CIRCLE, Size: 8.0f, Align: TEXTALIGN_MC);
6248 TextRender()->SetRenderFlags(0);
6249 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
6250 static int s_ChangedIndicator;
6251 DoButtonLogic(pId: &s_ChangedIndicator, Checked: 0, pRect: &ChangedIndicator, Flags: BUTTONFLAG_NONE, pToolTip: "This map has unsaved changes."); // just for the tooltip, result unused
6252 }
6253
6254 char aBuf[IO_MAX_PATH_LENGTH + 32];
6255 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "File: %s", Map()->m_aFilename);
6256 SLabelProperties Props;
6257 Props.m_MaxWidth = MenuBar.w;
6258 Props.m_EllipsisAtEnd = true;
6259 Ui()->DoLabel(pRect: &MenuBar, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
6260
6261 char aTimeStr[6];
6262 str_timestamp_format(buffer: aTimeStr, buffer_size: sizeof(aTimeStr), format: "%H:%M");
6263
6264 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);
6265 Ui()->DoLabel(pRect: &Info, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MR);
6266
6267 static int s_HelpButton = 0;
6268 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."))
6269 {
6270 m_QuickActionShowHelp.Call();
6271 }
6272
6273 static int s_CloseButton = 0;
6274 if(DoButton_Editor(pId: &s_CloseButton, pText: "×", Checked: 0, pRect: &Close, Flags: BUTTONFLAG_LEFT, pToolTip: "[Escape] Exit from the editor."))
6275 {
6276 OnClose();
6277 g_Config.m_ClEditor = 0;
6278 }
6279}
6280
6281void CEditor::ShowHelp()
6282{
6283 const char *pLink = Localize(pStr: "https://wiki.ddnet.org/wiki/Mapping");
6284 if(!Client()->ViewLink(pLink))
6285 {
6286 ShowFileDialogError(pFormat: "Failed to open the link '%s' in the default web browser.", pLink);
6287 }
6288}
6289
6290void CEditor::Render()
6291{
6292 // basic start
6293 Graphics()->Clear(r: 0.0f, g: 0.0f, b: 0.0f);
6294 CUIRect View = *Ui()->Screen();
6295 Ui()->MapScreen();
6296 m_CursorType = CURSOR_NORMAL;
6297
6298 float Width = View.w;
6299 float Height = View.h;
6300
6301 // reset tip
6302 str_copy(dst&: m_aTooltip, src: "");
6303
6304 // render checker
6305 RenderBackground(View, Texture: m_CheckerTexture, Size: 32.0f, Brightness: 1.0f);
6306
6307 CUIRect MenuBar, ModeBar, ToolBar, StatusBar, ExtraEditor, ToolBox;
6308 m_ShowPicker = Input()->KeyIsPressed(Key: KEY_SPACE) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Map()->m_vSelectedLayers.size() == 1;
6309
6310 if(m_GuiActive)
6311 {
6312 View.HSplitTop(Cut: 16.0f, pTop: &MenuBar, pBottom: &View);
6313 View.HSplitTop(Cut: 53.0f, pTop: &ToolBar, pBottom: &View);
6314 View.VSplitLeft(Cut: m_ToolBoxWidth, pLeft: &ToolBox, pRight: &View);
6315
6316 View.HSplitBottom(Cut: 16.0f, pTop: &View, pBottom: &StatusBar);
6317 if(!m_ShowPicker && m_ActiveExtraEditor != EXTRAEDITOR_NONE)
6318 View.HSplitBottom(Cut: m_aExtraEditorSplits[(int)m_ActiveExtraEditor], pTop: &View, pBottom: &ExtraEditor);
6319 }
6320 else
6321 {
6322 // hack to get keyboard inputs from toolbar even when GUI is not active
6323 ToolBar.x = -100;
6324 ToolBar.y = -100;
6325 ToolBar.w = 50;
6326 ToolBar.h = 50;
6327 }
6328
6329 // a little hack for now
6330 if(m_Mode == MODE_LAYERS)
6331 DoMapEditor(View);
6332
6333 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
6334 {
6335 // handle undo/redo hotkeys
6336 if(Ui()->CheckActiveItem(pId: nullptr))
6337 {
6338 if(Input()->KeyPress(Key: KEY_Z) && Input()->ModifierIsPressed() && !Input()->ShiftIsPressed())
6339 ActiveHistory().Undo();
6340 if((Input()->KeyPress(Key: KEY_Y) && Input()->ModifierIsPressed()) || (Input()->KeyPress(Key: KEY_Z) && Input()->ModifierIsPressed() && Input()->ShiftIsPressed()))
6341 ActiveHistory().Redo();
6342 }
6343
6344 // handle brush save/load hotkeys
6345 for(int i = KEY_1; i <= KEY_0; i++)
6346 {
6347 if(Input()->KeyPress(Key: i))
6348 {
6349 int Slot = i - KEY_1;
6350 if(Input()->ModifierIsPressed() && !m_pBrush->IsEmpty())
6351 {
6352 dbg_msg(sys: "editor", fmt: "saving current brush to %d", Slot);
6353 m_apSavedBrushes[Slot] = std::make_shared<CLayerGroup>(args&: *m_pBrush);
6354 }
6355 else if(m_apSavedBrushes[Slot])
6356 {
6357 dbg_msg(sys: "editor", fmt: "loading brush from slot %d", Slot);
6358 m_pBrush = std::make_shared<CLayerGroup>(args&: *m_apSavedBrushes[Slot]);
6359 }
6360 }
6361 }
6362 }
6363
6364 const float BackgroundBrightness = 0.26f;
6365 const float BackgroundScale = 80.0f;
6366
6367 if(m_GuiActive)
6368 {
6369 RenderBackground(View: MenuBar, Texture: IGraphics::CTextureHandle(), Size: BackgroundScale, Brightness: 0.0f);
6370 MenuBar.Margin(Cut: 2.0f, pOtherRect: &MenuBar);
6371
6372 RenderBackground(View: ToolBox, Texture: g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, Size: BackgroundScale, Brightness: BackgroundBrightness);
6373 ToolBox.Margin(Cut: 2.0f, pOtherRect: &ToolBox);
6374
6375 RenderBackground(View: ToolBar, Texture: g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, Size: BackgroundScale, Brightness: BackgroundBrightness);
6376 ToolBar.Margin(Cut: 2.0f, pOtherRect: &ToolBar);
6377 ToolBar.VSplitLeft(Cut: m_ToolBoxWidth, pLeft: &ModeBar, pRight: &ToolBar);
6378
6379 RenderBackground(View: StatusBar, Texture: g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, Size: BackgroundScale, Brightness: BackgroundBrightness);
6380 StatusBar.Margin(Cut: 2.0f, pOtherRect: &StatusBar);
6381 }
6382
6383 // do the toolbar
6384 if(m_Mode == MODE_LAYERS)
6385 DoToolbarLayers(ToolBar);
6386 else if(m_Mode == MODE_IMAGES)
6387 DoToolbarImages(ToolBar);
6388 else if(m_Mode == MODE_SOUNDS)
6389 DoToolbarSounds(ToolBar);
6390
6391 if(m_Dialog == DIALOG_NONE)
6392 {
6393 const bool ModPressed = Input()->ModifierIsPressed();
6394 const bool ShiftPressed = Input()->ShiftIsPressed();
6395 const bool AltPressed = Input()->AltIsPressed();
6396
6397 if(CLineInput::GetActiveInput() == nullptr)
6398 {
6399 // ctrl+a to append map
6400 if(Input()->KeyPress(Key: KEY_A) && ModPressed)
6401 {
6402 m_FileBrowser.ShowFileDialog(StorageType: IStorage::TYPE_ALL, FileType: CFileBrowser::EFileType::MAP, pTitle: "Append map", pButtonText: "Append", pInitialPath: "maps", pInitialFilename: "", pfnOpenCallback: CallbackAppendMap, pOpenCallbackUser: this);
6403 }
6404 }
6405
6406 // ctrl+n to create new map
6407 if(Input()->KeyPress(Key: KEY_N) && ModPressed)
6408 {
6409 if(HasUnsavedData())
6410 {
6411 if(!m_PopupEventWasActivated)
6412 {
6413 m_PopupEventType = POPEVENT_NEW;
6414 m_PopupEventActivated = true;
6415 }
6416 }
6417 else
6418 {
6419 Reset();
6420 }
6421 }
6422 // ctrl+o or ctrl+l to open
6423 if((Input()->KeyPress(Key: KEY_O) || Input()->KeyPress(Key: KEY_L)) && ModPressed)
6424 {
6425 if(ShiftPressed)
6426 {
6427 if(!m_QuickActionLoadCurrentMap.Disabled())
6428 {
6429 m_QuickActionLoadCurrentMap.Call();
6430 }
6431 }
6432 else
6433 {
6434 if(HasUnsavedData())
6435 {
6436 if(!m_PopupEventWasActivated)
6437 {
6438 m_PopupEventType = POPEVENT_LOAD;
6439 m_PopupEventActivated = true;
6440 }
6441 }
6442 else
6443 {
6444 m_FileBrowser.ShowFileDialog(StorageType: IStorage::TYPE_ALL, FileType: CFileBrowser::EFileType::MAP, pTitle: "Load map", pButtonText: "Load", pInitialPath: "maps", pInitialFilename: "", pfnOpenCallback: CallbackOpenMap, pOpenCallbackUser: this);
6445 }
6446 }
6447 }
6448
6449 // ctrl+shift+alt+s to save copy
6450 if(Input()->KeyPress(Key: KEY_S) && ModPressed && ShiftPressed && AltPressed)
6451 {
6452 char aDefaultName[IO_MAX_PATH_LENGTH];
6453 fs_split_file_extension(filename: fs_filename(path: Map()->m_aFilename), name: aDefaultName, name_size: sizeof(aDefaultName));
6454 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);
6455 }
6456 // ctrl+shift+s to save as
6457 else if(Input()->KeyPress(Key: KEY_S) && ModPressed && ShiftPressed)
6458 {
6459 m_QuickActionSaveAs.Call();
6460 }
6461 // ctrl+s to save
6462 else if(Input()->KeyPress(Key: KEY_S) && ModPressed)
6463 {
6464 if(Map()->m_aFilename[0] != '\0' && Map()->m_ValidSaveFilename)
6465 {
6466 CallbackSaveMap(pFilename: Map()->m_aFilename, StorageType: IStorage::TYPE_SAVE, pUser: this);
6467 }
6468 else
6469 {
6470 m_FileBrowser.ShowFileDialog(StorageType: IStorage::TYPE_SAVE, FileType: CFileBrowser::EFileType::MAP, pTitle: "Save map", pButtonText: "Save", pInitialPath: "maps", pInitialFilename: "", pfnOpenCallback: CallbackSaveMap, pOpenCallbackUser: this);
6471 }
6472 }
6473 }
6474
6475 if(m_GuiActive)
6476 {
6477 CUIRect DragBar;
6478 ToolBox.VSplitRight(Cut: 1.0f, pLeft: &ToolBox, pRight: &DragBar);
6479 DragBar.x -= 2.0f;
6480 DragBar.w += 4.0f;
6481 DoEditorDragBar(View: ToolBox, pDragBar: &DragBar, Side: EDragSide::RIGHT, pValue: &m_ToolBoxWidth);
6482
6483 if(m_Mode == MODE_LAYERS)
6484 RenderLayers(LayersBox: ToolBox);
6485 else if(m_Mode == MODE_IMAGES)
6486 {
6487 RenderImagesList(ToolBox);
6488 RenderSelectedImage(View);
6489 }
6490 else if(m_Mode == MODE_SOUNDS)
6491 RenderSounds(ToolBox);
6492 }
6493
6494 Ui()->MapScreen();
6495
6496 CUIRect TooltipRect;
6497 if(m_GuiActive)
6498 {
6499 RenderMenubar(MenuBar);
6500 RenderModebar(View: ModeBar);
6501 if(!m_ShowPicker)
6502 {
6503 if(m_ActiveExtraEditor != EXTRAEDITOR_NONE)
6504 {
6505 RenderBackground(View: ExtraEditor, Texture: g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, Size: BackgroundScale, Brightness: BackgroundBrightness);
6506 ExtraEditor.HMargin(Cut: 2.0f, pOtherRect: &ExtraEditor);
6507 ExtraEditor.VSplitRight(Cut: 2.0f, pLeft: &ExtraEditor, pRight: nullptr);
6508 }
6509
6510 static bool s_ShowServerSettingsEditorLast = false;
6511 if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES)
6512 {
6513 RenderEnvelopeEditor(View: ExtraEditor);
6514 }
6515 else if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS)
6516 {
6517 RenderServerSettingsEditor(View: ExtraEditor, ShowServerSettingsEditorLast: s_ShowServerSettingsEditorLast);
6518 }
6519 else if(m_ActiveExtraEditor == EXTRAEDITOR_HISTORY)
6520 {
6521 RenderEditorHistory(View: ExtraEditor);
6522 }
6523 s_ShowServerSettingsEditorLast = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS;
6524 }
6525 RenderStatusbar(View: StatusBar, pTooltipRect: &TooltipRect);
6526 }
6527
6528 RenderPressedKeys(View);
6529 RenderSavingIndicator(View);
6530
6531 if(m_Dialog == DIALOG_MAPSETTINGS_ERROR)
6532 {
6533 static int s_NullUiTarget = 0;
6534 Ui()->SetHotItem(&s_NullUiTarget);
6535 RenderMapSettingsErrorDialog();
6536 }
6537
6538 if(m_PopupEventActivated)
6539 {
6540 static SPopupMenuId s_PopupEventId;
6541 constexpr float PopupWidth = 400.0f;
6542 constexpr float PopupHeight = 150.0f;
6543 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);
6544 m_PopupEventActivated = false;
6545 m_PopupEventWasActivated = true;
6546 }
6547
6548 if(m_Dialog == DIALOG_NONE && !Ui()->IsPopupHovered() && Ui()->MouseInside(pRect: &View))
6549 {
6550 // handle zoom hotkeys
6551 if(CLineInput::GetActiveInput() == nullptr)
6552 {
6553 if(Input()->KeyPress(Key: KEY_KP_MINUS))
6554 MapView()->Zoom()->ChangeValue(Amount: 50.0f);
6555 if(Input()->KeyPress(Key: KEY_KP_PLUS))
6556 MapView()->Zoom()->ChangeValue(Amount: -50.0f);
6557 if(Input()->KeyPress(Key: KEY_KP_MULTIPLY))
6558 MapView()->ResetZoom();
6559 }
6560
6561 if(m_pBrush->IsEmpty() || !Input()->ShiftIsPressed())
6562 {
6563 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
6564 MapView()->Zoom()->ChangeValue(Amount: 20.0f);
6565 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
6566 MapView()->Zoom()->ChangeValue(Amount: -20.0f);
6567 }
6568 if(!m_pBrush->IsEmpty())
6569 {
6570 const bool HasTeleTiles = std::any_of(first: m_pBrush->m_vpLayers.begin(), last: m_pBrush->m_vpLayers.end(), pred: [](const auto &pLayer) {
6571 return pLayer->m_Type == LAYERTYPE_TILES && std::static_pointer_cast<CLayerTiles>(pLayer)->m_HasTele;
6572 });
6573 if(HasTeleTiles)
6574 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.");
6575
6576 if(Input()->ShiftIsPressed())
6577 {
6578 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
6579 AdjustBrushSpecialTiles(UseNextFree: false, Adjust: -1);
6580 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
6581 AdjustBrushSpecialTiles(UseNextFree: false, Adjust: 1);
6582 }
6583
6584 // Use ctrl+f to replace number in brush with next free
6585 if(Input()->ModifierIsPressed() && Input()->KeyPress(Key: KEY_F))
6586 AdjustBrushSpecialTiles(UseNextFree: true);
6587 }
6588 }
6589
6590 for(CEditorComponent &Component : m_vComponents)
6591 Component.OnRender(View);
6592
6593 MapView()->UpdateZoom();
6594
6595 // Cancel color pipette with escape before closing popup menus with escape
6596 if(m_ColorPipetteActive && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
6597 {
6598 m_ColorPipetteActive = false;
6599 }
6600
6601 Ui()->RenderPopupMenus();
6602 FreeDynamicPopupMenus();
6603
6604 UpdateColorPipette();
6605
6606 if(m_Dialog == DIALOG_NONE && !m_PopupEventActivated && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
6607 {
6608 OnClose();
6609 g_Config.m_ClEditor = 0;
6610 }
6611
6612 // The tooltip can be set in popup menus so we have to render the tooltip after the popup menus.
6613 if(m_GuiActive)
6614 RenderTooltip(TooltipRect);
6615
6616 RenderMousePointer();
6617}
6618
6619void CEditor::RenderPressedKeys(CUIRect View)
6620{
6621 if(!g_Config.m_EdShowkeys)
6622 return;
6623
6624 Ui()->MapScreen();
6625 CTextCursor Cursor;
6626 Cursor.SetPosition(vec2(View.x + 10, View.y + View.h - 24 - 10));
6627 Cursor.m_FontSize = 24.0f;
6628
6629 int NKeys = 0;
6630 for(int i = 0; i < KEY_LAST; i++)
6631 {
6632 if(Input()->KeyIsPressed(Key: i))
6633 {
6634 if(NKeys)
6635 TextRender()->TextEx(pCursor: &Cursor, pText: " + ", Length: -1);
6636 TextRender()->TextEx(pCursor: &Cursor, pText: Input()->KeyName(Key: i), Length: -1);
6637 NKeys++;
6638 }
6639 }
6640}
6641
6642void CEditor::RenderSavingIndicator(CUIRect View)
6643{
6644 if(m_WriterFinishJobs.empty())
6645 return;
6646
6647 const char *pText = "Saving…";
6648 const float FontSize = 24.0f;
6649
6650 Ui()->MapScreen();
6651 CUIRect Label, Spinner;
6652 View.Margin(Cut: 20.0f, pOtherRect: &View);
6653 View.HSplitBottom(Cut: FontSize, pTop: nullptr, pBottom: &View);
6654 View.VSplitRight(Cut: TextRender()->TextWidth(Size: FontSize, pText) + 2.0f, pLeft: &Spinner, pRight: &Label);
6655 Spinner.VSplitRight(Cut: Spinner.h, pLeft: nullptr, pRight: &Spinner);
6656 Ui()->DoLabel(pRect: &Label, pText, Size: FontSize, Align: TEXTALIGN_MR);
6657 Ui()->RenderProgressSpinner(Center: Spinner.Center(), OuterRadius: 8.0f);
6658}
6659
6660void CEditor::FreeDynamicPopupMenus()
6661{
6662 auto Iterator = m_PopupMessageContexts.begin();
6663 while(Iterator != m_PopupMessageContexts.end())
6664 {
6665 if(!Ui()->IsPopupOpen(pId: Iterator->second))
6666 {
6667 CUi::SMessagePopupContext *pContext = Iterator->second;
6668 Iterator = m_PopupMessageContexts.erase(position: Iterator);
6669 delete pContext;
6670 }
6671 else
6672 ++Iterator;
6673 }
6674}
6675
6676void CEditor::UpdateColorPipette()
6677{
6678 if(!m_ColorPipetteActive)
6679 return;
6680
6681 static char s_PipetteScreenButton;
6682 if(Ui()->HotItem() == &s_PipetteScreenButton)
6683 {
6684 // Read color one pixel to the top and left as we would otherwise not read the correct
6685 // color due to the cursor sprite being rendered over the current mouse position.
6686 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);
6687 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);
6688 Graphics()->ReadPixel(Position: ivec2(PixelX, PixelY), pColor: &m_PipetteColor);
6689 }
6690
6691 // Simulate button overlaying the entire screen to intercept all clicks for color pipette.
6692 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.");
6693 // Don't handle clicks if we are panning, so the pipette stays active while panning.
6694 // Checking m_pContainerPanned alone is not enough, as this variable is reset when
6695 // panning ends before this function is called.
6696 if(m_pContainerPanned == nullptr && m_pContainerPannedLast == nullptr)
6697 {
6698 if(ButtonResult == 1)
6699 {
6700 char aClipboard[9];
6701 str_format(buffer: aClipboard, buffer_size: sizeof(aClipboard), format: "%08X", m_PipetteColor.PackAlphaLast());
6702 Input()->SetClipboardText(aClipboard);
6703
6704 // Check if any of the saved colors is equal to the picked color and
6705 // bring it to the front of the list instead of adding a duplicate.
6706 int ShiftEnd = (int)std::size(m_aSavedColors) - 1;
6707 for(int i = 0; i < (int)std::size(m_aSavedColors); ++i)
6708 {
6709 if(m_aSavedColors[i].Pack() == m_PipetteColor.Pack())
6710 {
6711 ShiftEnd = i;
6712 break;
6713 }
6714 }
6715 for(int i = ShiftEnd; i > 0; --i)
6716 {
6717 m_aSavedColors[i] = m_aSavedColors[i - 1];
6718 }
6719 m_aSavedColors[0] = m_PipetteColor;
6720 }
6721 if(ButtonResult > 0)
6722 {
6723 m_ColorPipetteActive = false;
6724 }
6725 }
6726}
6727
6728void CEditor::RenderMousePointer()
6729{
6730 if(!m_ShowMousePointer)
6731 return;
6732
6733 constexpr float CursorSize = 16.0f;
6734
6735 // Cursor
6736 Graphics()->WrapClamp();
6737 Graphics()->TextureSet(Texture: m_aCursorTextures[m_CursorType]);
6738 Graphics()->QuadsBegin();
6739 if(m_CursorType == CURSOR_RESIZE_V)
6740 {
6741 Graphics()->QuadsSetRotation(Angle: pi / 2.0f);
6742 }
6743 if(m_pUiGotContext == Ui()->HotItem())
6744 {
6745 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 1.0f);
6746 }
6747 const float CursorOffset = m_CursorType == CURSOR_RESIZE_V || m_CursorType == CURSOR_RESIZE_H ? -CursorSize / 2.0f : 0.0f;
6748 IGraphics::CQuadItem QuadItem(Ui()->MouseX() + CursorOffset, Ui()->MouseY() + CursorOffset, CursorSize, CursorSize);
6749 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
6750 Graphics()->QuadsEnd();
6751 Graphics()->WrapNormal();
6752
6753 // Pipette color
6754 if(m_ColorPipetteActive)
6755 {
6756 CUIRect PipetteRect = {.x: Ui()->MouseX() + CursorSize, .y: Ui()->MouseY() + CursorSize, .w: 80.0f, .h: 20.0f};
6757 if(PipetteRect.x + PipetteRect.w + 2.0f > Ui()->Screen()->w)
6758 {
6759 PipetteRect.x = Ui()->MouseX() - PipetteRect.w - CursorSize / 2.0f;
6760 }
6761 if(PipetteRect.y + PipetteRect.h + 2.0f > Ui()->Screen()->h)
6762 {
6763 PipetteRect.y = Ui()->MouseY() - PipetteRect.h - CursorSize / 2.0f;
6764 }
6765 PipetteRect.Draw(Color: ColorRGBA(0.2f, 0.2f, 0.2f, 0.7f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
6766
6767 CUIRect Pipette, Label;
6768 PipetteRect.VSplitLeft(Cut: PipetteRect.h, pLeft: &Pipette, pRight: &Label);
6769 Pipette.Margin(Cut: 2.0f, pOtherRect: &Pipette);
6770 Pipette.Draw(Color: m_PipetteColor, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
6771
6772 char aLabel[8];
6773 str_format(buffer: aLabel, buffer_size: sizeof(aLabel), format: "#%06X", m_PipetteColor.PackAlphaLast(Alpha: false));
6774 Ui()->DoLabel(pRect: &Label, pText: aLabel, Size: 10.0f, Align: TEXTALIGN_MC);
6775 }
6776}
6777
6778void CEditor::RenderGameEntities(const std::shared_ptr<CLayerTiles> &pTiles)
6779{
6780 const CGameClient *pGameClient = (CGameClient *)Kernel()->RequestInterface<IGameClient>();
6781 const float TileSize = 32.f;
6782
6783 for(int y = 0; y < pTiles->m_Height; y++)
6784 {
6785 for(int x = 0; x < pTiles->m_Width; x++)
6786 {
6787 const unsigned char Index = pTiles->m_pTiles[y * pTiles->m_Width + x].m_Index - ENTITY_OFFSET;
6788 if(!((Index >= ENTITY_FLAGSTAND_RED && Index <= ENTITY_WEAPON_LASER) ||
6789 (Index >= ENTITY_ARMOR_SHOTGUN && Index <= ENTITY_ARMOR_LASER)))
6790 continue;
6791
6792 const bool DDNetOrCustomEntities = std::find_if(first: std::begin(arr: gs_apModEntitiesNames), last: std::end(arr: gs_apModEntitiesNames),
6793 pred: [&](const char *pEntitiesName) { return str_comp_nocase(a: m_SelectEntitiesImage.c_str(), b: pEntitiesName) == 0 &&
6794 str_comp_nocase(a: pEntitiesName, b: "ddnet") != 0; }) == std::end(arr: gs_apModEntitiesNames);
6795
6796 vec2 Pos(x * TileSize, y * TileSize);
6797 vec2 Scale;
6798 int VisualSize;
6799
6800 if(Index == ENTITY_FLAGSTAND_RED)
6801 {
6802 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpriteFlagRed);
6803 Scale = vec2(42, 84);
6804 VisualSize = 1;
6805 Pos.y -= (Scale.y / 2.f) * 0.75f;
6806 }
6807 else if(Index == ENTITY_FLAGSTAND_BLUE)
6808 {
6809 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpriteFlagBlue);
6810 Scale = vec2(42, 84);
6811 VisualSize = 1;
6812 Pos.y -= (Scale.y / 2.f) * 0.75f;
6813 }
6814 else if(Index == ENTITY_ARMOR_1)
6815 {
6816 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmor);
6817 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_HEALTH, ScaleX&: Scale.x, ScaleY&: Scale.y);
6818 VisualSize = 64;
6819 }
6820 else if(Index == ENTITY_HEALTH_1)
6821 {
6822 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupHealth);
6823 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_HEALTH, ScaleX&: Scale.x, ScaleY&: Scale.y);
6824 VisualSize = 64;
6825 }
6826 else if(Index == ENTITY_WEAPON_SHOTGUN)
6827 {
6828 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_aSpritePickupWeapons[WEAPON_SHOTGUN]);
6829 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_SHOTGUN, ScaleX&: Scale.x, ScaleY&: Scale.y);
6830 VisualSize = g_pData->m_Weapons.m_aId[WEAPON_SHOTGUN].m_VisualSize;
6831 }
6832 else if(Index == ENTITY_WEAPON_GRENADE)
6833 {
6834 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_aSpritePickupWeapons[WEAPON_GRENADE]);
6835 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_GRENADE, ScaleX&: Scale.x, ScaleY&: Scale.y);
6836 VisualSize = g_pData->m_Weapons.m_aId[WEAPON_GRENADE].m_VisualSize;
6837 }
6838 else if(Index == ENTITY_WEAPON_LASER)
6839 {
6840 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_aSpritePickupWeapons[WEAPON_LASER]);
6841 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_LASER, ScaleX&: Scale.x, ScaleY&: Scale.y);
6842 VisualSize = g_pData->m_Weapons.m_aId[WEAPON_LASER].m_VisualSize;
6843 }
6844 else if(Index == ENTITY_POWERUP_NINJA)
6845 {
6846 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_aSpritePickupWeapons[WEAPON_NINJA]);
6847 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_NINJA, ScaleX&: Scale.x, ScaleY&: Scale.y);
6848 VisualSize = 128;
6849 }
6850 else if(DDNetOrCustomEntities)
6851 {
6852 if(Index == ENTITY_ARMOR_SHOTGUN)
6853 {
6854 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmorShotgun);
6855 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_SHOTGUN, ScaleX&: Scale.x, ScaleY&: Scale.y);
6856 VisualSize = 64;
6857 }
6858 else if(Index == ENTITY_ARMOR_GRENADE)
6859 {
6860 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmorGrenade);
6861 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_GRENADE, ScaleX&: Scale.x, ScaleY&: Scale.y);
6862 VisualSize = 64;
6863 }
6864 else if(Index == ENTITY_ARMOR_NINJA)
6865 {
6866 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmorNinja);
6867 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_NINJA, ScaleX&: Scale.x, ScaleY&: Scale.y);
6868 VisualSize = 64;
6869 }
6870 else if(Index == ENTITY_ARMOR_LASER)
6871 {
6872 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmorLaser);
6873 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_LASER, ScaleX&: Scale.x, ScaleY&: Scale.y);
6874 VisualSize = 64;
6875 }
6876 else
6877 continue;
6878 }
6879 else
6880 continue;
6881
6882 Graphics()->QuadsBegin();
6883
6884 if(Index != ENTITY_FLAGSTAND_RED && Index != ENTITY_FLAGSTAND_BLUE)
6885 {
6886 const unsigned char Flags = pTiles->m_pTiles[y * pTiles->m_Width + x].m_Flags;
6887
6888 if(Flags & TILEFLAG_XFLIP)
6889 Scale.x = -Scale.x;
6890
6891 if(Flags & TILEFLAG_YFLIP)
6892 Scale.y = -Scale.y;
6893
6894 if(Flags & TILEFLAG_ROTATE)
6895 {
6896 Graphics()->QuadsSetRotation(Angle: 90.f * (pi / 180));
6897
6898 if(Index == ENTITY_POWERUP_NINJA)
6899 {
6900 if(Flags & TILEFLAG_XFLIP)
6901 Pos.y += 10.0f;
6902 else
6903 Pos.y -= 10.0f;
6904 }
6905 }
6906 else
6907 {
6908 if(Index == ENTITY_POWERUP_NINJA)
6909 {
6910 if(Flags & TILEFLAG_XFLIP)
6911 Pos.x += 10.0f;
6912 else
6913 Pos.x -= 10.0f;
6914 }
6915 }
6916 }
6917
6918 Scale *= VisualSize;
6919 Pos -= vec2((Scale.x - TileSize) / 2.f, (Scale.y - TileSize) / 2.f);
6920 Pos += direction(angle: Client()->GlobalTime() * 2.0f + x + y) * 2.5f;
6921
6922 IGraphics::CQuadItem Quad(Pos.x, Pos.y, Scale.x, Scale.y);
6923 Graphics()->QuadsDrawTL(pArray: &Quad, Num: 1);
6924 Graphics()->QuadsEnd();
6925 }
6926 }
6927}
6928
6929void CEditor::RenderSwitchEntities(const std::shared_ptr<CLayerTiles> &pTiles)
6930{
6931 const CGameClient *pGameClient = (CGameClient *)Kernel()->RequestInterface<IGameClient>();
6932 const float TileSize = 32.f;
6933
6934 std::function<unsigned char(int, int, unsigned char &)> GetIndex;
6935 if(pTiles->m_HasSwitch)
6936 {
6937 CLayerSwitch *pSwitchLayer = ((CLayerSwitch *)(pTiles.get()));
6938 GetIndex = [pSwitchLayer](int y, int x, unsigned char &Number) -> unsigned char {
6939 if(x < 0 || y < 0 || x >= pSwitchLayer->m_Width || y >= pSwitchLayer->m_Height)
6940 return 0;
6941 Number = pSwitchLayer->m_pSwitchTile[y * pSwitchLayer->m_Width + x].m_Number;
6942 return pSwitchLayer->m_pSwitchTile[y * pSwitchLayer->m_Width + x].m_Type - ENTITY_OFFSET;
6943 };
6944 }
6945 else
6946 {
6947 GetIndex = [pTiles](int y, int x, unsigned char &Number) -> unsigned char {
6948 if(x < 0 || y < 0 || x >= pTiles->m_Width || y >= pTiles->m_Height)
6949 return 0;
6950 Number = 0;
6951 return pTiles->m_pTiles[y * pTiles->m_Width + x].m_Index - ENTITY_OFFSET;
6952 };
6953 }
6954
6955 ivec2 aOffsets[] = {{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
6956
6957 const ColorRGBA OuterColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClLaserDoorOutlineColor));
6958 const ColorRGBA InnerColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClLaserDoorInnerColor));
6959 const float TicksHead = Client()->GlobalTime() * Client()->GameTickSpeed();
6960
6961 for(int y = 0; y < pTiles->m_Height; y++)
6962 {
6963 for(int x = 0; x < pTiles->m_Width; x++)
6964 {
6965 unsigned char Number = 0;
6966 const unsigned char Index = GetIndex(y, x, Number);
6967
6968 if(Index == ENTITY_DOOR)
6969 {
6970 for(size_t i = 0; i < sizeof(aOffsets) / sizeof(ivec2); ++i)
6971 {
6972 unsigned char NumberDoorLength = 0;
6973 unsigned char IndexDoorLength = GetIndex(y + aOffsets[i].y, x + aOffsets[i].x, NumberDoorLength);
6974 if(IndexDoorLength >= ENTITY_LASER_SHORT && IndexDoorLength <= ENTITY_LASER_LONG)
6975 {
6976 float XOff = std::cos(x: i * pi / 4.0f);
6977 float YOff = std::sin(x: i * pi / 4.0f);
6978 int Length = (IndexDoorLength - ENTITY_LASER_SHORT + 1) * 3;
6979 vec2 Pos(x + 0.5f, y + 0.5f);
6980 vec2 To(x + XOff * Length + 0.5f, y + YOff * Length + 0.5f);
6981 pGameClient->m_Items.RenderLaser(From: To * TileSize, Pos: Pos * TileSize, OuterColor, InnerColor, TicksBody: 1.0f, TicksHead, Type: (int)LASERTYPE_DOOR);
6982 }
6983 }
6984 }
6985 }
6986 }
6987}
6988
6989void CEditor::Reset(bool CreateDefault)
6990{
6991 Ui()->ClosePopupMenus();
6992 Map()->Clean();
6993
6994 for(CEditorComponent &Component : m_vComponents)
6995 Component.OnReset();
6996
6997 m_ToolbarPreviewSound = -1;
6998
6999 // create default layers
7000 if(CreateDefault)
7001 {
7002 m_EditorWasUsedBefore = true;
7003 Map()->CreateDefault();
7004 }
7005
7006 m_pContainerPanned = nullptr;
7007 m_pContainerPannedLast = nullptr;
7008
7009 m_ActiveEnvelopePreview = EEnvelopePreview::NONE;
7010 m_QuadEnvelopePointOperation = EQuadEnvelopePointOperation::NONE;
7011
7012 m_ResetZoomEnvelope = true;
7013 m_SettingsCommandInput.Clear();
7014 m_MapSettingsCommandContext.Reset();
7015}
7016
7017int CEditor::GetTextureUsageFlag() const
7018{
7019 return Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
7020}
7021
7022IGraphics::CTextureHandle CEditor::GetFrontTexture()
7023{
7024 if(!m_FrontTexture.IsValid())
7025 m_FrontTexture = Graphics()->LoadTexture(pFilename: "editor/front.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
7026 return m_FrontTexture;
7027}
7028
7029IGraphics::CTextureHandle CEditor::GetTeleTexture()
7030{
7031 if(!m_TeleTexture.IsValid())
7032 m_TeleTexture = Graphics()->LoadTexture(pFilename: "editor/tele.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
7033 return m_TeleTexture;
7034}
7035
7036IGraphics::CTextureHandle CEditor::GetSpeedupTexture()
7037{
7038 if(!m_SpeedupTexture.IsValid())
7039 m_SpeedupTexture = Graphics()->LoadTexture(pFilename: "editor/speedup.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
7040 return m_SpeedupTexture;
7041}
7042
7043IGraphics::CTextureHandle CEditor::GetSwitchTexture()
7044{
7045 if(!m_SwitchTexture.IsValid())
7046 m_SwitchTexture = Graphics()->LoadTexture(pFilename: "editor/switch.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
7047 return m_SwitchTexture;
7048}
7049
7050IGraphics::CTextureHandle CEditor::GetTuneTexture()
7051{
7052 if(!m_TuneTexture.IsValid())
7053 m_TuneTexture = Graphics()->LoadTexture(pFilename: "editor/tune.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
7054 return m_TuneTexture;
7055}
7056
7057IGraphics::CTextureHandle CEditor::GetEntitiesTexture()
7058{
7059 if(!m_EntitiesTexture.IsValid())
7060 m_EntitiesTexture = Graphics()->LoadTexture(pFilename: "editor/entities/DDNet.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
7061 return m_EntitiesTexture;
7062}
7063
7064void CEditor::Init()
7065{
7066 m_pInput = Kernel()->RequestInterface<IInput>();
7067 m_pClient = Kernel()->RequestInterface<IClient>();
7068 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
7069 m_pConfig = m_pConfigManager->Values();
7070 m_pEngine = Kernel()->RequestInterface<IEngine>();
7071 m_pGraphics = Kernel()->RequestInterface<IGraphics>();
7072 m_pTextRender = Kernel()->RequestInterface<ITextRender>();
7073 m_pStorage = Kernel()->RequestInterface<IStorage>();
7074 m_pSound = Kernel()->RequestInterface<ISound>();
7075 m_UI.Init(pKernel: Kernel());
7076 m_UI.SetPopupMenuClosedCallback([this]() {
7077 m_PopupEventWasActivated = false;
7078 });
7079 m_RenderMap.Init(pGraphics: m_pGraphics, pTextRender: m_pTextRender);
7080 m_ZoomEnvelopeX.OnInit(pEditor: this);
7081 m_ZoomEnvelopeY.OnInit(pEditor: this);
7082
7083 m_vComponents.emplace_back(args&: m_MapView);
7084 m_vComponents.emplace_back(args&: m_MapSettingsBackend);
7085 m_vComponents.emplace_back(args&: m_LayerSelector);
7086 m_vComponents.emplace_back(args&: m_FileBrowser);
7087 m_vComponents.emplace_back(args&: m_Prompt);
7088 m_vComponents.emplace_back(args&: m_FontTyper);
7089 for(CEditorComponent &Component : m_vComponents)
7090 Component.OnInit(pEditor: this);
7091
7092 m_CheckerTexture = Graphics()->LoadTexture(pFilename: "editor/checker.png", StorageType: IStorage::TYPE_ALL);
7093 m_aCursorTextures[CURSOR_NORMAL] = Graphics()->LoadTexture(pFilename: "editor/cursor.png", StorageType: IStorage::TYPE_ALL);
7094 m_aCursorTextures[CURSOR_RESIZE_H] = Graphics()->LoadTexture(pFilename: "editor/cursor_resize.png", StorageType: IStorage::TYPE_ALL);
7095 m_aCursorTextures[CURSOR_RESIZE_V] = m_aCursorTextures[CURSOR_RESIZE_H];
7096
7097 m_pTilesetPicker = std::make_shared<CLayerTiles>(args: Map(), args: 16, args: 16);
7098 m_pTilesetPicker->MakePalette();
7099 m_pTilesetPicker->m_Readonly = true;
7100
7101 m_pQuadsetPicker = std::make_shared<CLayerQuads>(args: Map());
7102 m_pQuadsetPicker->NewQuad(x: 0, y: 0, Width: 64, Height: 64);
7103 m_pQuadsetPicker->m_Readonly = true;
7104
7105 m_pBrush = std::make_shared<CLayerGroup>(args: Map());
7106
7107 Reset(CreateDefault: false);
7108}
7109
7110void CEditor::HandleCursorMovement()
7111{
7112 const vec2 UpdatedMousePos = Ui()->UpdatedMousePos();
7113 const vec2 UpdatedMouseDelta = Ui()->UpdatedMouseDelta();
7114
7115 // fix correct world x and y
7116 const std::shared_ptr<CLayerGroup> pGroup = Map()->SelectedGroup();
7117 if(pGroup)
7118 {
7119 float aPoints[4];
7120 pGroup->Mapping(pPoints: aPoints);
7121
7122 float WorldWidth = aPoints[2] - aPoints[0];
7123 float WorldHeight = aPoints[3] - aPoints[1];
7124
7125 m_MouseWorldScale = WorldWidth / Graphics()->WindowWidth();
7126
7127 m_MouseWorldPos.x = aPoints[0] + WorldWidth * (UpdatedMousePos.x / Graphics()->WindowWidth());
7128 m_MouseWorldPos.y = aPoints[1] + WorldHeight * (UpdatedMousePos.y / Graphics()->WindowHeight());
7129 m_MouseDeltaWorld.x = UpdatedMouseDelta.x * (WorldWidth / Graphics()->WindowWidth());
7130 m_MouseDeltaWorld.y = UpdatedMouseDelta.y * (WorldHeight / Graphics()->WindowHeight());
7131 }
7132 else
7133 {
7134 m_MouseWorldPos = vec2(-1.0f, -1.0f);
7135 m_MouseDeltaWorld = vec2(0.0f, 0.0f);
7136 }
7137
7138 m_MouseWorldNoParaPos = vec2(-1.0f, -1.0f);
7139 for(const std::shared_ptr<CLayerGroup> &pGameGroup : Map()->m_vpGroups)
7140 {
7141 if(!pGameGroup->m_GameGroup)
7142 continue;
7143
7144 float aPoints[4];
7145 pGameGroup->Mapping(pPoints: aPoints);
7146
7147 float WorldWidth = aPoints[2] - aPoints[0];
7148 float WorldHeight = aPoints[3] - aPoints[1];
7149
7150 m_MouseWorldNoParaPos.x = aPoints[0] + WorldWidth * (UpdatedMousePos.x / Graphics()->WindowWidth());
7151 m_MouseWorldNoParaPos.y = aPoints[1] + WorldHeight * (UpdatedMousePos.y / Graphics()->WindowHeight());
7152 }
7153
7154 OnMouseMove(MousePos: UpdatedMousePos);
7155}
7156
7157void CEditor::OnMouseMove(vec2 MousePos)
7158{
7159 m_vHoverTiles.clear();
7160 for(size_t g = 0; g < Map()->m_vpGroups.size(); g++)
7161 {
7162 const std::shared_ptr<CLayerGroup> pGroup = Map()->m_vpGroups[g];
7163 for(size_t l = 0; l < pGroup->m_vpLayers.size(); l++)
7164 {
7165 const std::shared_ptr<CLayer> pLayer = pGroup->m_vpLayers[l];
7166 int LayerType = pLayer->m_Type;
7167 if(LayerType != LAYERTYPE_TILES &&
7168 LayerType != LAYERTYPE_FRONT &&
7169 LayerType != LAYERTYPE_TELE &&
7170 LayerType != LAYERTYPE_SPEEDUP &&
7171 LayerType != LAYERTYPE_SWITCH &&
7172 LayerType != LAYERTYPE_TUNE)
7173 continue;
7174
7175 std::shared_ptr<CLayerTiles> pTiles = std::static_pointer_cast<CLayerTiles>(r: pLayer);
7176 pGroup->MapScreen();
7177 float aPoints[4];
7178 pGroup->Mapping(pPoints: aPoints);
7179 float WorldWidth = aPoints[2] - aPoints[0];
7180 float WorldHeight = aPoints[3] - aPoints[1];
7181 CUIRect Rect;
7182 Rect.x = aPoints[0] + WorldWidth * (MousePos.x / Graphics()->WindowWidth());
7183 Rect.y = aPoints[1] + WorldHeight * (MousePos.y / Graphics()->WindowHeight());
7184 Rect.w = 0;
7185 Rect.h = 0;
7186 CIntRect r;
7187 pTiles->Convert(Rect, pOut: &r);
7188 pTiles->Clamp(pRect: &r);
7189 int x = r.x;
7190 int y = r.y;
7191
7192 if(x < 0 || x >= pTiles->m_Width)
7193 continue;
7194 if(y < 0 || y >= pTiles->m_Height)
7195 continue;
7196 CTile Tile = pTiles->GetTile(x, y);
7197 if(Tile.m_Index)
7198 m_vHoverTiles.emplace_back(
7199 args&: g, args&: l, args&: x, args&: y, args&: Tile);
7200 }
7201 }
7202 Ui()->MapScreen();
7203}
7204
7205void CEditor::MouseAxisLock(vec2 &CursorRel)
7206{
7207 if(Input()->AltIsPressed())
7208 {
7209 // only lock with the paint brush and inside editor map area to avoid duplicate Alt behavior
7210 if(m_pBrush->IsEmpty() || Ui()->HotItem() != &m_MapEditorId)
7211 return;
7212
7213 const vec2 CurrentWorldPos = vec2(Ui()->MouseWorldX(), Ui()->MouseWorldY()) / 32.0f;
7214
7215 if(m_MouseAxisLockState == EAxisLock::START)
7216 {
7217 m_MouseAxisInitialPos = CurrentWorldPos;
7218 m_MouseAxisLockState = EAxisLock::NONE;
7219 return; // delta would be 0, calculate it in next frame
7220 }
7221
7222 const vec2 Delta = CurrentWorldPos - m_MouseAxisInitialPos;
7223
7224 // lock to axis if moved mouse by 1 block
7225 if(m_MouseAxisLockState == EAxisLock::NONE && (std::abs(x: Delta.x) > 1.0f || std::abs(x: Delta.y) > 1.0f))
7226 {
7227 m_MouseAxisLockState = (std::abs(x: Delta.x) > std::abs(x: Delta.y)) ? EAxisLock::HORIZONTAL : EAxisLock::VERTICAL;
7228 }
7229
7230 if(m_MouseAxisLockState == EAxisLock::HORIZONTAL)
7231 {
7232 CursorRel.y = 0;
7233 }
7234 else if(m_MouseAxisLockState == EAxisLock::VERTICAL)
7235 {
7236 CursorRel.x = 0;
7237 }
7238 }
7239 else
7240 {
7241 m_MouseAxisLockState = EAxisLock::START;
7242 }
7243}
7244
7245void CEditor::HandleAutosave()
7246{
7247 const float Time = Client()->GlobalTime();
7248 const float LastAutosaveUpdateTime = m_LastAutosaveUpdateTime;
7249 m_LastAutosaveUpdateTime = Time;
7250
7251 if(g_Config.m_EdAutosaveInterval == 0)
7252 return; // autosave disabled
7253 if(!Map()->m_ModifiedAuto || Map()->m_LastModifiedTime < 0.0f)
7254 return; // no unsaved changes
7255
7256 // Add time to autosave timer if the editor was disabled for more than 10 seconds,
7257 // to prevent autosave from immediately activating when the editor is activated
7258 // after being deactivated for some time.
7259 if(LastAutosaveUpdateTime >= 0.0f && Time - LastAutosaveUpdateTime > 10.0f)
7260 {
7261 Map()->m_LastSaveTime += Time - LastAutosaveUpdateTime;
7262 }
7263
7264 // Check if autosave timer has expired.
7265 if(Map()->m_LastSaveTime >= Time || Time - Map()->m_LastSaveTime < 60 * g_Config.m_EdAutosaveInterval)
7266 return;
7267
7268 // Wait for 5 seconds of no modification before saving, to prevent autosave
7269 // from immediately activating when a map is first modified or while user is
7270 // modifying the map, but don't delay the autosave for more than 1 minute.
7271 if(Time - Map()->m_LastModifiedTime < 5.0f && Time - Map()->m_LastSaveTime < 60 * (g_Config.m_EdAutosaveInterval + 1))
7272 return;
7273
7274 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
7275 ShowFileDialogError(pFormat: "%s", pErrorMessage);
7276 log_error("editor/autosave", "%s", pErrorMessage);
7277 };
7278 Map()->PerformAutosave(ErrorHandler);
7279}
7280
7281void CEditor::HandleWriterFinishJobs()
7282{
7283 if(m_WriterFinishJobs.empty())
7284 return;
7285
7286 std::shared_ptr<CDataFileWriterFinishJob> pJob = m_WriterFinishJobs.front();
7287 if(!pJob->Done())
7288 return;
7289 m_WriterFinishJobs.pop_front();
7290
7291 const char *pErrorMessage = pJob->ErrorMessage();
7292 if(pErrorMessage[0] != '\0')
7293 {
7294 ShowFileDialogError(pFormat: "%s", pErrorMessage);
7295 return;
7296 }
7297
7298 // send rcon.. if we can
7299 if(Client()->RconAuthed() && g_Config.m_EdAutoMapReload)
7300 {
7301 CServerInfo CurrentServerInfo;
7302 Client()->GetServerInfo(pServerInfo: &CurrentServerInfo);
7303
7304 if(net_addr_is_local(addr: &Client()->ServerAddress()))
7305 {
7306 char aMapName[MAX_MAP_LENGTH];
7307 IStorage::StripPathAndExtension(pFilename: pJob->RealFilename(), pBuffer: aMapName, BufferSize: sizeof(aMapName));
7308 if(!str_comp(a: aMapName, b: CurrentServerInfo.m_aMap))
7309 Client()->Rcon(pLine: "hot_reload");
7310 }
7311 }
7312}
7313
7314void CEditor::OnUpdate()
7315{
7316 CUIElementBase::Init(pUI: Ui()); // update static pointer because game and editor use separate UI
7317
7318 if(!m_EditorWasUsedBefore)
7319 {
7320 m_EditorWasUsedBefore = true;
7321 Reset();
7322 }
7323
7324 m_pContainerPannedLast = m_pContainerPanned;
7325
7326 // handle mouse movement
7327 vec2 CursorRel = vec2(0.0f, 0.0f);
7328 IInput::ECursorType CursorType = Input()->CursorRelative(pX: &CursorRel.x, pY: &CursorRel.y);
7329 if(CursorType != IInput::CURSOR_NONE)
7330 {
7331 Ui()->ConvertMouseMove(pX: &CursorRel.x, pY: &CursorRel.y, CursorType);
7332 MouseAxisLock(CursorRel);
7333 Ui()->OnCursorMove(X: CursorRel.x, Y: CursorRel.y);
7334 }
7335
7336 // handle key presses
7337 Input()->ConsumeEvents(Consumer: [&](const IInput::CEvent &Event) {
7338 if(m_Dialog == DIALOG_NONE &&
7339 CLineInput::GetActiveInput() == nullptr &&
7340 Event.m_Key == KEY_F1)
7341 {
7342 if((Event.m_Flags & IInput::FLAG_PRESS) != 0 &&
7343 (Event.m_Flags & IInput::FLAG_REPEAT) == 0)
7344 {
7345 m_QuickActionShowHelp.Call();
7346 }
7347 return;
7348 }
7349
7350 for(CEditorComponent &Component : m_vComponents)
7351 {
7352 // Events with flag `FLAG_RELEASE` must always be forwarded to all components so keys being
7353 // released can be handled in all components also after some components have been disabled.
7354 if(Component.OnInput(Event) && (Event.m_Flags & ~IInput::FLAG_RELEASE) != 0)
7355 return;
7356 }
7357 Ui()->OnInput(Event);
7358 });
7359
7360 HandleCursorMovement();
7361 HandleAutosave();
7362 HandleWriterFinishJobs();
7363
7364 for(CEditorComponent &Component : m_vComponents)
7365 Component.OnUpdate();
7366}
7367
7368void CEditor::OnRender()
7369{
7370 Ui()->SetMouseSlow(false);
7371
7372 // toggle gui
7373 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_TAB))
7374 m_GuiActive = !m_GuiActive;
7375
7376 if(Input()->KeyPress(Key: KEY_F10))
7377 m_ShowMousePointer = false;
7378
7379 if(m_Animate)
7380 m_AnimateTime = (time_get() - m_AnimateStart) / (float)time_freq();
7381 else
7382 m_AnimateTime = 0;
7383
7384 m_pUiGotContext = nullptr;
7385 Ui()->StartCheck();
7386
7387 Ui()->Update(MouseWorldPos: m_MouseWorldPos);
7388
7389 Render();
7390
7391 m_MouseDeltaWorld = vec2(0.0f, 0.0f);
7392
7393 if(Input()->KeyPress(Key: KEY_F10))
7394 {
7395 Graphics()->TakeScreenshot(pFilename: nullptr);
7396 m_ShowMousePointer = true;
7397 }
7398
7399 if(g_Config.m_Debug)
7400 Ui()->DebugRender(X: 2.0f, Y: Ui()->Screen()->h - 27.0f);
7401
7402 Ui()->FinishCheck();
7403 Ui()->ClearHotkeys();
7404 Input()->Clear();
7405
7406 CLineInput::RenderCandidates();
7407
7408#if defined(CONF_DEBUG)
7409 Map()->CheckIntegrity();
7410#endif
7411}
7412
7413void CEditor::OnActivate()
7414{
7415 ResetMentions();
7416 ResetIngameMoved();
7417}
7418
7419void CEditor::OnWindowResize()
7420{
7421 Ui()->OnWindowResize();
7422}
7423
7424void CEditor::OnClose()
7425{
7426 m_ColorPipetteActive = false;
7427
7428 if(m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
7429 Sound()->Pause(SampleId: m_ToolbarPreviewSound);
7430
7431 m_FileBrowser.OnEditorClose();
7432}
7433
7434void CEditor::OnDialogClose()
7435{
7436 m_Dialog = DIALOG_NONE;
7437 m_FileBrowser.OnDialogClose();
7438}
7439
7440void CEditor::LoadCurrentMap()
7441{
7442 CGameClient *pGameClient = (CGameClient *)Kernel()->RequestInterface<IGameClient>();
7443
7444 if(Load(pFilename: pGameClient->Map()->Path(), StorageType: IStorage::TYPE_SAVE))
7445 {
7446 Map()->m_ValidSaveFilename = !str_startswith(str: pGameClient->Map()->Path(), prefix: "downloadedmaps/");
7447 }
7448 else
7449 {
7450 Load(pFilename: pGameClient->Map()->Path(), StorageType: IStorage::TYPE_ALL);
7451 Map()->m_ValidSaveFilename = false;
7452 }
7453
7454 vec2 Center = pGameClient->m_Camera.m_Center;
7455 MapView()->SetWorldOffset(Center);
7456}
7457
7458bool CEditor::Save(const char *pFilename)
7459{
7460 // Check if file with this name is already being saved at the moment
7461 if(std::any_of(first: std::begin(cont&: m_WriterFinishJobs), last: std::end(cont&: m_WriterFinishJobs), pred: [pFilename](const std::shared_ptr<CDataFileWriterFinishJob> &Job) {
7462 return str_comp(a: pFilename, b: Job->RealFilename()) == 0;
7463 }))
7464 {
7465 return false;
7466 }
7467
7468 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
7469 ShowFileDialogError(pFormat: "%s", pErrorMessage);
7470 log_error("editor/save", "%s", pErrorMessage);
7471 };
7472 return Map()->Save(pFilename, ErrorHandler);
7473}
7474
7475bool CEditor::HandleMapDrop(const char *pFilename, int StorageType)
7476{
7477 OnDialogClose();
7478 if(HasUnsavedData())
7479 {
7480 str_copy(dst&: m_aFilenamePendingLoad, src: pFilename);
7481 m_PopupEventType = CEditor::POPEVENT_LOADDROP;
7482 m_PopupEventActivated = true;
7483 return true;
7484 }
7485 else
7486 {
7487 return Load(pFilename, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE);
7488 }
7489}
7490
7491bool CEditor::Load(const char *pFilename, int StorageType)
7492{
7493 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
7494 ShowFileDialogError(pFormat: "%s", pErrorMessage);
7495 log_error("editor/load", "%s", pErrorMessage);
7496 };
7497
7498 Reset();
7499 bool Result = Map()->Load(pFilename, StorageType, ErrorHandler: std::move(ErrorHandler));
7500 if(Result)
7501 {
7502 for(CEditorComponent &Component : m_vComponents)
7503 Component.OnMapLoad();
7504
7505 log_info("editor/load", "Loaded map '%s'", Map()->m_aFilename);
7506 }
7507 return Result;
7508}
7509
7510CEditorHistory &CEditor::ActiveHistory()
7511{
7512 if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS)
7513 {
7514 return Map()->m_ServerSettingsHistory;
7515 }
7516 else if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES)
7517 {
7518 return Map()->m_EnvelopeEditorHistory;
7519 }
7520 else
7521 {
7522 return Map()->m_EditorHistory;
7523 }
7524}
7525
7526void CEditor::AdjustBrushSpecialTiles(bool UseNextFree, int Adjust)
7527{
7528 // Adjust m_Angle of speedup or m_Number field of tune, switch and tele tiles by `Adjust` if `UseNextFree` is false
7529 // If `Adjust` is 0 and `UseNextFree` is false, then update numbers of brush tiles to global values
7530 // If true, then use the next free number instead
7531
7532 auto &&AdjustNumber = [Adjust](auto &Number, short Limit = 255) {
7533 Number = ((Number + Adjust) - 1 + Limit) % Limit + 1;
7534 };
7535
7536 for(auto &pLayer : m_pBrush->m_vpLayers)
7537 {
7538 if(pLayer->m_Type != LAYERTYPE_TILES)
7539 continue;
7540
7541 std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(r: pLayer);
7542
7543 if(pLayerTiles->m_HasTele)
7544 {
7545 int NextFreeTeleNumber = Map()->m_pTeleLayer->FindNextFreeNumber(Checkpoint: false);
7546 int NextFreeCPNumber = Map()->m_pTeleLayer->FindNextFreeNumber(Checkpoint: true);
7547 std::shared_ptr<CLayerTele> pTeleLayer = std::static_pointer_cast<CLayerTele>(r: pLayer);
7548
7549 for(int y = 0; y < pTeleLayer->m_Height; y++)
7550 {
7551 for(int x = 0; x < pTeleLayer->m_Width; x++)
7552 {
7553 int i = y * pTeleLayer->m_Width + x;
7554 if(!IsValidTeleTile(Index: pTeleLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pTeleLayer->m_pTeleTile[i].m_Number))
7555 continue;
7556
7557 if(UseNextFree)
7558 {
7559 if(IsTeleTileCheckpoint(Index: pTeleLayer->m_pTiles[i].m_Index))
7560 pTeleLayer->m_pTeleTile[i].m_Number = NextFreeCPNumber;
7561 else if(IsTeleTileNumberUsedAny(Index: pTeleLayer->m_pTiles[i].m_Index))
7562 pTeleLayer->m_pTeleTile[i].m_Number = NextFreeTeleNumber;
7563 }
7564 else
7565 AdjustNumber(pTeleLayer->m_pTeleTile[i].m_Number);
7566
7567 if(!UseNextFree && Adjust == 0 && IsTeleTileNumberUsedAny(Index: pTeleLayer->m_pTiles[i].m_Index))
7568 {
7569 if(IsTeleTileCheckpoint(Index: pTeleLayer->m_pTiles[i].m_Index))
7570 pTeleLayer->m_pTeleTile[i].m_Number = m_TeleCheckpointNumber;
7571 else
7572 pTeleLayer->m_pTeleTile[i].m_Number = m_TeleNumber;
7573 }
7574 }
7575 }
7576 }
7577 else if(pLayerTiles->m_HasTune)
7578 {
7579 if(!UseNextFree)
7580 {
7581 std::shared_ptr<CLayerTune> pTuneLayer = std::static_pointer_cast<CLayerTune>(r: pLayer);
7582 for(int y = 0; y < pTuneLayer->m_Height; y++)
7583 {
7584 for(int x = 0; x < pTuneLayer->m_Width; x++)
7585 {
7586 int i = y * pTuneLayer->m_Width + x;
7587 if(!IsValidTuneTile(Index: pTuneLayer->m_pTiles[i].m_Index) || !pTuneLayer->m_pTuneTile[i].m_Number)
7588 continue;
7589
7590 AdjustNumber(pTuneLayer->m_pTuneTile[i].m_Number);
7591 }
7592 }
7593 }
7594 }
7595 else if(pLayerTiles->m_HasSwitch)
7596 {
7597 int NextFreeNumber = Map()->m_pSwitchLayer->FindNextFreeNumber();
7598 std::shared_ptr<CLayerSwitch> pSwitchLayer = std::static_pointer_cast<CLayerSwitch>(r: pLayer);
7599
7600 for(int y = 0; y < pSwitchLayer->m_Height; y++)
7601 {
7602 for(int x = 0; x < pSwitchLayer->m_Width; x++)
7603 {
7604 int i = y * pSwitchLayer->m_Width + x;
7605 if(!IsValidSwitchTile(Index: pSwitchLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pSwitchLayer->m_pSwitchTile[i].m_Number))
7606 continue;
7607
7608 if(UseNextFree)
7609 pSwitchLayer->m_pSwitchTile[i].m_Number = NextFreeNumber;
7610 else
7611 AdjustNumber(pSwitchLayer->m_pSwitchTile[i].m_Number);
7612 }
7613 }
7614 }
7615 else if(pLayerTiles->m_HasSpeedup)
7616 {
7617 if(!UseNextFree)
7618 {
7619 std::shared_ptr<CLayerSpeedup> pSpeedupLayer = std::static_pointer_cast<CLayerSpeedup>(r: pLayer);
7620 for(int y = 0; y < pSpeedupLayer->m_Height; y++)
7621 {
7622 for(int x = 0; x < pSpeedupLayer->m_Width; x++)
7623 {
7624 int i = y * pSpeedupLayer->m_Width + x;
7625 if(!IsValidSpeedupTile(Index: pSpeedupLayer->m_pTiles[i].m_Index))
7626 continue;
7627
7628 if(Adjust != 0)
7629 {
7630 AdjustNumber(pSpeedupLayer->m_pSpeedupTile[i].m_Angle, 359);
7631 }
7632 else
7633 {
7634 pSpeedupLayer->m_pSpeedupTile[i].m_Angle = m_SpeedupAngle;
7635 pSpeedupLayer->m_SpeedupAngle = m_SpeedupAngle;
7636 }
7637 }
7638 }
7639 }
7640 }
7641 }
7642}
7643
7644IEditor *CreateEditor() { return new CEditor; }
7645