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