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/dbg.h>
11#include <base/fs.h>
12#include <base/io.h>
13#include <base/log.h>
14#include <base/mem.h>
15#include <base/str.h>
16#include <base/time.h>
17
18#include <engine/client.h>
19#include <engine/engine.h>
20#include <engine/font_icons.h>
21#include <engine/gfx/image_loader.h>
22#include <engine/gfx/image_manipulation.h>
23#include <engine/graphics.h>
24#include <engine/input.h>
25#include <engine/keys.h>
26#include <engine/shared/config.h>
27#include <engine/storage.h>
28#include <engine/textrender.h>
29
30#include <generated/client_data.h>
31
32#include <game/client/components/camera.h>
33#include <game/client/gameclient.h>
34#include <game/client/lineinput.h>
35#include <game/client/ui.h>
36#include <game/client/ui_listbox.h>
37#include <game/client/ui_scrollregion.h>
38#include <game/editor/editor_history.h>
39#include <game/editor/mapitems/image.h>
40#include <game/editor/mapitems/sound.h>
41#include <game/localization.h>
42
43#include <algorithm>
44#include <chrono>
45#include <iterator>
46#include <limits>
47#include <tuple>
48#include <type_traits>
49
50static const char *VANILLA_IMAGES[] = {
51 "bg_cloud1",
52 "bg_cloud2",
53 "bg_cloud3",
54 "desert_doodads",
55 "desert_main",
56 "desert_mountains",
57 "desert_mountains2",
58 "desert_sun",
59 "generic_deathtiles",
60 "generic_unhookable",
61 "grass_doodads",
62 "grass_main",
63 "jungle_background",
64 "jungle_deathtiles",
65 "jungle_doodads",
66 "jungle_main",
67 "jungle_midground",
68 "jungle_unhookables",
69 "moon",
70 "mountains",
71 "snow",
72 "stars",
73 "sun",
74 "winter_doodads",
75 "winter_main",
76 "winter_mountains",
77 "winter_mountains2",
78 "winter_mountains3"};
79
80bool CEditor::IsVanillaImage(const char *pImage)
81{
82 return std::any_of(first: std::begin(arr&: VANILLA_IMAGES), last: std::end(arr&: VANILLA_IMAGES), pred: [pImage](const char *pVanillaImage) { return str_comp(a: pImage, b: pVanillaImage) == 0; });
83}
84
85bool CEditor::CallbackOpenMap(const char *pFilename, int StorageType, void *pUser)
86{
87 CEditor *pEditor = (CEditor *)pUser;
88 if(pEditor->Load(pFilename, StorageType))
89 {
90 pEditor->Map()->m_ValidSaveFilename = StorageType == IStorage::TYPE_SAVE && pEditor->m_FileBrowser.IsValidSaveFilename();
91 if(pEditor->m_Dialog == DIALOG_FILE)
92 {
93 pEditor->OnDialogClose();
94 }
95 return true;
96 }
97 else
98 {
99 pEditor->ShowFileDialogError(pFormat: "Failed to load map from file '%s'.", pFilename);
100 return false;
101 }
102}
103
104bool CEditor::CallbackAppendMap(const char *pFilename, int StorageType, void *pUser)
105{
106 CEditor *pEditor = (CEditor *)pUser;
107 const auto &&ErrorHandler = [pEditor](const char *pErrorMessage) {
108 pEditor->ShowFileDialogError(pFormat: "%s", pErrorMessage);
109 log_error("editor/append", "%s", pErrorMessage);
110 };
111 if(pEditor->Map()->Append(pFilename, StorageType, IgnoreHistory: false, ErrorHandler))
112 {
113 pEditor->OnDialogClose();
114 return true;
115 }
116 else
117 {
118 pEditor->ShowFileDialogError(pFormat: "Failed to load map from file '%s'.", pFilename);
119 return false;
120 }
121}
122
123bool CEditor::CallbackSaveMap(const char *pFilename, int StorageType, void *pUser)
124{
125 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
126
127 CEditor *pEditor = static_cast<CEditor *>(pUser);
128
129 // Save map to specified file
130 if(pEditor->Save(pFilename))
131 {
132 if(pEditor->Map()->m_aFilename != pFilename)
133 {
134 str_copy(dst&: pEditor->Map()->m_aFilename, src: pFilename);
135 }
136 pEditor->Map()->m_ValidSaveFilename = true;
137 pEditor->Map()->m_Modified = false;
138 }
139 else
140 {
141 pEditor->ShowFileDialogError(pFormat: "Failed to save map to file '%s'.", pFilename);
142 return false;
143 }
144
145 // Also update autosave if it's older than half the configured autosave interval, so we also have periodic backups.
146 const float Time = pEditor->Client()->GlobalTime();
147 if(g_Config.m_EdAutosaveInterval > 0 && pEditor->Map()->m_LastSaveTime < Time && Time - pEditor->Map()->m_LastSaveTime > 30 * g_Config.m_EdAutosaveInterval)
148 {
149 const auto &&ErrorHandler = [pEditor](const char *pErrorMessage) {
150 pEditor->ShowFileDialogError(pFormat: "%s", pErrorMessage);
151 log_error("editor/autosave", "%s", pErrorMessage);
152 };
153 if(!pEditor->Map()->PerformAutosave(ErrorHandler))
154 return false;
155 }
156
157 pEditor->OnDialogClose();
158 return true;
159}
160
161bool CEditor::CallbackSaveCopyMap(const char *pFilename, int StorageType, void *pUser)
162{
163 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
164
165 CEditor *pEditor = static_cast<CEditor *>(pUser);
166
167 if(pEditor->Save(pFilename))
168 {
169 pEditor->OnDialogClose();
170 return true;
171 }
172 else
173 {
174 pEditor->ShowFileDialogError(pFormat: "Failed to save map to file '%s'.", pFilename);
175 return false;
176 }
177}
178
179bool CEditor::CallbackSaveImage(const char *pFilename, int StorageType, void *pUser)
180{
181 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
182
183 CEditor *pEditor = static_cast<CEditor *>(pUser);
184
185 std::shared_ptr<CEditorImage> pImg = pEditor->Map()->SelectedImage();
186
187 if(CImageLoader::SavePng(File: pEditor->Storage()->OpenFile(pFilename, Flags: IOFLAG_WRITE, Type: StorageType), pFilename, Image: *pImg))
188 {
189 pEditor->OnDialogClose();
190 return true;
191 }
192 else
193 {
194 pEditor->ShowFileDialogError(pFormat: "Failed to write image to file '%s'.", pFilename);
195 return false;
196 }
197}
198
199bool CEditor::CallbackSaveSound(const char *pFilename, int StorageType, void *pUser)
200{
201 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
202
203 CEditor *pEditor = static_cast<CEditor *>(pUser);
204
205 std::shared_ptr<CEditorSound> pSound = pEditor->Map()->SelectedSound();
206
207 IOHANDLE File = pEditor->Storage()->OpenFile(pFilename, Flags: IOFLAG_WRITE, Type: StorageType);
208 if(File)
209 {
210 io_write(io: File, buffer: pSound->m_pData, size: pSound->m_DataSize);
211 io_close(io: File);
212 pEditor->OnDialogClose();
213 return true;
214 }
215 pEditor->ShowFileDialogError(pFormat: "Failed to open file '%s'.", pFilename);
216 return false;
217}
218
219bool CEditor::CallbackCustomEntities(const char *pFilename, int StorageType, void *pUser)
220{
221 CEditor *pEditor = (CEditor *)pUser;
222
223 char aBuf[IO_MAX_PATH_LENGTH];
224 fs_split_file_extension(filename: fs_filename(path: pFilename), name: aBuf, name_size: sizeof(aBuf));
225
226 if(std::find(first: pEditor->m_vSelectEntitiesFiles.begin(), last: pEditor->m_vSelectEntitiesFiles.end(), val: std::string(aBuf)) != pEditor->m_vSelectEntitiesFiles.end())
227 {
228 pEditor->ShowFileDialogError(pFormat: "Custom entities cannot have the same name as default entities.");
229 return false;
230 }
231
232 CImageInfo ImgInfo;
233 if(!pEditor->Graphics()->LoadPng(Image&: ImgInfo, pFilename, StorageType))
234 {
235 pEditor->ShowFileDialogError(pFormat: "Failed to load image from file '%s'.", pFilename);
236 return false;
237 }
238
239 pEditor->m_SelectEntitiesImage = aBuf;
240 pEditor->m_AllowPlaceUnusedTiles = EUnusedEntities::ALLOWED_IMPLICIT;
241 pEditor->m_PreventUnusedTilesWasWarned = false;
242
243 pEditor->Graphics()->UnloadTexture(pIndex: &pEditor->m_EntitiesTexture);
244 pEditor->m_EntitiesTexture = pEditor->Graphics()->LoadTextureRawMove(Image&: ImgInfo, Flags: pEditor->Graphics()->TextureLoadFlags());
245
246 pEditor->OnDialogClose();
247 return true;
248}
249
250void CEditor::DoAudioPreview(CUIRect View, const void *pPlayPauseButtonId, const void *pStopButtonId, const void *pSeekBarId, int SampleId)
251{
252 CUIRect Button, SeekBar;
253 // play/pause button
254 {
255 View.VSplitLeft(Cut: View.h, pLeft: &Button, pRight: &View);
256 if(DoButton_FontIcon(pId: pPlayPauseButtonId, pText: Sound()->IsPlaying(SampleId) ? FontIcon::PAUSE : FontIcon::PLAY, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Play/pause audio preview.", Corners: IGraphics::CORNER_ALL) ||
257 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_SPACE)))
258 {
259 if(Sound()->IsPlaying(SampleId))
260 {
261 Sound()->Pause(SampleId);
262 }
263 else
264 {
265 if(SampleId != m_ToolbarPreviewSound && m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
266 Sound()->Pause(SampleId: m_ToolbarPreviewSound);
267
268 Sound()->Play(ChannelId: CSounds::CHN_GUI, SampleId, Flags: ISound::FLAG_PREVIEW, Volume: 1.0f);
269 }
270 }
271 }
272 // stop button
273 {
274 View.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &View);
275 View.VSplitLeft(Cut: View.h, pLeft: &Button, pRight: &View);
276 if(DoButton_FontIcon(pId: pStopButtonId, pText: FontIcon::STOP, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Stop audio preview.", Corners: IGraphics::CORNER_ALL))
277 {
278 Sound()->Stop(SampleId);
279 }
280 }
281 // do seekbar
282 {
283 View.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &View);
284 const float Cut = std::min(a: View.w, b: 200.0f);
285 View.VSplitLeft(Cut, pLeft: &SeekBar, pRight: &View);
286 SeekBar.HMargin(Cut: 2.5f, pOtherRect: &SeekBar);
287
288 const float Rounding = 5.0f;
289
290 char aBuffer[64];
291 const float CurrentTime = Sound()->GetSampleCurrentTime(SampleId);
292 const float TotalTime = Sound()->GetSampleTotalTime(SampleId);
293
294 // draw seek bar
295 SeekBar.Draw(Color: ColorRGBA(0, 0, 0, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
296
297 // draw filled bar
298 const float Amount = CurrentTime / TotalTime;
299 CUIRect FilledBar = SeekBar;
300 FilledBar.w = 2 * Rounding + (FilledBar.w - 2 * Rounding) * Amount;
301 FilledBar.Draw(Color: ColorRGBA(1, 1, 1, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
302
303 // draw time
304 char aCurrentTime[32];
305 str_time_float(secs: CurrentTime, format: ETimeFormat::HOURS, buffer: aCurrentTime, buffer_size: sizeof(aCurrentTime));
306 char aTotalTime[32];
307 str_time_float(secs: TotalTime, format: ETimeFormat::HOURS, buffer: aTotalTime, buffer_size: sizeof(aTotalTime));
308 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s / %s", aCurrentTime, aTotalTime);
309 Ui()->DoLabel(pRect: &SeekBar, pText: aBuffer, Size: SeekBar.h * 0.70f, Align: TEXTALIGN_MC);
310
311 // do the logic
312 const bool Inside = Ui()->MouseInside(pRect: &SeekBar);
313
314 if(Ui()->CheckActiveItem(pId: pSeekBarId))
315 {
316 if(!Ui()->MouseButton(Index: 0))
317 {
318 Ui()->SetActiveItem(nullptr);
319 }
320 else
321 {
322 const float AmountSeek = std::clamp(val: (Ui()->MouseX() - SeekBar.x - Rounding) / (SeekBar.w - 2 * Rounding), lo: 0.0f, hi: 1.0f);
323 Sound()->SetSampleCurrentTime(SampleId, Time: AmountSeek);
324 }
325 }
326 else if(Ui()->HotItem() == pSeekBarId)
327 {
328 if(Ui()->MouseButton(Index: 0))
329 Ui()->SetActiveItem(pSeekBarId);
330 }
331
332 if(Inside && !Ui()->MouseButton(Index: 0))
333 Ui()->SetHotItem(pSeekBarId);
334 }
335}
336
337void CEditor::DoToolbarLayers(CUIRect ToolBar)
338{
339 const bool ModPressed = Input()->ModifierIsPressed();
340 const bool ShiftPressed = Input()->ShiftIsPressed();
341
342 // handle shortcut for info button
343 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_I) && ModPressed && !ShiftPressed)
344 {
345 if(m_ShowTileInfo == SHOW_TILE_HEXADECIMAL)
346 m_ShowTileInfo = SHOW_TILE_DECIMAL;
347 else if(m_ShowTileInfo != SHOW_TILE_OFF)
348 m_ShowTileInfo = SHOW_TILE_OFF;
349 else
350 m_ShowTileInfo = SHOW_TILE_DECIMAL;
351 }
352
353 // handle shortcut for hex button
354 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_I) && ModPressed && ShiftPressed)
355 {
356 m_ShowTileInfo = m_ShowTileInfo == SHOW_TILE_HEXADECIMAL ? SHOW_TILE_OFF : SHOW_TILE_HEXADECIMAL;
357 }
358
359 // handle shortcut for unused button
360 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_U) && ModPressed && m_AllowPlaceUnusedTiles != EUnusedEntities::ALLOWED_IMPLICIT)
361 {
362 if(m_AllowPlaceUnusedTiles == EUnusedEntities::ALLOWED_EXPLICIT)
363 {
364 m_AllowPlaceUnusedTiles = EUnusedEntities::NOT_ALLOWED;
365 }
366 else
367 {
368 m_AllowPlaceUnusedTiles = EUnusedEntities::ALLOWED_EXPLICIT;
369 }
370 }
371
372 CUIRect ToolbarTop, ToolbarBottom;
373 CUIRect Button;
374
375 ToolBar.HSplitMid(pTop: &ToolbarTop, pBottom: &ToolbarBottom, Spacing: 5.0f);
376
377 // top line buttons
378 {
379 // detail button
380 ToolbarTop.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolbarTop);
381 static int s_HqButton = 0;
382 if(DoButton_Editor(pId: &s_HqButton, pText: "HD", Checked: m_ShowDetail, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+H] Toggle high detail.") ||
383 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_H) && ModPressed))
384 {
385 m_ShowDetail = !m_ShowDetail;
386 }
387
388 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
389
390 // animation buttons
391 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
392 static char s_JumpStartButton = 0;
393 if(DoButton_FontIcon(pId: &s_JumpStartButton, pText: FontIcon::BACKWARD_STEP, Checked: false, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Jump to beginning of animation.", Corners: IGraphics::CORNER_L))
394 {
395 Map()->m_EnvelopeEvaluator.m_AnimateTime = 0;
396 Map()->m_EnvelopeEvaluator.m_Animate = false;
397 }
398
399 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
400 static char s_AnimateButton = 0;
401 if(DoButton_FontIcon(pId: &s_AnimateButton, pText: Map()->m_EnvelopeEvaluator.m_Animate ? FontIcon::PAUSE : FontIcon::PLAY, Checked: Map()->m_EnvelopeEvaluator.m_Animate, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+M] Toggle animation.", Corners: IGraphics::CORNER_NONE) ||
402 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_M) && ModPressed))
403 {
404 Map()->m_EnvelopeEvaluator.m_AnimateStart = Client()->GlobalTime() - Map()->m_EnvelopeEvaluator.m_AnimateTime;
405 Map()->m_EnvelopeEvaluator.m_Animate = !Map()->m_EnvelopeEvaluator.m_Animate;
406 }
407
408 // animation settings button
409 ToolbarTop.VSplitLeft(Cut: 14.0f, pLeft: &Button, pRight: &ToolbarTop);
410 static char s_AnimateSettingsButton;
411 if(DoButton_FontIcon(pId: &s_AnimateSettingsButton, pText: FontIcon::CIRCLE_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Change the animation settings.", Corners: IGraphics::CORNER_R, FontSize: 8.0f))
412 {
413 Map()->m_EnvelopeEvaluator.m_AnimateUpdatePopup = true;
414 static SPopupMenuId s_PopupAnimateSettingsId;
415 Ui()->DoPopupMenu(pId: &s_PopupAnimateSettingsId, X: Button.x, Y: Button.y + Button.h, Width: 150.0f, Height: 37.0f, pContext: this, pfnFunc: PopupAnimateSettings);
416 }
417
418 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
419
420 // proof button
421 ToolbarTop.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolbarTop);
422 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))
423 {
424 m_QuickActionProof.Call();
425 }
426
427 ToolbarTop.VSplitLeft(Cut: 14.0f, pLeft: &Button, pRight: &ToolbarTop);
428 static int s_ProofModeButton = 0;
429 if(DoButton_FontIcon(pId: &s_ProofModeButton, pText: FontIcon::CIRCLE_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Select proof mode.", Corners: IGraphics::CORNER_R, FontSize: 8.0f))
430 {
431 static SPopupMenuId s_PopupProofModeId;
432 Ui()->DoPopupMenu(pId: &s_PopupProofModeId, X: Button.x, Y: Button.y + Button.h, Width: 60.0f, Height: 36.0f, pContext: this, pfnFunc: PopupProofMode);
433 }
434
435 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
436
437 // zoom button
438 ToolbarTop.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolbarTop);
439 static int s_ZoomButton = 0;
440 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."))
441 {
442 m_PreviewZoom = !m_PreviewZoom;
443 }
444
445 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
446
447 // grid button
448 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
449 static int s_GridButton = 0;
450 if(DoButton_FontIcon(pId: &s_GridButton, pText: FontIcon::BORDER_ALL, Checked: m_QuickActionToggleGrid.Active(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionToggleGrid.Description(), Corners: IGraphics::CORNER_L) ||
451 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_G) && ModPressed && !ShiftPressed))
452 {
453 m_QuickActionToggleGrid.Call();
454 }
455
456 // grid settings button
457 ToolbarTop.VSplitLeft(Cut: 14.0f, pLeft: &Button, pRight: &ToolbarTop);
458 static char s_GridSettingsButton;
459 if(DoButton_FontIcon(pId: &s_GridSettingsButton, pText: FontIcon::CIRCLE_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Change the grid settings.", Corners: IGraphics::CORNER_R, FontSize: 8.0f))
460 {
461 MapView()->MapGrid()->DoSettingsPopup(Position: vec2(Button.x, Button.y + Button.h));
462 }
463
464 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
465
466 // zoom group
467 ToolbarTop.VSplitLeft(Cut: 20.0f, pLeft: &Button, pRight: &ToolbarTop);
468 static int s_ZoomOutButton = 0;
469 if(DoButton_FontIcon(pId: &s_ZoomOutButton, pText: FontIcon::MINUS, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionZoomOut.Description(), Corners: IGraphics::CORNER_L))
470 {
471 m_QuickActionZoomOut.Call();
472 }
473
474 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
475 static int s_ZoomNormalButton = 0;
476 if(DoButton_FontIcon(pId: &s_ZoomNormalButton, pText: FontIcon::MAGNIFYING_GLASS, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionResetZoom.Description(), Corners: IGraphics::CORNER_NONE))
477 {
478 m_QuickActionResetZoom.Call();
479 }
480
481 ToolbarTop.VSplitLeft(Cut: 20.0f, pLeft: &Button, pRight: &ToolbarTop);
482 static int s_ZoomInButton = 0;
483 if(DoButton_FontIcon(pId: &s_ZoomInButton, pText: FontIcon::PLUS, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionZoomIn.Description(), Corners: IGraphics::CORNER_R))
484 {
485 m_QuickActionZoomIn.Call();
486 }
487
488 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
489
490 // undo/redo group
491 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
492 static int s_UndoButton = 0;
493 if(DoButton_FontIcon(pId: &s_UndoButton, pText: FontIcon::UNDO, Checked: Map()->m_EditorHistory.CanUndo() - 1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+Z] Undo the last action.", Corners: IGraphics::CORNER_L))
494 {
495 Map()->m_EditorHistory.Undo();
496 }
497
498 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
499 static int s_RedoButton = 0;
500 if(DoButton_FontIcon(pId: &s_RedoButton, pText: FontIcon::REDO, Checked: Map()->m_EditorHistory.CanRedo() - 1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+Y] Redo the last action.", Corners: IGraphics::CORNER_R))
501 {
502 Map()->m_EditorHistory.Redo();
503 }
504
505 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
506
507 // brush manipulation
508 {
509 int Enabled = m_pBrush->IsEmpty() ? -1 : 0;
510
511 // flip buttons
512 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
513 static int s_FlipXButton = 0;
514 if(DoButton_FontIcon(pId: &s_FlipXButton, pText: FontIcon::ARROWS_LEFT_RIGHT, Checked: Enabled, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[N] Flip the brush horizontally.", Corners: IGraphics::CORNER_L) || (Input()->KeyPress(Key: KEY_N) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Ui()->IsPopupOpen()))
515 {
516 for(auto &pLayer : m_pBrush->m_vpLayers)
517 pLayer->BrushFlipX();
518 }
519
520 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
521 static int s_FlipyButton = 0;
522 if(DoButton_FontIcon(pId: &s_FlipyButton, pText: FontIcon::ARROWS_UP_DOWN, Checked: Enabled, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[M] Flip the brush vertically.", Corners: IGraphics::CORNER_R) || (Input()->KeyPress(Key: KEY_M) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Ui()->IsPopupOpen()))
523 {
524 for(auto &pLayer : m_pBrush->m_vpLayers)
525 pLayer->BrushFlipY();
526 }
527 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
528
529 // rotate buttons
530 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
531 static int s_RotationAmount = 90;
532 bool TileLayer = false;
533 // check for tile layers in brush selection
534 for(auto &pLayer : m_pBrush->m_vpLayers)
535 if(pLayer->m_Type == LAYERTYPE_TILES)
536 {
537 TileLayer = true;
538 s_RotationAmount = std::max(a: 90, b: (s_RotationAmount / 90) * 90);
539 break;
540 }
541
542 static int s_CcwButton = 0;
543 if(DoButton_FontIcon(pId: &s_CcwButton, pText: FontIcon::ARROW_ROTATE_LEFT, Checked: Enabled, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[R] Rotate the brush counter-clockwise.", Corners: IGraphics::CORNER_L) || (Input()->KeyPress(Key: KEY_R) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Ui()->IsPopupOpen()))
544 {
545 for(auto &pLayer : m_pBrush->m_vpLayers)
546 pLayer->BrushRotate(Amount: -s_RotationAmount / 360.0f * pi * 2);
547 }
548
549 ToolbarTop.VSplitLeft(Cut: 30.0f, pLeft: &Button, pRight: &ToolbarTop);
550 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);
551 s_RotationAmount = RotationAmountRes.m_Value;
552
553 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
554 static int s_CwButton = 0;
555 if(DoButton_FontIcon(pId: &s_CwButton, pText: FontIcon::ARROW_ROTATE_RIGHT, Checked: Enabled, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[T] Rotate the brush clockwise.", Corners: IGraphics::CORNER_R) || (Input()->KeyPress(Key: KEY_T) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Ui()->IsPopupOpen()))
556 {
557 for(auto &pLayer : m_pBrush->m_vpLayers)
558 pLayer->BrushRotate(Amount: s_RotationAmount / 360.0f * pi * 2);
559 }
560 }
561
562 // Color pipette and palette
563 {
564 const float PipetteButtonWidth = 30.0f;
565 const float ColorPickerButtonWidth = 20.0f;
566 const float Spacing = 2.0f;
567 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));
568
569 CUIRect ColorPalette;
570 ToolbarTop.VSplitRight(Cut: NumColorsShown * (ColorPickerButtonWidth + Spacing) + PipetteButtonWidth, pLeft: &ToolbarTop, pRight: &ColorPalette);
571
572 // Pipette button
573 static char s_PipetteButton;
574 ColorPalette.VSplitLeft(Cut: PipetteButtonWidth, pLeft: &Button, pRight: &ColorPalette);
575 ColorPalette.VSplitLeft(Cut: Spacing, pLeft: nullptr, pRight: &ColorPalette);
576 if(DoButton_FontIcon(pId: &s_PipetteButton, pText: FontIcon::EYE_DROPPER, Checked: m_QuickActionPipette.Active(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionPipette.Description(), Corners: IGraphics::CORNER_ALL) ||
577 (CLineInput::GetActiveInput() == nullptr && ModPressed && ShiftPressed && Input()->KeyPress(Key: KEY_C)))
578 {
579 m_QuickActionPipette.Call();
580 }
581
582 // Palette color pickers
583 for(size_t i = 0; i < NumColorsShown; ++i)
584 {
585 ColorPalette.VSplitLeft(Cut: ColorPickerButtonWidth, pLeft: &Button, pRight: &ColorPalette);
586 ColorPalette.VSplitLeft(Cut: Spacing, pLeft: nullptr, pRight: &ColorPalette);
587 const auto &&SetColor = [&](ColorRGBA NewColor) {
588 m_aSavedColors[i] = NewColor;
589 };
590 DoColorPickerButton(pId: &m_aSavedColors[i], pRect: &Button, Color: m_aSavedColors[i], SetColor);
591 }
592 }
593 }
594
595 // Bottom line buttons
596 {
597 // refocus button
598 {
599 ToolbarBottom.VSplitLeft(Cut: 50.0f, pLeft: &Button, pRight: &ToolbarBottom);
600 int FocusButtonChecked = MapView()->IsFocused() ? -1 : 1;
601 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)))
602 m_QuickActionRefocus.Call();
603 ToolbarBottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarBottom);
604 }
605
606 // brush picker button
607 {
608 ToolbarBottom.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarBottom);
609 const int Checked = m_QuickActionBrushPicker.Disabled() ? -1 : (m_ShowPicker ? 1 : 0);
610 if(DoButton_FontIcon(pId: &m_QuickActionBrushPicker, pText: FontIcon::BRUSH, Checked, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionBrushPicker.Description(), Corners: IGraphics::CORNER_ALL))
611 {
612 m_QuickActionBrushPicker.Call();
613 }
614 ToolbarBottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarBottom);
615 }
616
617 // tile manipulation
618 {
619 // do tele/tune/switch/speedup button
620 {
621 std::shared_ptr<CLayerTiles> pS = std::static_pointer_cast<CLayerTiles>(r: Map()->SelectedLayerType(Index: 0, Type: LAYERTYPE_TILES));
622 if(pS)
623 {
624 const char *pButtonName = nullptr;
625 CUi::FPopupMenuFunction pfnPopupFunc = nullptr;
626 int Rows = 0;
627 int ExtraWidth = 0;
628 if(pS == Map()->m_pSwitchLayer)
629 {
630 pButtonName = "Switch";
631 pfnPopupFunc = PopupSwitch;
632 Rows = 3;
633 }
634 else if(pS == Map()->m_pSpeedupLayer)
635 {
636 pButtonName = "Speedup";
637 pfnPopupFunc = PopupSpeedup;
638 Rows = 3;
639 }
640 else if(pS == Map()->m_pTuneLayer)
641 {
642 pButtonName = "Tune";
643 pfnPopupFunc = PopupTune;
644 Rows = 2;
645 }
646 else if(pS == Map()->m_pTeleLayer)
647 {
648 pButtonName = "Tele";
649 pfnPopupFunc = PopupTele;
650 Rows = 3;
651 ExtraWidth = 50;
652 }
653
654 if(pButtonName != nullptr)
655 {
656 static char s_aButtonTooltip[64];
657 str_format(buffer: s_aButtonTooltip, buffer_size: sizeof(s_aButtonTooltip), format: "[Ctrl+T] %s", pButtonName);
658
659 ToolbarBottom.VSplitLeft(Cut: 60.0f, pLeft: &Button, pRight: &ToolbarBottom);
660 static int s_ModifierButton = 0;
661 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)))
662 {
663 static SPopupMenuId s_PopupModifierId;
664 if(!Ui()->IsPopupOpen(pId: &s_PopupModifierId))
665 {
666 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);
667 }
668 }
669 ToolbarBottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarBottom);
670 }
671 }
672 }
673 }
674
675 // do add quad/sound button
676 std::shared_ptr<CLayer> pLayer = Map()->SelectedLayer(Index: 0);
677 if(pLayer && (pLayer->m_Type == LAYERTYPE_QUADS || pLayer->m_Type == LAYERTYPE_SOUNDS))
678 {
679 // "Add sound source" button needs more space or the font size will be scaled down
680 ToolbarBottom.VSplitLeft(Cut: (pLayer->m_Type == LAYERTYPE_QUADS) ? 60.0f : 100.0f, pLeft: &Button, pRight: &ToolbarBottom);
681
682 if(pLayer->m_Type == LAYERTYPE_QUADS)
683 {
684 if(DoButton_Editor(pId: &m_QuickActionAddQuad, pText: m_QuickActionAddQuad.Label(), Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionAddQuad.Description()) ||
685 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_Q) && ModPressed))
686 {
687 m_QuickActionAddQuad.Call();
688 }
689 }
690 else if(pLayer->m_Type == LAYERTYPE_SOUNDS)
691 {
692 if(DoButton_Editor(pId: &m_QuickActionAddSoundSource, pText: m_QuickActionAddSoundSource.Label(), Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionAddSoundSource.Description()) ||
693 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_Q) && ModPressed))
694 {
695 m_QuickActionAddSoundSource.Call();
696 }
697 }
698
699 ToolbarBottom.VSplitLeft(Cut: 5.0f, pLeft: &Button, pRight: &ToolbarBottom);
700 }
701
702 // Brush draw mode button
703 {
704 ToolbarBottom.VSplitLeft(Cut: 65.0f, pLeft: &Button, pRight: &ToolbarBottom);
705 static int s_BrushDrawModeButton = 0;
706 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.") ||
707 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_D) && ModPressed && !ShiftPressed))
708 m_BrushDrawDestructive = !m_BrushDrawDestructive;
709 ToolbarBottom.VSplitLeft(Cut: 5.0f, pLeft: &Button, pRight: &ToolbarBottom);
710 }
711 }
712}
713
714void CEditor::DoToolbarImages(CUIRect ToolBar)
715{
716 CUIRect ToolBarTop, ToolBarBottom;
717 ToolBar.HSplitMid(pTop: &ToolBarTop, pBottom: &ToolBarBottom, Spacing: 5.0f);
718
719 std::shared_ptr<CEditorImage> pSelectedImage = Map()->SelectedImage();
720 if(pSelectedImage != nullptr)
721 {
722 char aLabel[64];
723 str_format(buffer: aLabel, buffer_size: sizeof(aLabel), format: "Size: %" PRIzu " × %" PRIzu, pSelectedImage->m_Width, pSelectedImage->m_Height);
724 Ui()->DoLabel(pRect: &ToolBarBottom, pText: aLabel, Size: 12.0f, Align: TEXTALIGN_ML);
725 }
726}
727
728void CEditor::DoToolbarSounds(CUIRect ToolBar)
729{
730 CUIRect ToolBarTop, ToolBarBottom;
731 ToolBar.HSplitMid(pTop: &ToolBarTop, pBottom: &ToolBarBottom, Spacing: 5.0f);
732
733 std::shared_ptr<CEditorSound> pSelectedSound = Map()->SelectedSound();
734 if(pSelectedSound != nullptr)
735 {
736 if(pSelectedSound->m_SoundId != m_ToolbarPreviewSound && m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
737 Sound()->Stop(SampleId: m_ToolbarPreviewSound);
738 m_ToolbarPreviewSound = pSelectedSound->m_SoundId;
739 }
740 else
741 {
742 m_ToolbarPreviewSound = -1;
743 }
744
745 if(m_ToolbarPreviewSound >= 0)
746 {
747 static int s_PlayPauseButton, s_StopButton, s_SeekBar = 0;
748 DoAudioPreview(View: ToolBarBottom, pPlayPauseButtonId: &s_PlayPauseButton, pStopButtonId: &s_StopButton, pSeekBarId: &s_SeekBar, SampleId: m_ToolbarPreviewSound);
749 }
750}
751
752static void Rotate(const CPoint *pCenter, CPoint *pPoint, float Rotation)
753{
754 int x = pPoint->x - pCenter->x;
755 int y = pPoint->y - pCenter->y;
756 pPoint->x = (int)(x * std::cos(x: Rotation) - y * std::sin(x: Rotation) + pCenter->x);
757 pPoint->y = (int)(x * std::sin(x: Rotation) + y * std::cos(x: Rotation) + pCenter->y);
758}
759
760void CEditor::DoSoundSource(int LayerIndex, CSoundSource *pSource, int Index)
761{
762 static ESoundSourceOp s_Operation = ESoundSourceOp::NONE;
763
764 float CenterX = fx2f(v: pSource->m_Position.x);
765 float CenterY = fx2f(v: pSource->m_Position.y);
766
767 const bool IgnoreGrid = Input()->AltIsPressed();
768
769 if(s_Operation == ESoundSourceOp::NONE)
770 {
771 if(!Ui()->MouseButton(Index: 0))
772 Map()->m_SoundSourceOperationTracker.End();
773 }
774
775 if(Ui()->CheckActiveItem(pId: pSource))
776 {
777 if(s_Operation != ESoundSourceOp::NONE)
778 {
779 Map()->m_SoundSourceOperationTracker.Begin(pSource, Operation: s_Operation, LayerIndex);
780 }
781
782 if(MapView()->MouseDeltaWorld() != vec2(0.0f, 0.0f))
783 {
784 if(s_Operation == ESoundSourceOp::MOVE)
785 {
786 vec2 Pos = MapView()->MouseWorldPos();
787 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
788 {
789 MapView()->MapGrid()->SnapToGrid(Position&: Pos);
790 }
791 pSource->m_Position.x = f2fx(v: Pos.x);
792 pSource->m_Position.y = f2fx(v: Pos.y);
793 }
794 }
795
796 if(s_Operation == ESoundSourceOp::CONTEXT_MENU)
797 {
798 if(!Ui()->MouseButton(Index: 1))
799 {
800 if(Map()->m_vSelectedLayers.size() == 1)
801 {
802 static SPopupMenuId s_PopupSourceId;
803 Ui()->DoPopupMenu(pId: &s_PopupSourceId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 200, pContext: this, pfnFunc: PopupSource);
804 Ui()->DisableMouseLock();
805 }
806 s_Operation = ESoundSourceOp::NONE;
807 Ui()->SetActiveItem(nullptr);
808 }
809 }
810 else
811 {
812 if(!Ui()->MouseButton(Index: 0))
813 {
814 Ui()->DisableMouseLock();
815 s_Operation = ESoundSourceOp::NONE;
816 Ui()->SetActiveItem(nullptr);
817 }
818 }
819
820 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
821 }
822 else if(Ui()->HotItem() == pSource)
823 {
824 m_pUiGotContext = pSource;
825
826 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
827 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold alt to ignore grid.");
828
829 if(Ui()->MouseButton(Index: 0))
830 {
831 s_Operation = ESoundSourceOp::MOVE;
832
833 Ui()->SetActiveItem(pSource);
834 Map()->m_SelectedSoundSource = Index;
835 }
836
837 if(Ui()->MouseButton(Index: 1))
838 {
839 Map()->m_SelectedSoundSource = Index;
840 s_Operation = ESoundSourceOp::CONTEXT_MENU;
841 Ui()->SetActiveItem(pSource);
842 }
843 }
844 else
845 {
846 Graphics()->SetColor(r: 0, g: 1, b: 0, a: 1);
847 }
848
849 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * MapView()->MouseWorldScale(), 5.0f * MapView()->MouseWorldScale());
850 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
851}
852
853void CEditor::UpdateHotSoundSource(const CLayerSounds *pLayer)
854{
855 const vec2 MouseWorld = MapView()->MouseWorldPos();
856
857 float MinDist = 500.0f;
858 const void *pMinSourceId = nullptr;
859
860 const auto UpdateMinimum = [&](vec2 Position, const void *pId) {
861 const float CurrDist = length_squared(a: (Position - MouseWorld) / MapView()->MouseWorldScale());
862 if(CurrDist < MinDist)
863 {
864 MinDist = CurrDist;
865 pMinSourceId = pId;
866 }
867 };
868
869 for(const CSoundSource &Source : pLayer->m_vSources)
870 {
871 UpdateMinimum(vec2(fx2f(v: Source.m_Position.x), fx2f(v: Source.m_Position.y)), &Source);
872 }
873
874 if(pMinSourceId != nullptr)
875 {
876 Ui()->SetHotItem(pMinSourceId);
877 }
878}
879
880void CEditor::PreparePointDrag(const CQuad *pQuad, int QuadIndex, int PointIndex)
881{
882 m_QuadDragOriginalPoints[QuadIndex][PointIndex] = pQuad->m_aPoints[PointIndex];
883}
884
885void CEditor::DoPointDrag(CQuad *pQuad, int QuadIndex, int PointIndex, ivec2 Offset)
886{
887 pQuad->m_aPoints[PointIndex] = m_QuadDragOriginalPoints[QuadIndex][PointIndex] + Offset;
888}
889
890CEditor::EAxis CEditor::GetDragAxis(ivec2 Offset) const
891{
892 if(Input()->ShiftIsPressed())
893 if(absolute(a: Offset.x) < absolute(a: Offset.y))
894 return EAxis::Y;
895 else
896 return EAxis::X;
897 else
898 return EAxis::NONE;
899}
900
901void CEditor::DrawAxis(EAxis Axis, CPoint &OriginalPoint, CPoint &Point) const
902{
903 if(Axis == EAxis::NONE)
904 return;
905
906 Graphics()->SetColor(r: 1, g: 0, b: 0.1f, a: 1);
907 if(Axis == EAxis::X)
908 {
909 IGraphics::CQuadItem Line(fx2f(v: OriginalPoint.x + Point.x) / 2.0f, fx2f(v: OriginalPoint.y), fx2f(v: Point.x - OriginalPoint.x), 1.0f * MapView()->MouseWorldScale());
910 Graphics()->QuadsDraw(pArray: &Line, Num: 1);
911 }
912 else if(Axis == EAxis::Y)
913 {
914 IGraphics::CQuadItem Line(fx2f(v: OriginalPoint.x), fx2f(v: OriginalPoint.y + Point.y) / 2.0f, 1.0f * MapView()->MouseWorldScale(), fx2f(v: Point.y - OriginalPoint.y));
915 Graphics()->QuadsDraw(pArray: &Line, Num: 1);
916 }
917
918 // Draw ghost of original point
919 IGraphics::CQuadItem QuadItem(fx2f(v: OriginalPoint.x), fx2f(v: OriginalPoint.y), 5.0f * MapView()->MouseWorldScale(), 5.0f * MapView()->MouseWorldScale());
920 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
921}
922
923void CEditor::ComputePointAlignments(const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, ivec2 Offset, std::vector<SAlignmentInfo> &vAlignments, bool Append) const
924{
925 if(!Append)
926 vAlignments.clear();
927 if(!g_Config.m_EdAlignQuads)
928 return;
929
930 bool GridEnabled = MapView()->MapGrid()->IsEnabled() && !Input()->AltIsPressed();
931
932 // Perform computation from the original position of this point
933 int Threshold = f2fx(v: std::max(a: 5.0f, b: 10.0f * MapView()->MouseWorldScale()));
934 CPoint OrigPoint = m_QuadDragOriginalPoints.at(k: QuadIndex)[PointIndex];
935 // Get the "current" point by applying the offset
936 CPoint Point = OrigPoint + Offset;
937
938 // Save smallest diff on both axis to only keep closest alignments
939 ivec2 SmallestDiff = ivec2(Threshold + 1, Threshold + 1);
940 // Store both axis alignments in separate vectors
941 std::vector<SAlignmentInfo> vAlignmentsX, vAlignmentsY;
942
943 // Check if we can align/snap to a specific point
944 auto &&CheckAlignment = [&](CPoint *pQuadPoint) {
945 ivec2 DirectedDiff = *pQuadPoint - Point;
946 ivec2 Diff = ivec2(absolute(a: DirectedDiff.x), absolute(a: DirectedDiff.y));
947
948 if(Diff.x <= Threshold && (!GridEnabled || Diff.x == 0))
949 {
950 // Only store alignments that have the smallest difference
951 if(Diff.x < SmallestDiff.x)
952 {
953 vAlignmentsX.clear();
954 SmallestDiff.x = Diff.x;
955 }
956
957 // We can have multiple alignments having the same difference/distance
958 if(Diff.x == SmallestDiff.x)
959 {
960 vAlignmentsX.push_back(x: SAlignmentInfo{
961 .m_AlignedPoint: *pQuadPoint, // Aligned point
962 {.m_X: OrigPoint.y}, // Value that can change (which is not snapped), original position
963 .m_Axis: EAxis::Y, // The alignment axis
964 .m_PointIndex: PointIndex, // The index of the point
965 .m_Diff: DirectedDiff.x,
966 });
967 }
968 }
969
970 if(Diff.y <= Threshold && (!GridEnabled || Diff.y == 0))
971 {
972 // Only store alignments that have the smallest difference
973 if(Diff.y < SmallestDiff.y)
974 {
975 vAlignmentsY.clear();
976 SmallestDiff.y = Diff.y;
977 }
978
979 if(Diff.y == SmallestDiff.y)
980 {
981 vAlignmentsY.push_back(x: SAlignmentInfo{
982 .m_AlignedPoint: *pQuadPoint,
983 {.m_X: OrigPoint.x},
984 .m_Axis: EAxis::X,
985 .m_PointIndex: PointIndex,
986 .m_Diff: DirectedDiff.y,
987 });
988 }
989 }
990 };
991
992 // Iterate through all the quads of the current layer
993 // Check alignment with each point of the quad (corners & pivot)
994 // Compute an AABB (Axis Aligned Bounding Box) to get the center of the quad
995 // Check alignment with the center of the quad
996 for(size_t i = 0; i < pLayer->m_vQuads.size(); i++)
997 {
998 auto *pCurrentQuad = &pLayer->m_vQuads[i];
999 CPoint Min = pCurrentQuad->m_aPoints[0];
1000 CPoint Max = pCurrentQuad->m_aPoints[0];
1001
1002 for(int v = 0; v < 5; v++)
1003 {
1004 CPoint *pQuadPoint = &pCurrentQuad->m_aPoints[v];
1005
1006 if(v != 4)
1007 { // Don't use pivot to compute AABB
1008 if(pQuadPoint->x < Min.x)
1009 Min.x = pQuadPoint->x;
1010 if(pQuadPoint->y < Min.y)
1011 Min.y = pQuadPoint->y;
1012 if(pQuadPoint->x > Max.x)
1013 Max.x = pQuadPoint->x;
1014 if(pQuadPoint->y > Max.y)
1015 Max.y = pQuadPoint->y;
1016 }
1017
1018 // Don't check alignment with current point
1019 if(pQuadPoint == &pQuad->m_aPoints[PointIndex])
1020 continue;
1021
1022 // Don't check alignment with other selected points
1023 bool IsCurrentPointSelected = Map()->IsQuadSelected(Index: i) && (Map()->IsQuadCornerSelected(Index: v) || (v == PointIndex && PointIndex == 4));
1024 if(IsCurrentPointSelected)
1025 continue;
1026
1027 CheckAlignment(pQuadPoint);
1028 }
1029
1030 // Don't check alignment with center of selected quads
1031 if(!Map()->IsQuadSelected(Index: i))
1032 {
1033 CPoint Center = (Min + Max) / 2.0f;
1034 CheckAlignment(&Center);
1035 }
1036 }
1037
1038 // Finally concatenate both alignment vectors into the output
1039 vAlignments.reserve(n: vAlignmentsX.size() + vAlignmentsY.size());
1040 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsX.begin(), last: vAlignmentsX.end());
1041 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsY.begin(), last: vAlignmentsY.end());
1042}
1043
1044void CEditor::ComputePointsAlignments(const std::shared_ptr<CLayerQuads> &pLayer, bool Pivot, ivec2 Offset, std::vector<SAlignmentInfo> &vAlignments) const
1045{
1046 // This method is used to compute alignments from selected points
1047 // and only apply the closest alignment on X and Y to the offset.
1048
1049 vAlignments.clear();
1050 std::vector<SAlignmentInfo> vAllAlignments;
1051
1052 for(int Selected : Map()->m_vSelectedQuads)
1053 {
1054 CQuad *pQuad = &pLayer->m_vQuads[Selected];
1055
1056 if(!Pivot)
1057 {
1058 for(int m = 0; m < 4; m++)
1059 {
1060 if(Map()->IsQuadPointSelected(QuadIndex: Selected, Index: m))
1061 {
1062 ComputePointAlignments(pLayer, pQuad, QuadIndex: Selected, PointIndex: m, Offset, vAlignments&: vAllAlignments, Append: true);
1063 }
1064 }
1065 }
1066 else
1067 {
1068 ComputePointAlignments(pLayer, pQuad, QuadIndex: Selected, PointIndex: 4, Offset, vAlignments&: vAllAlignments, Append: true);
1069 }
1070 }
1071
1072 ivec2 SmallestDiff = ivec2(std::numeric_limits<int>::max(), std::numeric_limits<int>::max());
1073 std::vector<SAlignmentInfo> vAlignmentsX, vAlignmentsY;
1074
1075 for(const auto &Alignment : vAllAlignments)
1076 {
1077 int AbsDiff = absolute(a: Alignment.m_Diff);
1078 if(Alignment.m_Axis == EAxis::X)
1079 {
1080 if(AbsDiff < SmallestDiff.y)
1081 {
1082 SmallestDiff.y = AbsDiff;
1083 vAlignmentsY.clear();
1084 }
1085 if(AbsDiff == SmallestDiff.y)
1086 vAlignmentsY.emplace_back(args: Alignment);
1087 }
1088 else if(Alignment.m_Axis == EAxis::Y)
1089 {
1090 if(AbsDiff < SmallestDiff.x)
1091 {
1092 SmallestDiff.x = AbsDiff;
1093 vAlignmentsX.clear();
1094 }
1095 if(AbsDiff == SmallestDiff.x)
1096 vAlignmentsX.emplace_back(args: Alignment);
1097 }
1098 }
1099
1100 vAlignments.reserve(n: vAlignmentsX.size() + vAlignmentsY.size());
1101 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsX.begin(), last: vAlignmentsX.end());
1102 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsY.begin(), last: vAlignmentsY.end());
1103}
1104
1105void CEditor::ComputeAABBAlignments(const std::shared_ptr<CLayerQuads> &pLayer, const SAxisAlignedBoundingBox &AABB, ivec2 Offset, std::vector<SAlignmentInfo> &vAlignments) const
1106{
1107 vAlignments.clear();
1108 if(!g_Config.m_EdAlignQuads)
1109 return;
1110
1111 // This method is a bit different than the point alignment in the way where instead of trying to align 1 point to all quads,
1112 // we try to align 5 points to all quads, these 5 points being 5 points of an AABB.
1113 // Otherwise, the concept is the same, we use the original position of the AABB to make the computations.
1114 int Threshold = f2fx(v: std::max(a: 5.0f, b: 10.0f * MapView()->MouseWorldScale()));
1115 ivec2 SmallestDiff = ivec2(Threshold + 1, Threshold + 1);
1116 std::vector<SAlignmentInfo> vAlignmentsX, vAlignmentsY;
1117
1118 bool GridEnabled = MapView()->MapGrid()->IsEnabled() && !Input()->AltIsPressed();
1119
1120 auto &&CheckAlignment = [&](CPoint &Aligned, int Point) {
1121 CPoint ToCheck = AABB.m_aPoints[Point] + Offset;
1122 ivec2 DirectedDiff = Aligned - ToCheck;
1123 ivec2 Diff = ivec2(absolute(a: DirectedDiff.x), absolute(a: DirectedDiff.y));
1124
1125 if(Diff.x <= Threshold && (!GridEnabled || Diff.x == 0))
1126 {
1127 if(Diff.x < SmallestDiff.x)
1128 {
1129 SmallestDiff.x = Diff.x;
1130 vAlignmentsX.clear();
1131 }
1132
1133 if(Diff.x == SmallestDiff.x)
1134 {
1135 vAlignmentsX.push_back(x: SAlignmentInfo{
1136 .m_AlignedPoint: Aligned,
1137 {.m_X: AABB.m_aPoints[Point].y},
1138 .m_Axis: EAxis::Y,
1139 .m_PointIndex: Point,
1140 .m_Diff: DirectedDiff.x,
1141 });
1142 }
1143 }
1144
1145 if(Diff.y <= Threshold && (!GridEnabled || Diff.y == 0))
1146 {
1147 if(Diff.y < SmallestDiff.y)
1148 {
1149 SmallestDiff.y = Diff.y;
1150 vAlignmentsY.clear();
1151 }
1152
1153 if(Diff.y == SmallestDiff.y)
1154 {
1155 vAlignmentsY.push_back(x: SAlignmentInfo{
1156 .m_AlignedPoint: Aligned,
1157 {.m_X: AABB.m_aPoints[Point].x},
1158 .m_Axis: EAxis::X,
1159 .m_PointIndex: Point,
1160 .m_Diff: DirectedDiff.y,
1161 });
1162 }
1163 }
1164 };
1165
1166 auto &&CheckAABBAlignment = [&](CPoint &QuadMin, CPoint &QuadMax) {
1167 CPoint QuadCenter = (QuadMin + QuadMax) / 2.0f;
1168 CPoint aQuadPoints[5] = {
1169 QuadMin, // Top left
1170 {QuadMax.x, QuadMin.y}, // Top right
1171 {QuadMin.x, QuadMax.y}, // Bottom left
1172 QuadMax, // Bottom right
1173 QuadCenter,
1174 };
1175
1176 // Check all points with all the other points
1177 for(auto &QuadPoint : aQuadPoints)
1178 {
1179 // i is the quad point which is "aligned" and that we want to compare with
1180 for(int j = 0; j < 5; j++)
1181 {
1182 // j is the point we try to align
1183 CheckAlignment(QuadPoint, j);
1184 }
1185 }
1186 };
1187
1188 // Iterate through all quads of the current layer
1189 // Compute AABB of all quads and check if the dragged AABB can be aligned to this AABB.
1190 for(size_t i = 0; i < pLayer->m_vQuads.size(); i++)
1191 {
1192 auto *pCurrentQuad = &pLayer->m_vQuads[i];
1193 if(Map()->IsQuadSelected(Index: i)) // Don't check with other selected quads
1194 continue;
1195
1196 // Get AABB of this quad
1197 CPoint QuadMin = pCurrentQuad->m_aPoints[0], QuadMax = pCurrentQuad->m_aPoints[0];
1198 for(int v = 1; v < 4; v++)
1199 {
1200 QuadMin.x = std::min(a: QuadMin.x, b: pCurrentQuad->m_aPoints[v].x);
1201 QuadMin.y = std::min(a: QuadMin.y, b: pCurrentQuad->m_aPoints[v].y);
1202 QuadMax.x = std::max(a: QuadMax.x, b: pCurrentQuad->m_aPoints[v].x);
1203 QuadMax.y = std::max(a: QuadMax.y, b: pCurrentQuad->m_aPoints[v].y);
1204 }
1205
1206 CheckAABBAlignment(QuadMin, QuadMax);
1207 }
1208
1209 // Finally, concatenate both alignment vectors into the output
1210 vAlignments.reserve(n: vAlignmentsX.size() + vAlignmentsY.size());
1211 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsX.begin(), last: vAlignmentsX.end());
1212 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsY.begin(), last: vAlignmentsY.end());
1213}
1214
1215void CEditor::DrawPointAlignments(const std::vector<SAlignmentInfo> &vAlignments, ivec2 Offset) const
1216{
1217 if(!g_Config.m_EdAlignQuads)
1218 return;
1219
1220 // Drawing an alignment is easy, we convert fixed to float for the aligned point coords
1221 // and we also convert the "changing" value after applying the offset (which might be edited to actually align the value with the alignment).
1222 Graphics()->SetColor(r: 1, g: 0, b: 0.1f, a: 1);
1223 for(const SAlignmentInfo &Alignment : vAlignments)
1224 {
1225 // We don't use IGraphics::CLineItem to draw because we don't want to stop QuadsBegin(), quads work just fine.
1226 if(Alignment.m_Axis == EAxis::X)
1227 { // Alignment on X axis is same Y values but different X values
1228 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 * MapView()->MouseWorldScale());
1229 Graphics()->QuadsDrawTL(pArray: &Line, Num: 1);
1230 }
1231 else if(Alignment.m_Axis == EAxis::Y)
1232 { // Alignment on Y axis is same X values but different Y values
1233 IGraphics::CQuadItem Line(fx2f(v: Alignment.m_AlignedPoint.x), fx2f(v: Alignment.m_AlignedPoint.y), 1.0f * MapView()->MouseWorldScale(), fx2f(v: Alignment.m_Y + Offset.y - Alignment.m_AlignedPoint.y));
1234 Graphics()->QuadsDrawTL(pArray: &Line, Num: 1);
1235 }
1236 }
1237}
1238
1239void CEditor::DrawAABB(const SAxisAlignedBoundingBox &AABB, ivec2 Offset) const
1240{
1241 // Drawing an AABB is simply converting the points from fixed to float
1242 // Then making lines out of quads and drawing them
1243 vec2 TL = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TL].x + Offset.x), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TL].y + Offset.y)};
1244 vec2 TR = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TR].x + Offset.x), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TR].y + Offset.y)};
1245 vec2 BL = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BL].x + Offset.x), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BL].y + Offset.y)};
1246 vec2 BR = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BR].x + Offset.x), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BR].y + Offset.y)};
1247 vec2 Center = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_CENTER].x + Offset.x), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_CENTER].y + Offset.y)};
1248
1249 // We don't use IGraphics::CLineItem to draw because we don't want to stop QuadsBegin(), quads work just fine.
1250 IGraphics::CQuadItem Lines[4] = {
1251 {TL.x, TL.y, TR.x - TL.x, 1.0f * MapView()->MouseWorldScale()},
1252 {TL.x, TL.y, 1.0f * MapView()->MouseWorldScale(), BL.y - TL.y},
1253 {TR.x, TR.y, 1.0f * MapView()->MouseWorldScale(), BR.y - TR.y},
1254 {BL.x, BL.y, BR.x - BL.x, 1.0f * MapView()->MouseWorldScale()},
1255 };
1256 Graphics()->SetColor(r: 1, g: 0, b: 1, a: 1);
1257 Graphics()->QuadsDrawTL(pArray: Lines, Num: 4);
1258
1259 IGraphics::CQuadItem CenterQuad(Center.x, Center.y, 5.0f * MapView()->MouseWorldScale(), 5.0f * MapView()->MouseWorldScale());
1260 Graphics()->QuadsDraw(pArray: &CenterQuad, Num: 1);
1261}
1262
1263void CEditor::QuadSelectionAABB(const std::shared_ptr<CLayerQuads> &pLayer, SAxisAlignedBoundingBox &OutAABB)
1264{
1265 // Compute an englobing AABB of the current selection of quads
1266 CPoint Min{
1267 std::numeric_limits<int>::max(),
1268 std::numeric_limits<int>::max(),
1269 };
1270 CPoint Max{
1271 std::numeric_limits<int>::min(),
1272 std::numeric_limits<int>::min(),
1273 };
1274 for(int Selected : Map()->m_vSelectedQuads)
1275 {
1276 CQuad *pQuad = &pLayer->m_vQuads[Selected];
1277 for(int i = 0; i < 4; i++)
1278 {
1279 auto *pPoint = &pQuad->m_aPoints[i];
1280 Min.x = std::min(a: Min.x, b: pPoint->x);
1281 Min.y = std::min(a: Min.y, b: pPoint->y);
1282 Max.x = std::max(a: Max.x, b: pPoint->x);
1283 Max.y = std::max(a: Max.y, b: pPoint->y);
1284 }
1285 }
1286 CPoint Center = (Min + Max) / 2.0f;
1287 CPoint aPoints[SAxisAlignedBoundingBox::NUM_POINTS] = {
1288 Min, // Top left
1289 {Max.x, Min.y}, // Top right
1290 {Min.x, Max.y}, // Bottom left
1291 Max, // Bottom right
1292 Center,
1293 };
1294 mem_copy(dest: OutAABB.m_aPoints, source: aPoints, size: sizeof(CPoint) * SAxisAlignedBoundingBox::NUM_POINTS);
1295}
1296
1297void CEditor::ApplyAlignments(const std::vector<SAlignmentInfo> &vAlignments, ivec2 &Offset)
1298{
1299 if(vAlignments.empty())
1300 return;
1301
1302 // To find the alignments we simply iterate through the vector of alignments and find the first
1303 // X and Y alignments.
1304 // Then, we use the saved m_Diff to adjust the offset
1305 bvec2 GotAdjust = bvec2(false, false);
1306 ivec2 Adjust = ivec2(0, 0);
1307 for(const SAlignmentInfo &Alignment : vAlignments)
1308 {
1309 if(Alignment.m_Axis == EAxis::X && !GotAdjust.y)
1310 {
1311 GotAdjust.y = true;
1312 Adjust.y = Alignment.m_Diff;
1313 }
1314 else if(Alignment.m_Axis == EAxis::Y && !GotAdjust.x)
1315 {
1316 GotAdjust.x = true;
1317 Adjust.x = Alignment.m_Diff;
1318 }
1319 }
1320
1321 Offset += Adjust;
1322}
1323
1324void CEditor::ApplyAxisAlignment(ivec2 &Offset) const
1325{
1326 // This is used to preserve axis alignment when pressing `Shift`
1327 // Should be called before any other computation
1328 EAxis Axis = GetDragAxis(Offset);
1329 Offset.x = ((Axis == EAxis::NONE || Axis == EAxis::X) ? Offset.x : 0);
1330 Offset.y = ((Axis == EAxis::NONE || Axis == EAxis::Y) ? Offset.y : 0);
1331}
1332
1333static CColor AverageColor(const std::vector<CQuad *> &vpQuads)
1334{
1335 CColor Average = {0, 0, 0, 0};
1336 for(CQuad *pQuad : vpQuads)
1337 {
1338 for(CColor Color : pQuad->m_aColors)
1339 {
1340 Average += Color;
1341 }
1342 }
1343 return Average / std::size(CQuad{}.m_aColors) / vpQuads.size();
1344}
1345
1346void CEditor::DoQuad(int LayerIndex, const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int Index)
1347{
1348 enum
1349 {
1350 OP_NONE = 0,
1351 OP_SELECT,
1352 OP_MOVE_ALL,
1353 OP_MOVE_PIVOT,
1354 OP_ROTATE,
1355 OP_CONTEXT_MENU,
1356 OP_DELETE,
1357 };
1358
1359 // some basic values
1360 const void *pId = &pQuad->m_aPoints[4]; // use pivot addr as id
1361 static std::vector<std::vector<CPoint>> s_vvRotatePoints;
1362 static int s_Operation = OP_NONE;
1363 static vec2 s_MouseStart = vec2(0.0f, 0.0f);
1364 static float s_RotateAngle = 0;
1365 static CPoint s_OriginalPosition;
1366 static std::vector<SAlignmentInfo> s_PivotAlignments; // Alignments per pivot per quad
1367 static std::vector<SAlignmentInfo> s_vAABBAlignments; // Alignments for one AABB (single quad or selection of multiple quads)
1368 static SAxisAlignedBoundingBox s_SelectionAABB; // Selection AABB
1369 static ivec2 s_LastOffset; // Last offset, stored as static so we can use it to draw every frame
1370
1371 // get pivot
1372 float CenterX = fx2f(v: pQuad->m_aPoints[4].x);
1373 float CenterY = fx2f(v: pQuad->m_aPoints[4].y);
1374
1375 const bool IgnoreGrid = Input()->AltIsPressed();
1376
1377 auto &&GetDragOffset = [&]() -> ivec2 {
1378 vec2 Pos = MapView()->MouseWorldPos();
1379 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
1380 {
1381 MapView()->MapGrid()->SnapToGrid(Position&: Pos);
1382 }
1383 return ivec2(f2fx(v: Pos.x) - s_OriginalPosition.x, f2fx(v: Pos.y) - s_OriginalPosition.y);
1384 };
1385
1386 // draw selection background
1387 if(Map()->IsQuadSelected(Index))
1388 {
1389 Graphics()->SetColor(r: 0, g: 0, b: 0, a: 1);
1390 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 7.0f * MapView()->MouseWorldScale(), 7.0f * MapView()->MouseWorldScale());
1391 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1392 }
1393
1394 if(Ui()->CheckActiveItem(pId))
1395 {
1396 if(MapView()->MouseDeltaWorld() != vec2(0.0f, 0.0f))
1397 {
1398 if(s_Operation == OP_SELECT)
1399 {
1400 if(length_squared(a: s_MouseStart - Ui()->MousePos()) > 20.0f)
1401 {
1402 if(!Map()->IsQuadSelected(Index))
1403 Map()->SelectQuad(Index);
1404
1405 s_OriginalPosition = pQuad->m_aPoints[4];
1406
1407 if(Input()->ShiftIsPressed())
1408 {
1409 s_Operation = OP_MOVE_PIVOT;
1410 // When moving, we need to save the original position of all selected pivots
1411 for(int Selected : Map()->m_vSelectedQuads)
1412 {
1413 const CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
1414 PreparePointDrag(pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: 4);
1415 }
1416 }
1417 else
1418 {
1419 s_Operation = OP_MOVE_ALL;
1420 // When moving, we need to save the original position of all selected quads points
1421 for(int Selected : Map()->m_vSelectedQuads)
1422 {
1423 const CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
1424 for(size_t v = 0; v < 5; v++)
1425 PreparePointDrag(pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: v);
1426 }
1427 // And precompute AABB of selection since it will not change during drag
1428 if(g_Config.m_EdAlignQuads)
1429 QuadSelectionAABB(pLayer, OutAABB&: s_SelectionAABB);
1430 }
1431 }
1432 }
1433
1434 // check if we only should move pivot
1435 if(s_Operation == OP_MOVE_PIVOT)
1436 {
1437 Map()->m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: Map()->m_vSelectedQuads, GroupIndex: -1, LayerIndex);
1438
1439 s_LastOffset = GetDragOffset(); // Update offset
1440 ApplyAxisAlignment(Offset&: s_LastOffset); // Apply axis alignment to the offset
1441
1442 ComputePointsAlignments(pLayer, Pivot: true, Offset: s_LastOffset, vAlignments&: s_PivotAlignments);
1443 ApplyAlignments(vAlignments: s_PivotAlignments, Offset&: s_LastOffset);
1444
1445 for(auto &Selected : Map()->m_vSelectedQuads)
1446 {
1447 CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
1448 DoPointDrag(pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: 4, Offset: s_LastOffset);
1449 }
1450 }
1451 else if(s_Operation == OP_MOVE_ALL)
1452 {
1453 Map()->m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: Map()->m_vSelectedQuads, GroupIndex: -1, LayerIndex);
1454
1455 // Compute drag offset
1456 s_LastOffset = GetDragOffset();
1457 ApplyAxisAlignment(Offset&: s_LastOffset);
1458
1459 // Then compute possible alignments with the selection AABB
1460 ComputeAABBAlignments(pLayer, AABB: s_SelectionAABB, Offset: s_LastOffset, vAlignments&: s_vAABBAlignments);
1461 // Apply alignments before drag
1462 ApplyAlignments(vAlignments: s_vAABBAlignments, Offset&: s_LastOffset);
1463 // Then do the drag
1464 for(int Selected : Map()->m_vSelectedQuads)
1465 {
1466 CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
1467 for(int v = 0; v < 5; v++)
1468 DoPointDrag(pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: v, Offset: s_LastOffset);
1469 }
1470 }
1471 else if(s_Operation == OP_ROTATE)
1472 {
1473 Map()->m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: Map()->m_vSelectedQuads, GroupIndex: -1, LayerIndex);
1474
1475 for(size_t i = 0; i < Map()->m_vSelectedQuads.size(); ++i)
1476 {
1477 CQuad *pCurrentQuad = &pLayer->m_vQuads[Map()->m_vSelectedQuads[i]];
1478 for(int v = 0; v < 4; v++)
1479 {
1480 pCurrentQuad->m_aPoints[v] = s_vvRotatePoints[i][v];
1481 Rotate(pCenter: &pCurrentQuad->m_aPoints[4], pPoint: &pCurrentQuad->m_aPoints[v], Rotation: s_RotateAngle);
1482 }
1483 }
1484
1485 s_RotateAngle += Ui()->MouseDeltaX() * (Input()->ShiftIsPressed() ? 0.0001f : 0.002f);
1486 }
1487 }
1488
1489 // Draw axis and alignments when moving
1490 if(s_Operation == OP_MOVE_PIVOT || s_Operation == OP_MOVE_ALL)
1491 {
1492 EAxis Axis = GetDragAxis(Offset: s_LastOffset);
1493 DrawAxis(Axis, OriginalPoint&: s_OriginalPosition, Point&: pQuad->m_aPoints[4]);
1494
1495 str_copy(dst&: m_aTooltip, src: "Hold shift to keep alignment on one axis.");
1496 }
1497
1498 if(s_Operation == OP_MOVE_PIVOT)
1499 DrawPointAlignments(vAlignments: s_PivotAlignments, Offset: s_LastOffset);
1500
1501 if(s_Operation == OP_MOVE_ALL)
1502 {
1503 DrawPointAlignments(vAlignments: s_vAABBAlignments, Offset: s_LastOffset);
1504
1505 if(g_Config.m_EdShowQuadsRect)
1506 DrawAABB(AABB: s_SelectionAABB, Offset: s_LastOffset);
1507 }
1508
1509 if(s_Operation == OP_CONTEXT_MENU)
1510 {
1511 if(!Ui()->MouseButton(Index: 1))
1512 {
1513 if(Map()->m_vSelectedLayers.size() == 1)
1514 {
1515 m_QuadPopupContext.m_pEditor = this;
1516 m_QuadPopupContext.m_SelectedQuadIndex = Map()->FindSelectedQuadIndex(Index);
1517 dbg_assert(m_QuadPopupContext.m_SelectedQuadIndex >= 0, "Selected quad index not found for quad popup");
1518 m_QuadPopupContext.m_Color = PackColor(Color: AverageColor(vpQuads: Map()->SelectedQuads()));
1519 Ui()->DoPopupMenu(pId: &m_QuadPopupContext, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 251, pContext: &m_QuadPopupContext, pfnFunc: PopupQuad);
1520 Ui()->DisableMouseLock();
1521 }
1522 s_Operation = OP_NONE;
1523 Ui()->SetActiveItem(nullptr);
1524 }
1525 }
1526 else if(s_Operation == OP_DELETE)
1527 {
1528 if(!Ui()->MouseButton(Index: 1))
1529 {
1530 if(Map()->m_vSelectedLayers.size() == 1)
1531 {
1532 Ui()->DisableMouseLock();
1533 Map()->OnModify();
1534 Map()->DeleteSelectedQuads();
1535 }
1536 s_Operation = OP_NONE;
1537 Ui()->SetActiveItem(nullptr);
1538 }
1539 }
1540 else if(s_Operation == OP_ROTATE)
1541 {
1542 if(Ui()->MouseButton(Index: 0))
1543 {
1544 Ui()->DisableMouseLock();
1545 s_Operation = OP_NONE;
1546 Ui()->SetActiveItem(nullptr);
1547 Map()->m_QuadTracker.EndQuadTrack();
1548 }
1549 else if(Ui()->MouseButton(Index: 1))
1550 {
1551 Ui()->DisableMouseLock();
1552 s_Operation = OP_NONE;
1553 Ui()->SetActiveItem(nullptr);
1554
1555 // Reset points to old position
1556 for(size_t i = 0; i < Map()->m_vSelectedQuads.size(); ++i)
1557 {
1558 CQuad *pCurrentQuad = &pLayer->m_vQuads[Map()->m_vSelectedQuads[i]];
1559 for(int v = 0; v < 4; v++)
1560 pCurrentQuad->m_aPoints[v] = s_vvRotatePoints[i][v];
1561 }
1562 }
1563 }
1564 else
1565 {
1566 if(!Ui()->MouseButton(Index: 0))
1567 {
1568 if(s_Operation == OP_SELECT)
1569 {
1570 if(Input()->ShiftIsPressed())
1571 Map()->ToggleSelectQuad(Index);
1572 else
1573 Map()->SelectQuad(Index);
1574 }
1575 else if(s_Operation == OP_MOVE_PIVOT || s_Operation == OP_MOVE_ALL)
1576 {
1577 Map()->m_QuadTracker.EndQuadTrack();
1578 }
1579
1580 Ui()->DisableMouseLock();
1581 s_Operation = OP_NONE;
1582 Ui()->SetActiveItem(nullptr);
1583
1584 s_LastOffset = ivec2();
1585 s_OriginalPosition = ivec2();
1586 s_vAABBAlignments.clear();
1587 s_PivotAlignments.clear();
1588 }
1589 }
1590
1591 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1592 }
1593 else if(Input()->KeyPress(Key: KEY_R) && !Map()->m_vSelectedQuads.empty() && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Ui()->IsPopupOpen())
1594 {
1595 Ui()->EnableMouseLock(pId);
1596 Ui()->SetActiveItem(pId);
1597 s_Operation = OP_ROTATE;
1598 s_RotateAngle = 0;
1599
1600 s_vvRotatePoints.clear();
1601 s_vvRotatePoints.resize(sz: Map()->m_vSelectedQuads.size());
1602 for(size_t i = 0; i < Map()->m_vSelectedQuads.size(); ++i)
1603 {
1604 CQuad *pCurrentQuad = &pLayer->m_vQuads[Map()->m_vSelectedQuads[i]];
1605
1606 s_vvRotatePoints[i].resize(sz: 4);
1607 s_vvRotatePoints[i][0] = pCurrentQuad->m_aPoints[0];
1608 s_vvRotatePoints[i][1] = pCurrentQuad->m_aPoints[1];
1609 s_vvRotatePoints[i][2] = pCurrentQuad->m_aPoints[2];
1610 s_vvRotatePoints[i][3] = pCurrentQuad->m_aPoints[3];
1611 }
1612 }
1613 else if(Ui()->HotItem() == pId)
1614 {
1615 m_pUiGotContext = pId;
1616
1617 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1618 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.");
1619
1620 if(Ui()->MouseButton(Index: 0))
1621 {
1622 Ui()->SetActiveItem(pId);
1623
1624 s_MouseStart = Ui()->MousePos();
1625 s_Operation = OP_SELECT;
1626 }
1627 else if(Ui()->MouseButtonClicked(Index: 1))
1628 {
1629 if(Input()->ShiftIsPressed())
1630 {
1631 s_Operation = OP_DELETE;
1632
1633 if(!Map()->IsQuadSelected(Index))
1634 Map()->SelectQuad(Index);
1635
1636 Ui()->SetActiveItem(pId);
1637 }
1638 else
1639 {
1640 s_Operation = OP_CONTEXT_MENU;
1641
1642 if(!Map()->IsQuadSelected(Index))
1643 Map()->SelectQuad(Index);
1644
1645 Ui()->SetActiveItem(pId);
1646 }
1647 }
1648 }
1649 else
1650 Graphics()->SetColor(r: 0, g: 1, b: 0, a: 1);
1651
1652 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * MapView()->MouseWorldScale(), 5.0f * MapView()->MouseWorldScale());
1653 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1654}
1655
1656void CEditor::DoQuadPoint(int LayerIndex, const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int QuadIndex, int V)
1657{
1658 const void *pId = &pQuad->m_aPoints[V];
1659 const vec2 Center = vec2(fx2f(v: pQuad->m_aPoints[V].x), fx2f(v: pQuad->m_aPoints[V].y));
1660 const bool IgnoreGrid = Input()->AltIsPressed();
1661
1662 // draw selection background
1663 if(Map()->IsQuadPointSelected(QuadIndex, Index: V))
1664 {
1665 Graphics()->SetColor(r: 0, g: 0, b: 0, a: 1);
1666 IGraphics::CQuadItem QuadItem(Center.x, Center.y, 7.0f * MapView()->MouseWorldScale(), 7.0f * MapView()->MouseWorldScale());
1667 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1668 }
1669
1670 enum
1671 {
1672 OP_NONE = 0,
1673 OP_SELECT,
1674 OP_MOVEPOINT,
1675 OP_MOVEUV,
1676 OP_CONTEXT_MENU
1677 };
1678
1679 static int s_Operation = OP_NONE;
1680 static vec2 s_MouseStart = vec2(0.0f, 0.0f);
1681 static CPoint s_OriginalPoint;
1682 static std::vector<SAlignmentInfo> s_Alignments; // Alignments
1683 static ivec2 s_LastOffset;
1684
1685 auto &&GetDragOffset = [&]() -> ivec2 {
1686 vec2 Pos = MapView()->MouseWorldPos();
1687 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
1688 {
1689 MapView()->MapGrid()->SnapToGrid(Position&: Pos);
1690 }
1691 return ivec2(f2fx(v: Pos.x) - s_OriginalPoint.x, f2fx(v: Pos.y) - s_OriginalPoint.y);
1692 };
1693
1694 if(Ui()->CheckActiveItem(pId))
1695 {
1696 if(MapView()->MouseDeltaWorld() != vec2(0.0f, 0.0f))
1697 {
1698 if(s_Operation == OP_SELECT)
1699 {
1700 if(length_squared(a: s_MouseStart - Ui()->MousePos()) > 20.0f)
1701 {
1702 if(!Map()->IsQuadPointSelected(QuadIndex, Index: V))
1703 Map()->SelectQuadPoint(QuadIndex, Index: V);
1704
1705 if(Input()->ShiftIsPressed())
1706 {
1707 s_Operation = OP_MOVEUV;
1708 Ui()->EnableMouseLock(pId);
1709 }
1710 else
1711 {
1712 s_Operation = OP_MOVEPOINT;
1713 // Save original positions before moving
1714 s_OriginalPoint = pQuad->m_aPoints[V];
1715 for(int Selected : Map()->m_vSelectedQuads)
1716 {
1717 for(int m = 0; m < 4; m++)
1718 if(Map()->IsQuadPointSelected(QuadIndex: Selected, Index: m))
1719 PreparePointDrag(pQuad: &pLayer->m_vQuads[Selected], QuadIndex: Selected, PointIndex: m);
1720 }
1721 }
1722 }
1723 }
1724
1725 if(s_Operation == OP_MOVEPOINT)
1726 {
1727 Map()->m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: Map()->m_vSelectedQuads, GroupIndex: -1, LayerIndex);
1728
1729 s_LastOffset = GetDragOffset(); // Update offset
1730 ApplyAxisAlignment(Offset&: s_LastOffset); // Apply axis alignment to offset
1731
1732 ComputePointsAlignments(pLayer, Pivot: false, Offset: s_LastOffset, vAlignments&: s_Alignments);
1733 ApplyAlignments(vAlignments: s_Alignments, Offset&: s_LastOffset);
1734
1735 for(int Selected : Map()->m_vSelectedQuads)
1736 {
1737 for(int m = 0; m < 4; m++)
1738 {
1739 if(Map()->IsQuadPointSelected(QuadIndex: Selected, Index: m))
1740 {
1741 DoPointDrag(pQuad: &pLayer->m_vQuads[Selected], QuadIndex: Selected, PointIndex: m, Offset: s_LastOffset);
1742 }
1743 }
1744 }
1745 }
1746 else if(s_Operation == OP_MOVEUV)
1747 {
1748 int SelectedPoints = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3);
1749
1750 Map()->m_QuadTracker.BeginQuadPointPropTrack(pLayer, vSelectedQuads: Map()->m_vSelectedQuads, SelectedQuadPoints: SelectedPoints, GroupIndex: -1, LayerIndex);
1751 Map()->m_QuadTracker.AddQuadPointPropTrack(Prop: EQuadPointProp::TEX_U);
1752 Map()->m_QuadTracker.AddQuadPointPropTrack(Prop: EQuadPointProp::TEX_V);
1753
1754 for(int Selected : Map()->m_vSelectedQuads)
1755 {
1756 CQuad *pSelectedQuad = &pLayer->m_vQuads[Selected];
1757 for(int m = 0; m < 4; m++)
1758 {
1759 if(Map()->IsQuadPointSelected(QuadIndex: Selected, Index: m))
1760 {
1761 // 0,2;1,3 - line x
1762 // 0,1;2,3 - line y
1763
1764 pSelectedQuad->m_aTexcoords[m].x += f2fx(v: MapView()->MouseDeltaWorld().x * 0.001f);
1765 pSelectedQuad->m_aTexcoords[(m + 2) % 4].x += f2fx(v: MapView()->MouseDeltaWorld().x * 0.001f);
1766
1767 pSelectedQuad->m_aTexcoords[m].y += f2fx(v: MapView()->MouseDeltaWorld().y * 0.001f);
1768 pSelectedQuad->m_aTexcoords[m ^ 1].y += f2fx(v: MapView()->MouseDeltaWorld().y * 0.001f);
1769 }
1770 }
1771 }
1772 }
1773 }
1774
1775 // Draw axis and alignments when dragging
1776 if(s_Operation == OP_MOVEPOINT)
1777 {
1778 Graphics()->SetColor(r: 1, g: 0, b: 0.1f, a: 1);
1779
1780 // Axis
1781 EAxis Axis = GetDragAxis(Offset: s_LastOffset);
1782 DrawAxis(Axis, OriginalPoint&: s_OriginalPoint, Point&: pQuad->m_aPoints[V]);
1783
1784 // Alignments
1785 DrawPointAlignments(vAlignments: s_Alignments, Offset: s_LastOffset);
1786
1787 str_copy(dst&: m_aTooltip, src: "Hold shift to keep alignment on one axis.");
1788 }
1789
1790 if(s_Operation == OP_CONTEXT_MENU)
1791 {
1792 if(!Ui()->MouseButton(Index: 1))
1793 {
1794 if(Map()->m_vSelectedLayers.size() == 1)
1795 {
1796 if(!Map()->IsQuadSelected(Index: QuadIndex))
1797 Map()->SelectQuad(Index: QuadIndex);
1798
1799 m_PointPopupContext.m_pEditor = this;
1800 m_PointPopupContext.m_SelectedQuadPoint = V;
1801 m_PointPopupContext.m_SelectedQuadIndex = Map()->FindSelectedQuadIndex(Index: QuadIndex);
1802 dbg_assert(m_PointPopupContext.m_SelectedQuadIndex >= 0, "Selected quad index not found for quad point popup");
1803 Ui()->DoPopupMenu(pId: &m_PointPopupContext, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 75, pContext: &m_PointPopupContext, pfnFunc: PopupPoint);
1804 }
1805 Ui()->SetActiveItem(nullptr);
1806 }
1807 }
1808 else
1809 {
1810 if(!Ui()->MouseButton(Index: 0))
1811 {
1812 if(s_Operation == OP_SELECT)
1813 {
1814 if(Input()->ShiftIsPressed())
1815 Map()->ToggleSelectQuadPoint(QuadIndex, Index: V);
1816 else
1817 Map()->SelectQuadPoint(QuadIndex, Index: V);
1818 }
1819
1820 if(s_Operation == OP_MOVEPOINT)
1821 {
1822 Map()->m_QuadTracker.EndQuadTrack();
1823 }
1824 else if(s_Operation == OP_MOVEUV)
1825 {
1826 Map()->m_QuadTracker.EndQuadPointPropTrackAll();
1827 }
1828
1829 Ui()->DisableMouseLock();
1830 Ui()->SetActiveItem(nullptr);
1831 }
1832 }
1833
1834 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1835 }
1836 else if(Ui()->HotItem() == pId)
1837 {
1838 m_pUiGotContext = pId;
1839
1840 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1841 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold shift to move the texture. Hold alt to ignore grid.");
1842
1843 if(Ui()->MouseButton(Index: 0))
1844 {
1845 Ui()->SetActiveItem(pId);
1846
1847 s_MouseStart = Ui()->MousePos();
1848 s_Operation = OP_SELECT;
1849 }
1850 else if(Ui()->MouseButtonClicked(Index: 1))
1851 {
1852 s_Operation = OP_CONTEXT_MENU;
1853
1854 Ui()->SetActiveItem(pId);
1855
1856 if(!Map()->IsQuadPointSelected(QuadIndex, Index: V))
1857 Map()->SelectQuadPoint(QuadIndex, Index: V);
1858 }
1859 }
1860 else
1861 Graphics()->SetColor(r: 1, g: 0, b: 0, a: 1);
1862
1863 IGraphics::CQuadItem QuadItem(Center.x, Center.y, 5.0f * MapView()->MouseWorldScale(), 5.0f * MapView()->MouseWorldScale());
1864 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1865}
1866
1867void CEditor::DoQuadEnvelopes(const CLayerQuads *pLayerQuads)
1868{
1869 const std::vector<CQuad> &vQuads = pLayerQuads->m_vQuads;
1870 if(vQuads.empty())
1871 {
1872 return;
1873 }
1874
1875 std::vector<std::pair<const CQuad *, CEnvelope *>> vQuadsWithEnvelopes;
1876 vQuadsWithEnvelopes.reserve(n: vQuads.size());
1877 for(const auto &Quad : vQuads)
1878 {
1879 if(m_ActiveEnvelopePreview != EEnvelopePreview::ALL &&
1880 !(m_ActiveEnvelopePreview == EEnvelopePreview::SELECTED && Quad.m_PosEnv == Map()->m_SelectedEnvelope))
1881 {
1882 continue;
1883 }
1884 if(Quad.m_PosEnv < 0 ||
1885 Quad.m_PosEnv >= (int)Map()->m_vpEnvelopes.size() ||
1886 Map()->m_vpEnvelopes[Quad.m_PosEnv]->m_vPoints.empty())
1887 {
1888 continue;
1889 }
1890 vQuadsWithEnvelopes.emplace_back(args: &Quad, args: Map()->m_vpEnvelopes[Quad.m_PosEnv].get());
1891 }
1892 if(vQuadsWithEnvelopes.empty())
1893 {
1894 return;
1895 }
1896
1897 Map()->SelectedGroup()->MapScreen();
1898
1899 // Draw lines between points
1900 Graphics()->TextureClear();
1901 IGraphics::CLineItemBatch LineItemBatch;
1902 Graphics()->LinesBatchBegin(pBatch: &LineItemBatch);
1903 Graphics()->SetColor(ColorRGBA(0.0f, 1.0f, 1.0f, 0.75f));
1904 for(const auto &[pQuad, pEnvelope] : vQuadsWithEnvelopes)
1905 {
1906 if(pEnvelope->m_vPoints.size() < 2)
1907 {
1908 continue;
1909 }
1910
1911 const CPoint *pPivotPoint = &pQuad->m_aPoints[4];
1912 const vec2 PivotPoint = vec2(fx2f(v: pPivotPoint->x), fx2f(v: pPivotPoint->y));
1913
1914 for(int PointIndex = 0; PointIndex <= (int)pEnvelope->m_vPoints.size() - 2; PointIndex++)
1915 {
1916 const auto &PointStart = pEnvelope->m_vPoints[PointIndex];
1917 const auto &PointEnd = pEnvelope->m_vPoints[PointIndex + 1];
1918 const float PointStartTime = PointStart.m_Time.AsSeconds();
1919 const float PointEndTime = PointEnd.m_Time.AsSeconds();
1920 const float TimeRange = PointEndTime - PointStartTime;
1921
1922 int Steps;
1923 if(PointStart.m_Curvetype == CURVETYPE_BEZIER)
1924 {
1925 Steps = std::clamp(val: round_to_int(f: TimeRange * 10.0f), lo: 50, hi: 150);
1926 }
1927 else
1928 {
1929 Steps = 1;
1930 }
1931 ColorRGBA StartPosition = PointStart.ColorValue();
1932 for(int Step = 1; Step <= Steps; Step++)
1933 {
1934 ColorRGBA EndPosition;
1935 if(Step == Steps)
1936 {
1937 EndPosition = PointEnd.ColorValue();
1938 }
1939 else
1940 {
1941 const float SectionEndTime = PointStartTime + TimeRange * (Step / (float)Steps);
1942 EndPosition = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
1943 pEnvelope->Eval(Time: SectionEndTime, Result&: EndPosition, Channels: 2);
1944 }
1945
1946 const vec2 Pos0 = PivotPoint + vec2(StartPosition.r, StartPosition.g);
1947 const vec2 Pos1 = PivotPoint + vec2(EndPosition.r, EndPosition.g);
1948 const IGraphics::CLineItem Item = IGraphics::CLineItem(Pos0, Pos1);
1949 Graphics()->LinesBatchDraw(pBatch: &LineItemBatch, pArray: &Item, Num: 1);
1950
1951 StartPosition = EndPosition;
1952 }
1953 }
1954 }
1955 Graphics()->LinesBatchEnd(pBatch: &LineItemBatch);
1956
1957 // Draw quads at points
1958 if(pLayerQuads->m_Image >= 0 && pLayerQuads->m_Image < (int)Map()->m_vpImages.size())
1959 {
1960 Graphics()->TextureSet(Texture: Map()->m_vpImages[pLayerQuads->m_Image]->m_Texture);
1961 }
1962 else
1963 {
1964 Graphics()->TextureClear();
1965 }
1966 Graphics()->QuadsBegin();
1967 for(const auto &[pQuad, pEnvelope] : vQuadsWithEnvelopes)
1968 {
1969 for(size_t PointIndex = 0; PointIndex < pEnvelope->m_vPoints.size(); PointIndex++)
1970 {
1971 const CEnvPoint_runtime &EnvPoint = pEnvelope->m_vPoints[PointIndex];
1972 const vec2 Offset = vec2(fx2f(v: EnvPoint.m_aValues[0]), fx2f(v: EnvPoint.m_aValues[1]));
1973 const float Rotation = fx2f(v: EnvPoint.m_aValues[2]) / 180.0f * pi;
1974
1975 const float Alpha = (Map()->m_SelectedQuadEnvelope == pQuad->m_PosEnv && Map()->IsEnvPointSelected(Index: PointIndex)) ? 0.65f : 0.35f;
1976 Graphics()->SetColor4(
1977 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),
1978 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),
1979 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),
1980 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));
1981
1982 const CPoint *pPoints;
1983 CPoint aRotated[4];
1984 if(Rotation != 0.0f)
1985 {
1986 std::copy_n(first: pQuad->m_aPoints, n: std::size(aRotated), result: aRotated);
1987 for(auto &Point : aRotated)
1988 {
1989 Rotate(pCenter: &pQuad->m_aPoints[4], pPoint: &Point, Rotation);
1990 }
1991 pPoints = aRotated;
1992 }
1993 else
1994 {
1995 pPoints = pQuad->m_aPoints;
1996 }
1997 Graphics()->QuadsSetSubsetFree(
1998 x0: fx2f(v: pQuad->m_aTexcoords[0].x), y0: fx2f(v: pQuad->m_aTexcoords[0].y),
1999 x1: fx2f(v: pQuad->m_aTexcoords[1].x), y1: fx2f(v: pQuad->m_aTexcoords[1].y),
2000 x2: fx2f(v: pQuad->m_aTexcoords[2].x), y2: fx2f(v: pQuad->m_aTexcoords[2].y),
2001 x3: fx2f(v: pQuad->m_aTexcoords[3].x), y3: fx2f(v: pQuad->m_aTexcoords[3].y));
2002
2003 const IGraphics::CFreeformItem Freeform(
2004 fx2f(v: pPoints[0].x) + Offset.x, fx2f(v: pPoints[0].y) + Offset.y,
2005 fx2f(v: pPoints[1].x) + Offset.x, fx2f(v: pPoints[1].y) + Offset.y,
2006 fx2f(v: pPoints[2].x) + Offset.x, fx2f(v: pPoints[2].y) + Offset.y,
2007 fx2f(v: pPoints[3].x) + Offset.x, fx2f(v: pPoints[3].y) + Offset.y);
2008 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2009 }
2010 }
2011 Graphics()->QuadsEnd();
2012
2013 // Draw quad envelope point handles
2014 Graphics()->TextureClear();
2015 Graphics()->QuadsBegin();
2016 for(const auto &[pQuad, pEnvelope] : vQuadsWithEnvelopes)
2017 {
2018 for(size_t PointIndex = 0; PointIndex < pEnvelope->m_vPoints.size(); PointIndex++)
2019 {
2020 DoQuadEnvPoint(pQuad, pEnvelope, QuadIndex: pQuad - vQuads.data(), PointIndex);
2021 }
2022 }
2023 Graphics()->QuadsEnd();
2024}
2025
2026void CEditor::DoQuadEnvPoint(const CQuad *pQuad, CEnvelope *pEnvelope, int QuadIndex, int PointIndex)
2027{
2028 CEnvPoint_runtime *pPoint = &pEnvelope->m_vPoints[PointIndex];
2029 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]));
2030 const bool IgnoreGrid = Input()->AltIsPressed();
2031
2032 if(Ui()->CheckActiveItem(pId: pPoint) && Map()->m_CurrentQuadIndex == QuadIndex)
2033 {
2034 if(MapView()->MouseDeltaWorld() != vec2(0.0f, 0.0f))
2035 {
2036 if(m_QuadEnvelopePointOperation == EQuadEnvelopePointOperation::MOVE)
2037 {
2038 vec2 Pos = MapView()->MouseWorldPos();
2039 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
2040 {
2041 MapView()->MapGrid()->SnapToGrid(Position&: Pos);
2042 }
2043 pPoint->m_aValues[0] = f2fx(v: Pos.x) - pQuad->m_aPoints[4].x;
2044 pPoint->m_aValues[1] = f2fx(v: Pos.y) - pQuad->m_aPoints[4].y;
2045 }
2046 else if(m_QuadEnvelopePointOperation == EQuadEnvelopePointOperation::ROTATE)
2047 {
2048 pPoint->m_aValues[2] += 10 * Ui()->MouseDeltaX();
2049 }
2050 }
2051
2052 if(!Ui()->MouseButton(Index: 0))
2053 {
2054 Ui()->DisableMouseLock();
2055 m_QuadEnvelopePointOperation = EQuadEnvelopePointOperation::NONE;
2056 Ui()->SetActiveItem(nullptr);
2057 }
2058
2059 Graphics()->SetColor(ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
2060 }
2061 else if(Ui()->HotItem() == pPoint && Map()->m_CurrentQuadIndex == QuadIndex)
2062 {
2063 Graphics()->SetColor(ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
2064 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold ctrl to rotate. Hold alt to ignore grid.");
2065
2066 if(Ui()->MouseButton(Index: 0))
2067 {
2068 if(Input()->ModifierIsPressed())
2069 {
2070 Ui()->EnableMouseLock(pId: pPoint);
2071 m_QuadEnvelopePointOperation = EQuadEnvelopePointOperation::ROTATE;
2072 }
2073 else
2074 {
2075 m_QuadEnvelopePointOperation = EQuadEnvelopePointOperation::MOVE;
2076 }
2077 Map()->SelectQuad(Index: QuadIndex);
2078 Map()->SelectEnvPoint(Index: PointIndex);
2079 Map()->m_SelectedQuadEnvelope = pQuad->m_PosEnv;
2080 Ui()->SetActiveItem(pPoint);
2081 }
2082 else
2083 {
2084 Map()->DeselectEnvPoints();
2085 Map()->m_SelectedQuadEnvelope = -1;
2086 }
2087 }
2088 else
2089 {
2090 Graphics()->SetColor(ColorRGBA(0.0f, 1.0f, 1.0f, 1.0f));
2091 }
2092
2093 IGraphics::CQuadItem QuadItem(Center.x, Center.y, 5.0f * MapView()->MouseWorldScale(), 5.0f * MapView()->MouseWorldScale());
2094 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
2095}
2096
2097void CEditor::UpdateHotQuadPoint(const CLayerQuads *pLayer)
2098{
2099 const vec2 MouseWorld = MapView()->MouseWorldPos();
2100
2101 float MinDist = 500.0f;
2102 const void *pMinPointId = nullptr;
2103
2104 const auto UpdateMinimum = [&](vec2 Position, const void *pId) {
2105 const float CurrDist = length_squared(a: (Position - MouseWorld) / MapView()->MouseWorldScale());
2106 if(CurrDist < MinDist)
2107 {
2108 MinDist = CurrDist;
2109 pMinPointId = pId;
2110 return true;
2111 }
2112 return false;
2113 };
2114
2115 for(const CQuad &Quad : pLayer->m_vQuads)
2116 {
2117 if(m_ShowEnvelopePreview &&
2118 m_ActiveEnvelopePreview != EEnvelopePreview::NONE &&
2119 Quad.m_PosEnv >= 0 &&
2120 Quad.m_PosEnv < (int)Map()->m_vpEnvelopes.size())
2121 {
2122 for(const auto &EnvPoint : Map()->m_vpEnvelopes[Quad.m_PosEnv]->m_vPoints)
2123 {
2124 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]));
2125 if(UpdateMinimum(Position, &EnvPoint) && Ui()->ActiveItem() == nullptr)
2126 {
2127 Map()->m_CurrentQuadIndex = &Quad - pLayer->m_vQuads.data();
2128 }
2129 }
2130 }
2131
2132 for(const auto &Point : Quad.m_aPoints)
2133 {
2134 UpdateMinimum(vec2(fx2f(v: Point.x), fx2f(v: Point.y)), &Point);
2135 }
2136 }
2137
2138 if(pMinPointId != nullptr)
2139 {
2140 Ui()->SetHotItem(pMinPointId);
2141 }
2142}
2143
2144void CEditor::DoColorPickerButton(const void *pId, const CUIRect *pRect, ColorRGBA Color, const std::function<void(ColorRGBA Color)> &SetColor)
2145{
2146 CUIRect ColorRect;
2147 pRect->Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * Ui()->ButtonColorMul(pId)), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
2148 pRect->Margin(Cut: 1.0f, pOtherRect: &ColorRect);
2149 ColorRect.Draw(Color, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
2150
2151 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.");
2152 if(Input()->ShiftIsPressed())
2153 {
2154 if(ButtonResult == 1)
2155 {
2156 std::string Clipboard = Input()->GetClipboardText();
2157 if(Clipboard[0] == '#' || Clipboard[0] == '$') // ignore leading # (web color format) and $ (console color format)
2158 Clipboard = Clipboard.substr(pos: 1);
2159 if(str_isallnum_hex(str: Clipboard.c_str()))
2160 {
2161 std::optional<ColorRGBA> ParsedColor = color_parse<ColorRGBA>(pStr: Clipboard.c_str());
2162 if(ParsedColor)
2163 {
2164 m_ColorPickerPopupContext.m_State = EEditState::ONE_GO;
2165 SetColor(ParsedColor.value());
2166 }
2167 }
2168 }
2169 else if(ButtonResult == 2)
2170 {
2171 char aClipboard[9];
2172 str_format(buffer: aClipboard, buffer_size: sizeof(aClipboard), format: "%08X", Color.PackAlphaLast());
2173 Input()->SetClipboardText(aClipboard);
2174 }
2175 }
2176 else if(ButtonResult > 0)
2177 {
2178 if(m_ColorPickerPopupContext.m_ColorMode == CUi::SColorPickerPopupContext::MODE_UNSET)
2179 m_ColorPickerPopupContext.m_ColorMode = CUi::SColorPickerPopupContext::MODE_RGBA;
2180 m_ColorPickerPopupContext.m_RgbaColor = Color;
2181 m_ColorPickerPopupContext.m_HslaColor = color_cast<ColorHSLA>(rgb: Color);
2182 m_ColorPickerPopupContext.m_HsvaColor = color_cast<ColorHSVA>(hsl: m_ColorPickerPopupContext.m_HslaColor);
2183 m_ColorPickerPopupContext.m_Alpha = true;
2184 m_pColorPickerPopupActiveId = pId;
2185 Ui()->ShowPopupColorPicker(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext: &m_ColorPickerPopupContext);
2186 }
2187
2188 if(Ui()->IsPopupOpen(pId: &m_ColorPickerPopupContext))
2189 {
2190 if(m_pColorPickerPopupActiveId == pId)
2191 SetColor(m_ColorPickerPopupContext.m_RgbaColor);
2192 }
2193 else
2194 {
2195 m_pColorPickerPopupActiveId = nullptr;
2196 if(m_ColorPickerPopupContext.m_State == EEditState::EDITING)
2197 {
2198 m_ColorPickerPopupContext.m_State = EEditState::END;
2199 SetColor(m_ColorPickerPopupContext.m_RgbaColor);
2200 m_ColorPickerPopupContext.m_State = EEditState::NONE;
2201 }
2202 }
2203}
2204
2205bool CEditor::IsAllowPlaceUnusedTiles() const
2206{
2207 // explicit allow and implicit allow
2208 return m_AllowPlaceUnusedTiles != EUnusedEntities::NOT_ALLOWED;
2209}
2210
2211void CEditor::CRenderLayersState::Reset()
2212{
2213 m_ScrollRegion.Reset();
2214 m_Operation = ELayerOperation::NONE;
2215 m_PreviousOperation = ELayerOperation::NONE;
2216 m_pDraggedButton = nullptr;
2217 m_InitialMouseY = 0.0f;
2218 m_InitialCutHeight = 0.0f;
2219 m_ScrollToSelectionNext = false;
2220 m_InitialGroupIndex = 0;
2221 m_vInitialLayerIndices.clear();
2222 m_LayerPopupContext = {};
2223}
2224
2225void CEditor::RenderLayers(CUIRect LayersBox)
2226{
2227 CRenderLayersState &State = m_RenderLayersState;
2228
2229 const float RowHeight = 12.0f;
2230 char aBuf[64];
2231
2232 CUIRect UnscrolledLayersBox = LayersBox;
2233
2234 CScrollRegionParams ScrollParams;
2235 ScrollParams.m_ScrollbarThickness = 10.0f;
2236 ScrollParams.m_ScrollbarMargin = 3.0f;
2237 ScrollParams.m_ScrollUnit = RowHeight * 5.0f;
2238 State.m_ScrollRegion.Begin(pClipRect: &LayersBox, pParams: &ScrollParams);
2239
2240 constexpr float MinDragDistance = 5.0f;
2241 int GroupAfterDraggedLayer = -1;
2242 int LayerAfterDraggedLayer = -1;
2243 bool DraggedPositionFound = false;
2244 bool MoveLayers = false;
2245 bool MoveGroup = false;
2246 bool StartDragLayer = false;
2247 bool StartDragGroup = false;
2248 std::vector<int> vButtonsPerGroup;
2249
2250 auto SetOperation = [&](ELayerOperation Operation) {
2251 if(Operation != State.m_Operation)
2252 {
2253 State.m_PreviousOperation = State.m_Operation;
2254 State.m_Operation = Operation;
2255 if(Operation == ELayerOperation::NONE)
2256 {
2257 State.m_pDraggedButton = nullptr;
2258 }
2259 }
2260 };
2261
2262 vButtonsPerGroup.reserve(n: Map()->m_vpGroups.size());
2263 for(const std::shared_ptr<CLayerGroup> &pGroup : Map()->m_vpGroups)
2264 {
2265 vButtonsPerGroup.push_back(x: pGroup->m_vpLayers.size() + 1);
2266 }
2267
2268 if(State.m_pDraggedButton != nullptr && Ui()->ActiveItem() != State.m_pDraggedButton)
2269 {
2270 SetOperation(ELayerOperation::NONE);
2271 }
2272
2273 if(State.m_Operation == ELayerOperation::LAYER_DRAG || State.m_Operation == ELayerOperation::GROUP_DRAG)
2274 {
2275 float MinDraggableValue = UnscrolledLayersBox.y;
2276 float MaxDraggableValue = MinDraggableValue;
2277 for(int NumButtons : vButtonsPerGroup)
2278 {
2279 MaxDraggableValue += NumButtons * (RowHeight + 2.0f) + 5.0f;
2280 }
2281 MaxDraggableValue += LayersBox.y - UnscrolledLayersBox.y;
2282
2283 if(State.m_Operation == ELayerOperation::GROUP_DRAG)
2284 {
2285 MaxDraggableValue -= vButtonsPerGroup[Map()->m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f;
2286 }
2287 else if(State.m_Operation == ELayerOperation::LAYER_DRAG)
2288 {
2289 MinDraggableValue += RowHeight + 2.0f;
2290 MaxDraggableValue -= Map()->m_vSelectedLayers.size() * (RowHeight + 2.0f) + 5.0f;
2291 }
2292
2293 UnscrolledLayersBox.HSplitTop(Cut: State.m_InitialCutHeight, pTop: nullptr, pBottom: &UnscrolledLayersBox);
2294 UnscrolledLayersBox.y -= State.m_InitialMouseY - Ui()->MouseY();
2295
2296 UnscrolledLayersBox.y = std::clamp(val: UnscrolledLayersBox.y, lo: MinDraggableValue, hi: MaxDraggableValue);
2297
2298 UnscrolledLayersBox.w = LayersBox.w;
2299 }
2300
2301 const bool ScrollToSelection = LayerSelector()->SelectByTile() || State.m_ScrollToSelectionNext;
2302 State.m_ScrollToSelectionNext = false;
2303
2304 // render layers
2305 for(int g = 0; g < (int)Map()->m_vpGroups.size(); g++)
2306 {
2307 if(State.m_Operation == ELayerOperation::LAYER_DRAG && g > 0 && !DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight / 2)
2308 {
2309 DraggedPositionFound = true;
2310 GroupAfterDraggedLayer = g;
2311
2312 LayerAfterDraggedLayer = Map()->m_vpGroups[g - 1]->m_vpLayers.size();
2313
2314 CUIRect Slot;
2315 LayersBox.HSplitTop(Cut: Map()->m_vSelectedLayers.size() * (RowHeight + 2.0f), pTop: &Slot, pBottom: &LayersBox);
2316 State.m_ScrollRegion.AddRect(Rect: Slot);
2317 }
2318
2319 CUIRect Slot, VisibleToggle;
2320 if(State.m_Operation == ELayerOperation::GROUP_DRAG)
2321 {
2322 if(g == Map()->m_SelectedGroup)
2323 {
2324 UnscrolledLayersBox.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: &UnscrolledLayersBox);
2325 UnscrolledLayersBox.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &UnscrolledLayersBox);
2326 }
2327 else if(!DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight * vButtonsPerGroup[g] / 2 + 3.0f)
2328 {
2329 DraggedPositionFound = true;
2330 GroupAfterDraggedLayer = g;
2331
2332 CUIRect TmpSlot;
2333 if(Map()->m_vpGroups[Map()->m_SelectedGroup]->m_Collapse)
2334 LayersBox.HSplitTop(Cut: RowHeight + 7.0f, pTop: &TmpSlot, pBottom: &LayersBox);
2335 else
2336 LayersBox.HSplitTop(Cut: vButtonsPerGroup[Map()->m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f, pTop: &TmpSlot, pBottom: &LayersBox);
2337 State.m_ScrollRegion.AddRect(Rect: TmpSlot, ShouldScrollHere: false);
2338 }
2339 }
2340 if(State.m_Operation != ELayerOperation::GROUP_DRAG || g != Map()->m_SelectedGroup)
2341 {
2342 LayersBox.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: &LayersBox);
2343
2344 CUIRect TmpRect;
2345 LayersBox.HSplitTop(Cut: 2.0f, pTop: &TmpRect, pBottom: &LayersBox);
2346 State.m_ScrollRegion.AddRect(Rect: TmpRect);
2347 }
2348
2349 if(State.m_ScrollRegion.AddRect(Rect: Slot))
2350 {
2351 Slot.VSplitLeft(Cut: 15.0f, pLeft: &VisibleToggle, pRight: &Slot);
2352
2353 const int MouseClick = DoButton_FontIcon(pId: &Map()->m_vpGroups[g]->m_Visible, pText: Map()->m_vpGroups[g]->m_Visible ? FontIcon::EYE : FontIcon::EYE_SLASH, Checked: Map()->m_vpGroups[g]->m_Collapse ? 1 : 0, pRect: &VisibleToggle, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Left click to toggle visibility. Right click to show this group only.", Corners: IGraphics::CORNER_L, FontSize: 8.0f);
2354 if(MouseClick == 1)
2355 {
2356 Map()->m_vpGroups[g]->m_Visible = !Map()->m_vpGroups[g]->m_Visible;
2357 }
2358 else if(MouseClick == 2)
2359 {
2360 if(Input()->ShiftIsPressed())
2361 {
2362 if(g != Map()->m_SelectedGroup)
2363 Map()->SelectLayer(LayerIndex: 0, GroupIndex: g);
2364 }
2365
2366 int NumActive = 0;
2367 for(auto &Group : Map()->m_vpGroups)
2368 {
2369 if(Group == Map()->m_vpGroups[g])
2370 {
2371 Group->m_Visible = true;
2372 continue;
2373 }
2374
2375 if(Group->m_Visible)
2376 {
2377 Group->m_Visible = false;
2378 NumActive++;
2379 }
2380 }
2381 if(NumActive == 0)
2382 {
2383 for(auto &Group : Map()->m_vpGroups)
2384 {
2385 Group->m_Visible = true;
2386 }
2387 }
2388 }
2389
2390 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "#%d %s", g, Map()->m_vpGroups[g]->m_aName);
2391
2392 bool Clicked;
2393 bool Abrupted;
2394 if(int Result = DoButton_DraggableEx(pId: Map()->m_vpGroups[g].get(), pText: aBuf, Checked: g == Map()->m_SelectedGroup, pRect: &Slot, pClicked: &Clicked, pAbrupted: &Abrupted,
2395 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))
2396 {
2397 if(State.m_Operation == ELayerOperation::NONE)
2398 {
2399 State.m_InitialMouseY = Ui()->MouseY();
2400 State.m_InitialCutHeight = State.m_InitialMouseY - UnscrolledLayersBox.y;
2401 SetOperation(ELayerOperation::CLICK);
2402
2403 if(g != Map()->m_SelectedGroup)
2404 Map()->SelectLayer(LayerIndex: 0, GroupIndex: g);
2405 }
2406
2407 if(Abrupted)
2408 {
2409 SetOperation(ELayerOperation::NONE);
2410 }
2411
2412 if(State.m_Operation == ELayerOperation::CLICK && absolute(a: Ui()->MouseY() - State.m_InitialMouseY) > MinDragDistance)
2413 {
2414 StartDragGroup = true;
2415 State.m_pDraggedButton = Map()->m_vpGroups[g].get();
2416 }
2417
2418 if(State.m_Operation == ELayerOperation::CLICK && Clicked)
2419 {
2420 if(g != Map()->m_SelectedGroup)
2421 Map()->SelectLayer(LayerIndex: 0, GroupIndex: g);
2422
2423 if(Input()->ShiftIsPressed() && Map()->m_SelectedGroup == g)
2424 {
2425 Map()->m_vSelectedLayers.clear();
2426 for(size_t i = 0; i < Map()->m_vpGroups[g]->m_vpLayers.size(); i++)
2427 {
2428 Map()->AddSelectedLayer(LayerIndex: i);
2429 }
2430 }
2431
2432 if(Result == 2)
2433 {
2434 Ui()->DoPopupMenu(pId: &State.m_PopupGroupId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 145, Height: 256, pContext: this, pfnFunc: PopupGroup);
2435 }
2436
2437 if(!Map()->m_vpGroups[g]->m_vpLayers.empty() && Ui()->DoDoubleClickLogic(pId: Map()->m_vpGroups[g].get()))
2438 Map()->m_vpGroups[g]->m_Collapse ^= 1;
2439
2440 SetOperation(ELayerOperation::NONE);
2441 }
2442
2443 if(State.m_Operation == ELayerOperation::GROUP_DRAG && Clicked)
2444 MoveGroup = true;
2445 }
2446 else if(State.m_pDraggedButton == Map()->m_vpGroups[g].get())
2447 {
2448 SetOperation(ELayerOperation::NONE);
2449 }
2450 }
2451
2452 for(int i = 0; i < (int)Map()->m_vpGroups[g]->m_vpLayers.size(); i++)
2453 {
2454 if(Map()->m_vpGroups[g]->m_Collapse)
2455 continue;
2456
2457 bool IsLayerSelected = false;
2458 if(Map()->m_SelectedGroup == g)
2459 {
2460 for(const auto &Selected : Map()->m_vSelectedLayers)
2461 {
2462 if(Selected == i)
2463 {
2464 IsLayerSelected = true;
2465 break;
2466 }
2467 }
2468 }
2469
2470 if(State.m_Operation == ELayerOperation::GROUP_DRAG && g == Map()->m_SelectedGroup)
2471 {
2472 UnscrolledLayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &UnscrolledLayersBox);
2473 }
2474 else if(State.m_Operation == ELayerOperation::LAYER_DRAG)
2475 {
2476 if(IsLayerSelected)
2477 {
2478 UnscrolledLayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &UnscrolledLayersBox);
2479 }
2480 else
2481 {
2482 if(!DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight / 2)
2483 {
2484 DraggedPositionFound = true;
2485 GroupAfterDraggedLayer = g + 1;
2486 LayerAfterDraggedLayer = i;
2487 for(size_t j = 0; j < Map()->m_vSelectedLayers.size(); j++)
2488 {
2489 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: nullptr, pBottom: &LayersBox);
2490 State.m_ScrollRegion.AddRect(Rect: Slot);
2491 }
2492 }
2493 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &LayersBox);
2494 if(!State.m_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: ScrollToSelection && IsLayerSelected))
2495 continue;
2496 }
2497 }
2498 else
2499 {
2500 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &LayersBox);
2501 if(!State.m_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: ScrollToSelection && IsLayerSelected))
2502 continue;
2503 }
2504
2505 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
2506
2507 CUIRect Button;
2508 Slot.VSplitLeft(Cut: 12.0f, pLeft: nullptr, pRight: &Slot);
2509 Slot.VSplitLeft(Cut: 15.0f, pLeft: &VisibleToggle, pRight: &Button);
2510
2511 const int MouseClick = DoButton_FontIcon(pId: &Map()->m_vpGroups[g]->m_vpLayers[i]->m_Visible, pText: Map()->m_vpGroups[g]->m_vpLayers[i]->m_Visible ? FontIcon::EYE : FontIcon::EYE_SLASH, Checked: 0, pRect: &VisibleToggle, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Left click to toggle visibility. Right click to show only this layer within its group.", Corners: IGraphics::CORNER_L, FontSize: 8.0f);
2512 if(MouseClick == 1)
2513 {
2514 Map()->m_vpGroups[g]->m_vpLayers[i]->m_Visible = !Map()->m_vpGroups[g]->m_vpLayers[i]->m_Visible;
2515 }
2516 else if(MouseClick == 2)
2517 {
2518 if(Input()->ShiftIsPressed())
2519 {
2520 if(!IsLayerSelected)
2521 Map()->SelectLayer(LayerIndex: i, GroupIndex: g);
2522 }
2523
2524 int NumActive = 0;
2525 for(auto &Layer : Map()->m_vpGroups[g]->m_vpLayers)
2526 {
2527 if(Layer == Map()->m_vpGroups[g]->m_vpLayers[i])
2528 {
2529 Layer->m_Visible = true;
2530 continue;
2531 }
2532
2533 if(Layer->m_Visible)
2534 {
2535 Layer->m_Visible = false;
2536 NumActive++;
2537 }
2538 }
2539 if(NumActive == 0)
2540 {
2541 for(auto &Layer : Map()->m_vpGroups[g]->m_vpLayers)
2542 {
2543 Layer->m_Visible = true;
2544 }
2545 }
2546 }
2547
2548 if(Map()->m_vpGroups[g]->m_vpLayers[i]->m_aName[0])
2549 str_copy(dst&: aBuf, src: Map()->m_vpGroups[g]->m_vpLayers[i]->m_aName);
2550 else
2551 {
2552 if(Map()->m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_TILES)
2553 {
2554 std::shared_ptr<CLayerTiles> pTiles = std::static_pointer_cast<CLayerTiles>(r: Map()->m_vpGroups[g]->m_vpLayers[i]);
2555 str_copy(dst&: aBuf, src: pTiles->m_Image >= 0 ? Map()->m_vpImages[pTiles->m_Image]->m_aName : "Tiles");
2556 }
2557 else if(Map()->m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_QUADS)
2558 {
2559 std::shared_ptr<CLayerQuads> pQuads = std::static_pointer_cast<CLayerQuads>(r: Map()->m_vpGroups[g]->m_vpLayers[i]);
2560 str_copy(dst&: aBuf, src: pQuads->m_Image >= 0 ? Map()->m_vpImages[pQuads->m_Image]->m_aName : "Quads");
2561 }
2562 else if(Map()->m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_SOUNDS)
2563 {
2564 std::shared_ptr<CLayerSounds> pSounds = std::static_pointer_cast<CLayerSounds>(r: Map()->m_vpGroups[g]->m_vpLayers[i]);
2565 str_copy(dst&: aBuf, src: pSounds->m_Sound >= 0 ? Map()->m_vpSounds[pSounds->m_Sound]->m_aName : "Sounds");
2566 }
2567 }
2568
2569 int Checked = IsLayerSelected ? 1 : 0;
2570 if(Map()->m_vpGroups[g]->m_vpLayers[i]->IsEntitiesLayer())
2571 {
2572 Checked += 6;
2573 }
2574
2575 bool Clicked;
2576 bool Abrupted;
2577 if(int Result = DoButton_DraggableEx(pId: Map()->m_vpGroups[g]->m_vpLayers[i].get(), pText: aBuf, Checked, pRect: &Button, pClicked: &Clicked, pAbrupted: &Abrupted,
2578 Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Select layer. Hold shift to select multiple.", Corners: IGraphics::CORNER_R))
2579 {
2580 if(State.m_Operation == ELayerOperation::NONE)
2581 {
2582 State.m_InitialMouseY = Ui()->MouseY();
2583 State.m_InitialCutHeight = State.m_InitialMouseY - UnscrolledLayersBox.y;
2584
2585 SetOperation(ELayerOperation::CLICK);
2586
2587 if(!Input()->ShiftIsPressed() && !IsLayerSelected)
2588 {
2589 Map()->SelectLayer(LayerIndex: i, GroupIndex: g);
2590 }
2591 }
2592
2593 if(Abrupted)
2594 {
2595 SetOperation(ELayerOperation::NONE);
2596 }
2597
2598 if(State.m_Operation == ELayerOperation::CLICK && absolute(a: Ui()->MouseY() - State.m_InitialMouseY) > MinDragDistance)
2599 {
2600 bool EntitiesLayerSelected = false;
2601 for(int k : Map()->m_vSelectedLayers)
2602 {
2603 if(Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers[k]->IsEntitiesLayer())
2604 EntitiesLayerSelected = true;
2605 }
2606
2607 if(!EntitiesLayerSelected)
2608 StartDragLayer = true;
2609
2610 State.m_pDraggedButton = Map()->m_vpGroups[g]->m_vpLayers[i].get();
2611 }
2612
2613 if(State.m_Operation == ELayerOperation::CLICK && Clicked)
2614 {
2615 State.m_LayerPopupContext.m_pEditor = this;
2616 if(Result == 1)
2617 {
2618 if(Input()->ShiftIsPressed() && Map()->m_SelectedGroup == g)
2619 {
2620 auto Position = std::find(first: Map()->m_vSelectedLayers.begin(), last: Map()->m_vSelectedLayers.end(), val: i);
2621 if(Position != Map()->m_vSelectedLayers.end())
2622 Map()->m_vSelectedLayers.erase(position: Position);
2623 else
2624 Map()->AddSelectedLayer(LayerIndex: i);
2625 }
2626 else if(!Input()->ShiftIsPressed())
2627 {
2628 Map()->SelectLayer(LayerIndex: i, GroupIndex: g);
2629 }
2630 }
2631 else if(Result == 2)
2632 {
2633 State.m_LayerPopupContext.m_vpLayers.clear();
2634 State.m_LayerPopupContext.m_vLayerIndices.clear();
2635
2636 if(!IsLayerSelected)
2637 {
2638 Map()->SelectLayer(LayerIndex: i, GroupIndex: g);
2639 }
2640
2641 if(Map()->m_vSelectedLayers.size() > 1)
2642 {
2643 // move right clicked layer to first index to render correct popup
2644 if(Map()->m_vSelectedLayers[0] != i)
2645 {
2646 auto Position = std::find(first: Map()->m_vSelectedLayers.begin(), last: Map()->m_vSelectedLayers.end(), val: i);
2647 std::swap(a&: Map()->m_vSelectedLayers[0], b&: *Position);
2648 }
2649
2650 bool AllTile = true;
2651 for(size_t j = 0; AllTile && j < Map()->m_vSelectedLayers.size(); j++)
2652 {
2653 int LayerIndex = Map()->m_vSelectedLayers[j];
2654 if(Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers[LayerIndex]->m_Type == LAYERTYPE_TILES)
2655 {
2656 State.m_LayerPopupContext.m_vpLayers.push_back(x: std::static_pointer_cast<CLayerTiles>(r: Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers[Map()->m_vSelectedLayers[j]]));
2657 State.m_LayerPopupContext.m_vLayerIndices.push_back(x: LayerIndex);
2658 }
2659 else
2660 AllTile = false;
2661 }
2662
2663 // Don't allow editing if all selected layers are not tile layers
2664 if(!AllTile)
2665 {
2666 State.m_LayerPopupContext.m_vpLayers.clear();
2667 State.m_LayerPopupContext.m_vLayerIndices.clear();
2668 }
2669 }
2670
2671 Ui()->DoPopupMenu(pId: &State.m_LayerPopupContext, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 150, Height: 300, pContext: &State.m_LayerPopupContext, pfnFunc: PopupLayer);
2672 }
2673
2674 SetOperation(ELayerOperation::NONE);
2675 }
2676
2677 if(State.m_Operation == ELayerOperation::LAYER_DRAG && Clicked)
2678 {
2679 MoveLayers = true;
2680 }
2681 }
2682 else if(State.m_pDraggedButton == Map()->m_vpGroups[g]->m_vpLayers[i].get())
2683 {
2684 SetOperation(ELayerOperation::NONE);
2685 }
2686 }
2687
2688 if(State.m_Operation != ELayerOperation::GROUP_DRAG || g != Map()->m_SelectedGroup)
2689 {
2690 LayersBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &LayersBox);
2691 State.m_ScrollRegion.AddRect(Rect: Slot);
2692 }
2693 }
2694
2695 if(!DraggedPositionFound && State.m_Operation == ELayerOperation::LAYER_DRAG)
2696 {
2697 GroupAfterDraggedLayer = Map()->m_vpGroups.size();
2698 LayerAfterDraggedLayer = Map()->m_vpGroups[GroupAfterDraggedLayer - 1]->m_vpLayers.size();
2699
2700 CUIRect TmpSlot;
2701 LayersBox.HSplitTop(Cut: Map()->m_vSelectedLayers.size() * (RowHeight + 2.0f), pTop: &TmpSlot, pBottom: &LayersBox);
2702 State.m_ScrollRegion.AddRect(Rect: TmpSlot);
2703 }
2704
2705 if(!DraggedPositionFound && State.m_Operation == ELayerOperation::GROUP_DRAG)
2706 {
2707 GroupAfterDraggedLayer = Map()->m_vpGroups.size();
2708
2709 CUIRect TmpSlot;
2710 if(Map()->m_vpGroups[Map()->m_SelectedGroup]->m_Collapse)
2711 LayersBox.HSplitTop(Cut: RowHeight + 7.0f, pTop: &TmpSlot, pBottom: &LayersBox);
2712 else
2713 LayersBox.HSplitTop(Cut: vButtonsPerGroup[Map()->m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f, pTop: &TmpSlot, pBottom: &LayersBox);
2714 State.m_ScrollRegion.AddRect(Rect: TmpSlot, ShouldScrollHere: false);
2715 }
2716
2717 if(MoveLayers && 1 <= GroupAfterDraggedLayer && GroupAfterDraggedLayer <= (int)Map()->m_vpGroups.size())
2718 {
2719 std::vector<std::shared_ptr<CLayer>> &vpNewGroupLayers = Map()->m_vpGroups[GroupAfterDraggedLayer - 1]->m_vpLayers;
2720 if(0 <= LayerAfterDraggedLayer && LayerAfterDraggedLayer <= (int)vpNewGroupLayers.size())
2721 {
2722 std::vector<std::shared_ptr<CLayer>> vpSelectedLayers;
2723 std::vector<std::shared_ptr<CLayer>> &vpSelectedGroupLayers = Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers;
2724 std::shared_ptr<CLayer> pNextLayer = nullptr;
2725 if(LayerAfterDraggedLayer < (int)vpNewGroupLayers.size())
2726 pNextLayer = vpNewGroupLayers[LayerAfterDraggedLayer];
2727
2728 std::sort(first: Map()->m_vSelectedLayers.begin(), last: Map()->m_vSelectedLayers.end(), comp: std::greater<>());
2729 for(int k : Map()->m_vSelectedLayers)
2730 {
2731 vpSelectedLayers.insert(position: vpSelectedLayers.begin(), x: vpSelectedGroupLayers[k]);
2732 }
2733 for(int k : Map()->m_vSelectedLayers)
2734 {
2735 vpSelectedGroupLayers.erase(position: vpSelectedGroupLayers.begin() + k);
2736 }
2737
2738 auto InsertPosition = std::find(first: vpNewGroupLayers.begin(), last: vpNewGroupLayers.end(), val: pNextLayer);
2739 int InsertPositionIndex = InsertPosition - vpNewGroupLayers.begin();
2740 vpNewGroupLayers.insert(position: InsertPosition, first: vpSelectedLayers.begin(), last: vpSelectedLayers.end());
2741
2742 int NumSelectedLayers = Map()->m_vSelectedLayers.size();
2743 Map()->m_vSelectedLayers.clear();
2744 for(int i = 0; i < NumSelectedLayers; i++)
2745 Map()->m_vSelectedLayers.push_back(x: InsertPositionIndex + i);
2746
2747 Map()->m_SelectedGroup = GroupAfterDraggedLayer - 1;
2748 Map()->OnModify();
2749 }
2750 }
2751
2752 if(MoveGroup && 0 <= GroupAfterDraggedLayer && GroupAfterDraggedLayer <= (int)Map()->m_vpGroups.size())
2753 {
2754 std::shared_ptr<CLayerGroup> pSelectedGroup = Map()->m_vpGroups[Map()->m_SelectedGroup];
2755 std::shared_ptr<CLayerGroup> pNextGroup = nullptr;
2756 if(GroupAfterDraggedLayer < (int)Map()->m_vpGroups.size())
2757 pNextGroup = Map()->m_vpGroups[GroupAfterDraggedLayer];
2758
2759 Map()->m_vpGroups.erase(position: Map()->m_vpGroups.begin() + Map()->m_SelectedGroup);
2760
2761 auto InsertPosition = std::find(first: Map()->m_vpGroups.begin(), last: Map()->m_vpGroups.end(), val: pNextGroup);
2762 Map()->m_vpGroups.insert(position: InsertPosition, x: pSelectedGroup);
2763
2764 auto Pos = std::find(first: Map()->m_vpGroups.begin(), last: Map()->m_vpGroups.end(), val: pSelectedGroup);
2765 Map()->m_SelectedGroup = Pos - Map()->m_vpGroups.begin();
2766
2767 Map()->OnModify();
2768 }
2769
2770 if(MoveLayers || MoveGroup)
2771 {
2772 SetOperation(ELayerOperation::NONE);
2773 }
2774 if(StartDragLayer)
2775 {
2776 SetOperation(ELayerOperation::LAYER_DRAG);
2777 State.m_InitialGroupIndex = Map()->m_SelectedGroup;
2778 State.m_vInitialLayerIndices = std::vector(Map()->m_vSelectedLayers);
2779 }
2780 if(StartDragGroup)
2781 {
2782 State.m_InitialGroupIndex = Map()->m_SelectedGroup;
2783 SetOperation(ELayerOperation::GROUP_DRAG);
2784 }
2785
2786 if(State.m_Operation == ELayerOperation::LAYER_DRAG || State.m_Operation == ELayerOperation::GROUP_DRAG)
2787 {
2788 if(State.m_pDraggedButton == nullptr)
2789 {
2790 SetOperation(ELayerOperation::NONE);
2791 }
2792 else
2793 {
2794 State.m_ScrollRegion.DoEdgeScrolling();
2795 Ui()->SetActiveItem(State.m_pDraggedButton);
2796 }
2797 }
2798
2799 if(Input()->KeyPress(Key: KEY_DOWN) && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && CLineInput::GetActiveInput() == nullptr && State.m_Operation == ELayerOperation::NONE)
2800 {
2801 if(Input()->ShiftIsPressed())
2802 {
2803 if(Map()->m_vSelectedLayers[Map()->m_vSelectedLayers.size() - 1] < (int)Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers.size() - 1)
2804 Map()->AddSelectedLayer(LayerIndex: Map()->m_vSelectedLayers[Map()->m_vSelectedLayers.size() - 1] + 1);
2805 }
2806 else
2807 {
2808 Map()->SelectNextLayer();
2809 }
2810 State.m_ScrollToSelectionNext = true;
2811 }
2812 if(Input()->KeyPress(Key: KEY_UP) && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && CLineInput::GetActiveInput() == nullptr && State.m_Operation == ELayerOperation::NONE)
2813 {
2814 if(Input()->ShiftIsPressed())
2815 {
2816 if(Map()->m_vSelectedLayers[Map()->m_vSelectedLayers.size() - 1] > 0)
2817 Map()->AddSelectedLayer(LayerIndex: Map()->m_vSelectedLayers[Map()->m_vSelectedLayers.size() - 1] - 1);
2818 }
2819 else
2820 {
2821 Map()->SelectPreviousLayer();
2822 }
2823
2824 State.m_ScrollToSelectionNext = true;
2825 }
2826
2827 CUIRect AddGroupButton, CollapseAllButton;
2828 LayersBox.HSplitTop(Cut: RowHeight + 1.0f, pTop: &AddGroupButton, pBottom: &LayersBox);
2829 if(State.m_ScrollRegion.AddRect(Rect: AddGroupButton))
2830 {
2831 AddGroupButton.HSplitTop(Cut: RowHeight, pTop: &AddGroupButton, pBottom: nullptr);
2832 if(DoButton_Editor(pId: &State.m_AddGroupButtonId, pText: m_QuickActionAddGroup.Label(), Checked: 0, pRect: &AddGroupButton, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionAddGroup.Description()))
2833 {
2834 m_QuickActionAddGroup.Call();
2835 }
2836 }
2837
2838 LayersBox.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &LayersBox);
2839 LayersBox.HSplitTop(Cut: RowHeight + 1.0f, pTop: &CollapseAllButton, pBottom: &LayersBox);
2840 if(State.m_ScrollRegion.AddRect(Rect: CollapseAllButton))
2841 {
2842 size_t TotalCollapsed = 0;
2843 for(const auto &pGroup : Map()->m_vpGroups)
2844 {
2845 if(pGroup->m_vpLayers.empty() || pGroup->m_Collapse)
2846 {
2847 TotalCollapsed++;
2848 }
2849 }
2850
2851 const char *pActionText = TotalCollapsed == Map()->m_vpGroups.size() ? "Expand all" : "Collapse all";
2852
2853 CollapseAllButton.HSplitTop(Cut: RowHeight, pTop: &CollapseAllButton, pBottom: nullptr);
2854 if(DoButton_Editor(pId: &State.m_CollapseAllButtonId, pText: pActionText, Checked: 0, pRect: &CollapseAllButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Expand or collapse all groups."))
2855 {
2856 for(const auto &pGroup : Map()->m_vpGroups)
2857 {
2858 if(TotalCollapsed == Map()->m_vpGroups.size())
2859 pGroup->m_Collapse = false;
2860 else if(!pGroup->m_vpLayers.empty())
2861 pGroup->m_Collapse = true;
2862 }
2863 }
2864 }
2865
2866 State.m_ScrollRegion.End();
2867
2868 if(State.m_Operation == ELayerOperation::NONE)
2869 {
2870 if(State.m_PreviousOperation == ELayerOperation::GROUP_DRAG)
2871 {
2872 State.m_PreviousOperation = ELayerOperation::NONE;
2873 Map()->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEditGroupProp>(args: Map(), args&: Map()->m_SelectedGroup, args: EGroupProp::ORDER, args&: State.m_InitialGroupIndex, args&: Map()->m_SelectedGroup));
2874 }
2875 else if(State.m_PreviousOperation == ELayerOperation::LAYER_DRAG)
2876 {
2877 if(State.m_InitialGroupIndex != Map()->m_SelectedGroup)
2878 {
2879 Map()->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEditLayersGroupAndOrder>(args: Map(), args&: State.m_InitialGroupIndex, args&: State.m_vInitialLayerIndices, args&: Map()->m_SelectedGroup, args&: Map()->m_vSelectedLayers));
2880 }
2881 else
2882 {
2883 std::vector<std::shared_ptr<IEditorAction>> vpActions;
2884 std::vector<int> vLayerIndices = Map()->m_vSelectedLayers;
2885 std::sort(first: vLayerIndices.begin(), last: vLayerIndices.end());
2886 std::sort(first: State.m_vInitialLayerIndices.begin(), last: State.m_vInitialLayerIndices.end());
2887 for(int k = 0; k < (int)vLayerIndices.size(); k++)
2888 {
2889 int LayerIndex = vLayerIndices[k];
2890 vpActions.push_back(x: std::make_shared<CEditorActionEditLayerProp>(args: Map(), args&: Map()->m_SelectedGroup, args&: LayerIndex, args: ELayerProp::ORDER, args&: State.m_vInitialLayerIndices[k], args&: LayerIndex));
2891 }
2892 Map()->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionBulk>(args: Map(), args&: vpActions, args: nullptr, args: true));
2893 }
2894 State.m_PreviousOperation = ELayerOperation::NONE;
2895 }
2896 }
2897}
2898
2899bool CEditor::ReplaceImage(const char *pFilename, int StorageType, bool CheckDuplicate)
2900{
2901 // check if we have that image already
2902 char aBuf[IO_MAX_PATH_LENGTH];
2903 fs_split_file_extension(filename: fs_filename(path: pFilename), name: aBuf, name_size: sizeof(aBuf));
2904 if(CheckDuplicate)
2905 {
2906 for(const auto &pImage : Map()->m_vpImages)
2907 {
2908 if(!str_comp(a: pImage->m_aName, b: aBuf))
2909 {
2910 ShowFileDialogError(pFormat: "Image named '%s' was already added.", pImage->m_aName);
2911 return false;
2912 }
2913 }
2914 }
2915
2916 CImageInfo ImgInfo;
2917 if(!Graphics()->LoadPng(Image&: ImgInfo, pFilename, StorageType))
2918 {
2919 ShowFileDialogError(pFormat: "Failed to load image from file '%s'.", pFilename);
2920 return false;
2921 }
2922
2923 std::shared_ptr<CEditorImage> pImg = Map()->SelectedImage();
2924 pImg->CEditorImage::Free();
2925 *pImg = std::move(ImgInfo);
2926 str_copy(dst&: pImg->m_aName, src: aBuf);
2927 pImg->m_External = IsVanillaImage(pImage: pImg->m_aName);
2928
2929 ConvertToRgba(Image&: *pImg);
2930 DilateImage(Image: *pImg);
2931
2932 pImg->m_AutoMapper.Load(pTileName: pImg->m_aName);
2933 int TextureLoadFlag = Graphics()->TextureLoadFlags();
2934 if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0)
2935 TextureLoadFlag = 0;
2936 pImg->m_Texture = Graphics()->LoadTextureRaw(Image: *pImg, Flags: TextureLoadFlag, pTexName: pFilename);
2937
2938 Map()->SortImages();
2939 Map()->SelectImage(pImage: pImg);
2940 OnDialogClose();
2941 return true;
2942}
2943
2944bool CEditor::ReplaceImageCallback(const char *pFilename, int StorageType, void *pUser)
2945{
2946 return static_cast<CEditor *>(pUser)->ReplaceImage(pFilename, StorageType, CheckDuplicate: true);
2947}
2948
2949bool CEditor::AddImage(const char *pFilename, int StorageType, void *pUser)
2950{
2951 CEditor *pEditor = (CEditor *)pUser;
2952
2953 // check if we have that image already
2954 char aBuf[IO_MAX_PATH_LENGTH];
2955 fs_split_file_extension(filename: fs_filename(path: pFilename), name: aBuf, name_size: sizeof(aBuf));
2956 for(const auto &pImage : pEditor->Map()->m_vpImages)
2957 {
2958 if(!str_comp(a: pImage->m_aName, b: aBuf))
2959 {
2960 pEditor->ShowFileDialogError(pFormat: "Image named '%s' was already added.", pImage->m_aName);
2961 return false;
2962 }
2963 }
2964
2965 if(pEditor->Map()->m_vpImages.size() >= MAX_MAPIMAGES)
2966 {
2967 pEditor->m_PopupEventType = POPEVENT_IMAGE_MAX;
2968 pEditor->m_PopupEventActivated = true;
2969 return false;
2970 }
2971
2972 CImageInfo ImgInfo;
2973 if(!pEditor->Graphics()->LoadPng(Image&: ImgInfo, pFilename, StorageType))
2974 {
2975 pEditor->ShowFileDialogError(pFormat: "Failed to load image from file '%s'.", pFilename);
2976 return false;
2977 }
2978
2979 std::shared_ptr<CEditorImage> pImg = std::make_shared<CEditorImage>(args: pEditor->Map());
2980 *pImg = std::move(ImgInfo);
2981
2982 pImg->m_External = IsVanillaImage(pImage: aBuf);
2983
2984 ConvertToRgba(Image&: *pImg);
2985 DilateImage(Image: *pImg);
2986
2987 int TextureLoadFlag = pEditor->Graphics()->TextureLoadFlags();
2988 if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0)
2989 TextureLoadFlag = 0;
2990 pImg->m_Texture = pEditor->Graphics()->LoadTextureRaw(Image: *pImg, Flags: TextureLoadFlag, pTexName: pFilename);
2991 str_copy(dst&: pImg->m_aName, src: aBuf);
2992 pImg->m_AutoMapper.Load(pTileName: pImg->m_aName);
2993 pEditor->Map()->m_vpImages.push_back(x: pImg);
2994 pEditor->Map()->SortImages();
2995 pEditor->Map()->SelectImage(pImage: pImg);
2996 pEditor->OnDialogClose();
2997 return true;
2998}
2999
3000bool CEditor::AddSound(const char *pFilename, int StorageType, void *pUser)
3001{
3002 CEditor *pEditor = (CEditor *)pUser;
3003
3004 // check if we have that sound already
3005 char aBuf[IO_MAX_PATH_LENGTH];
3006 fs_split_file_extension(filename: fs_filename(path: pFilename), name: aBuf, name_size: sizeof(aBuf));
3007 for(const auto &pSound : pEditor->Map()->m_vpSounds)
3008 {
3009 if(!str_comp(a: pSound->m_aName, b: aBuf))
3010 {
3011 pEditor->ShowFileDialogError(pFormat: "Sound named '%s' was already added.", pSound->m_aName);
3012 return false;
3013 }
3014 }
3015
3016 if(pEditor->Map()->m_vpSounds.size() >= MAX_MAPSOUNDS)
3017 {
3018 pEditor->m_PopupEventType = POPEVENT_SOUND_MAX;
3019 pEditor->m_PopupEventActivated = true;
3020 return false;
3021 }
3022
3023 // load external
3024 void *pData;
3025 unsigned DataSize;
3026 if(!pEditor->Storage()->ReadFile(pFilename, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
3027 {
3028 pEditor->ShowFileDialogError(pFormat: "Failed to open sound file '%s'.", pFilename);
3029 return false;
3030 }
3031
3032 // load sound
3033 const int SoundId = pEditor->Sound()->LoadOpusFromMem(pData, DataSize, ForceLoad: true, pContextName: pFilename);
3034 if(SoundId == -1)
3035 {
3036 free(ptr: pData);
3037 pEditor->ShowFileDialogError(pFormat: "Failed to load sound from file '%s'.", pFilename);
3038 return false;
3039 }
3040
3041 // add sound
3042 std::shared_ptr<CEditorSound> pSound = std::make_shared<CEditorSound>(args: pEditor->Map());
3043 pSound->m_SoundId = SoundId;
3044 pSound->m_DataSize = DataSize;
3045 pSound->m_pData = pData;
3046 str_copy(dst&: pSound->m_aName, src: aBuf);
3047 pEditor->Map()->m_vpSounds.push_back(x: pSound);
3048
3049 pEditor->Map()->SelectSound(pSound);
3050 pEditor->OnDialogClose();
3051 return true;
3052}
3053
3054bool CEditor::ReplaceSound(const char *pFilename, int StorageType, bool CheckDuplicate)
3055{
3056 // check if we have that sound already
3057 char aBuf[IO_MAX_PATH_LENGTH];
3058 fs_split_file_extension(filename: fs_filename(path: pFilename), name: aBuf, name_size: sizeof(aBuf));
3059 if(CheckDuplicate)
3060 {
3061 for(const auto &pSound : Map()->m_vpSounds)
3062 {
3063 if(!str_comp(a: pSound->m_aName, b: aBuf))
3064 {
3065 ShowFileDialogError(pFormat: "Sound named '%s' was already added.", pSound->m_aName);
3066 return false;
3067 }
3068 }
3069 }
3070
3071 // load external
3072 void *pData;
3073 unsigned DataSize;
3074 if(!Storage()->ReadFile(pFilename, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
3075 {
3076 ShowFileDialogError(pFormat: "Failed to open sound file '%s'.", pFilename);
3077 return false;
3078 }
3079
3080 // load sound
3081 const int SoundId = Sound()->LoadOpusFromMem(pData, DataSize, ForceLoad: true, pContextName: pFilename);
3082 if(SoundId == -1)
3083 {
3084 free(ptr: pData);
3085 ShowFileDialogError(pFormat: "Failed to load sound from file '%s'.", pFilename);
3086 return false;
3087 }
3088
3089 std::shared_ptr<CEditorSound> pSound = Map()->SelectedSound();
3090
3091 if(m_ToolbarPreviewSound == pSound->m_SoundId)
3092 {
3093 m_ToolbarPreviewSound = SoundId;
3094 }
3095
3096 // unload sample
3097 Sound()->UnloadSample(SampleId: pSound->m_SoundId);
3098 free(ptr: pSound->m_pData);
3099
3100 // replace sound
3101 str_copy(dst&: pSound->m_aName, src: aBuf);
3102 pSound->m_SoundId = SoundId;
3103 pSound->m_pData = pData;
3104 pSound->m_DataSize = DataSize;
3105
3106 Map()->SelectSound(pSound);
3107 OnDialogClose();
3108 return true;
3109}
3110
3111bool CEditor::ReplaceSoundCallback(const char *pFilename, int StorageType, void *pUser)
3112{
3113 return static_cast<CEditor *>(pUser)->ReplaceSound(pFilename, StorageType, CheckDuplicate: true);
3114}
3115
3116void CEditor::RenderImagesList(CUIRect ToolBox)
3117{
3118 const float RowHeight = 12.0f;
3119
3120 static CScrollRegion s_ScrollRegion;
3121 CScrollRegionParams ScrollParams;
3122 ScrollParams.m_ScrollbarThickness = 10.0f;
3123 ScrollParams.m_ScrollbarMargin = 3.0f;
3124 ScrollParams.m_ScrollUnit = RowHeight * 5;
3125 s_ScrollRegion.Begin(pClipRect: &ToolBox, pParams: &ScrollParams);
3126
3127 bool ScrollToSelection = false;
3128 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Map()->m_vpImages.empty())
3129 {
3130 if(Input()->KeyPress(Key: KEY_DOWN))
3131 {
3132 const int OldImage = Map()->m_SelectedImage;
3133 Map()->SelectNextImage();
3134 ScrollToSelection = OldImage != Map()->m_SelectedImage;
3135 }
3136 else if(Input()->KeyPress(Key: KEY_UP))
3137 {
3138 const int OldImage = Map()->m_SelectedImage;
3139 Map()->SelectPreviousImage();
3140 ScrollToSelection = OldImage != Map()->m_SelectedImage;
3141 }
3142 }
3143
3144 for(int e = 0; e < 2; e++) // two passes, first embedded, then external
3145 {
3146 CUIRect Slot;
3147 ToolBox.HSplitTop(Cut: RowHeight + 3.0f, pTop: &Slot, pBottom: &ToolBox);
3148 if(s_ScrollRegion.AddRect(Rect: Slot))
3149 Ui()->DoLabel(pRect: &Slot, pText: e == 0 ? "Embedded" : "External", Size: 12.0f, Align: TEXTALIGN_MC);
3150
3151 for(int i = 0; i < (int)Map()->m_vpImages.size(); i++)
3152 {
3153 if((e && !Map()->m_vpImages[i]->m_External) ||
3154 (!e && Map()->m_vpImages[i]->m_External))
3155 {
3156 continue;
3157 }
3158
3159 ToolBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &ToolBox);
3160 int Selected = Map()->m_SelectedImage == i;
3161 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: Selected && ScrollToSelection))
3162 continue;
3163 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
3164
3165 const bool ImageUsed = std::any_of(first: Map()->m_vpGroups.cbegin(), last: Map()->m_vpGroups.cend(), pred: [i](const auto &pGroup) {
3166 return std::any_of(pGroup->m_vpLayers.cbegin(), pGroup->m_vpLayers.cend(), [i](const auto &pLayer) {
3167 if(pLayer->m_Type == LAYERTYPE_QUADS)
3168 return std::static_pointer_cast<CLayerQuads>(pLayer)->m_Image == i;
3169 else if(pLayer->m_Type == LAYERTYPE_TILES)
3170 return std::static_pointer_cast<CLayerTiles>(pLayer)->m_Image == i;
3171 return false;
3172 });
3173 });
3174
3175 if(!ImageUsed)
3176 Selected += 2; // Image is unused
3177
3178 if(Selected < 2 && e == 1)
3179 {
3180 if(!IsVanillaImage(pImage: Map()->m_vpImages[i]->m_aName))
3181 {
3182 Selected += 4; // Image should be embedded
3183 }
3184 }
3185
3186 if(int Result = DoButton_Ex(pId: &Map()->m_vpImages[i], pText: Map()->m_vpImages[i]->m_aName, Checked: Selected, pRect: &Slot,
3187 Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Select image.", Corners: IGraphics::CORNER_ALL))
3188 {
3189 Map()->m_SelectedImage = i;
3190
3191 if(Result == 2)
3192 {
3193 const int Height = Map()->SelectedImage()->m_External ? 73 : 107;
3194 static SPopupMenuId s_PopupImageId;
3195 Ui()->DoPopupMenu(pId: &s_PopupImageId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 140, Height, pContext: this, pfnFunc: PopupImage);
3196 }
3197 }
3198 }
3199
3200 // separator
3201 ToolBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &ToolBox);
3202 if(s_ScrollRegion.AddRect(Rect: Slot))
3203 {
3204 IGraphics::CLineItem LineItem(Slot.x, Slot.y + Slot.h / 2, Slot.x + Slot.w, Slot.y + Slot.h / 2);
3205 Graphics()->TextureClear();
3206 Graphics()->LinesBegin();
3207 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
3208 Graphics()->LinesEnd();
3209 }
3210 }
3211
3212 // new image
3213 static int s_AddImageButton = 0;
3214 CUIRect AddImageButton;
3215 ToolBox.HSplitTop(Cut: 5.0f + RowHeight + 1.0f, pTop: &AddImageButton, pBottom: &ToolBox);
3216 if(s_ScrollRegion.AddRect(Rect: AddImageButton))
3217 {
3218 AddImageButton.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &AddImageButton);
3219 AddImageButton.HSplitTop(Cut: RowHeight, pTop: &AddImageButton, pBottom: nullptr);
3220 if(DoButton_Editor(pId: &s_AddImageButton, pText: m_QuickActionAddImage.Label(), Checked: 0, pRect: &AddImageButton, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionAddImage.Description()))
3221 m_QuickActionAddImage.Call();
3222 }
3223 s_ScrollRegion.End();
3224}
3225
3226void CEditor::RenderSelectedImage(CUIRect View) const
3227{
3228 std::shared_ptr<CEditorImage> pSelectedImage = Map()->SelectedImage();
3229 if(pSelectedImage == nullptr)
3230 return;
3231
3232 View.Margin(Cut: 10.0f, pOtherRect: &View);
3233 if(View.h < View.w)
3234 View.w = View.h;
3235 else
3236 View.h = View.w;
3237 float Max = std::max(a: pSelectedImage->m_Width, b: pSelectedImage->m_Height);
3238 View.w *= pSelectedImage->m_Width / Max;
3239 View.h *= pSelectedImage->m_Height / Max;
3240 Graphics()->TextureSet(Texture: pSelectedImage->m_Texture);
3241 Graphics()->WrapClamp();
3242 Graphics()->QuadsBegin();
3243 IGraphics::CQuadItem QuadItem(View.x, View.y, View.w, View.h);
3244 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
3245 Graphics()->QuadsEnd();
3246 Graphics()->WrapNormal();
3247}
3248
3249void CEditor::RenderSounds(CUIRect ToolBox)
3250{
3251 const float RowHeight = 12.0f;
3252
3253 static CScrollRegion s_ScrollRegion;
3254 CScrollRegionParams ScrollParams;
3255 ScrollParams.m_ScrollbarThickness = 10.0f;
3256 ScrollParams.m_ScrollbarMargin = 3.0f;
3257 ScrollParams.m_ScrollUnit = RowHeight * 5;
3258 s_ScrollRegion.Begin(pClipRect: &ToolBox, pParams: &ScrollParams);
3259
3260 bool ScrollToSelection = false;
3261 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Map()->m_vpSounds.empty())
3262 {
3263 if(Input()->KeyPress(Key: KEY_DOWN))
3264 {
3265 Map()->SelectNextSound();
3266 ScrollToSelection = true;
3267 }
3268 else if(Input()->KeyPress(Key: KEY_UP))
3269 {
3270 Map()->SelectPreviousSound();
3271 ScrollToSelection = true;
3272 }
3273 }
3274
3275 CUIRect Slot;
3276 ToolBox.HSplitTop(Cut: RowHeight + 3.0f, pTop: &Slot, pBottom: &ToolBox);
3277 if(s_ScrollRegion.AddRect(Rect: Slot))
3278 Ui()->DoLabel(pRect: &Slot, pText: "Embedded", Size: 12.0f, Align: TEXTALIGN_MC);
3279
3280 for(int i = 0; i < (int)Map()->m_vpSounds.size(); i++)
3281 {
3282 ToolBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &ToolBox);
3283 int Selected = Map()->m_SelectedSound == i;
3284 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: Selected && ScrollToSelection))
3285 continue;
3286 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
3287
3288 const bool SoundUsed = std::any_of(first: Map()->m_vpGroups.cbegin(), last: Map()->m_vpGroups.cend(), pred: [i](const auto &pGroup) {
3289 return std::any_of(pGroup->m_vpLayers.cbegin(), pGroup->m_vpLayers.cend(), [i](const auto &pLayer) {
3290 if(pLayer->m_Type == LAYERTYPE_SOUNDS)
3291 return std::static_pointer_cast<CLayerSounds>(pLayer)->m_Sound == i;
3292 return false;
3293 });
3294 });
3295
3296 if(!SoundUsed)
3297 Selected += 2; // Sound is unused
3298
3299 if(int Result = DoButton_Ex(pId: &Map()->m_vpSounds[i], pText: Map()->m_vpSounds[i]->m_aName, Checked: Selected, pRect: &Slot,
3300 Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Select sound.", Corners: IGraphics::CORNER_ALL))
3301 {
3302 Map()->m_SelectedSound = i;
3303
3304 if(Result == 2)
3305 {
3306 static SPopupMenuId s_PopupSoundId;
3307 Ui()->DoPopupMenu(pId: &s_PopupSoundId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 140, Height: 90, pContext: this, pfnFunc: PopupSound);
3308 }
3309 }
3310 }
3311
3312 // separator
3313 ToolBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &ToolBox);
3314 if(s_ScrollRegion.AddRect(Rect: Slot))
3315 {
3316 IGraphics::CLineItem LineItem(Slot.x, Slot.y + Slot.h / 2, Slot.x + Slot.w, Slot.y + Slot.h / 2);
3317 Graphics()->TextureClear();
3318 Graphics()->LinesBegin();
3319 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
3320 Graphics()->LinesEnd();
3321 }
3322
3323 // new sound
3324 static int s_AddSoundButton = 0;
3325 CUIRect AddSoundButton;
3326 ToolBox.HSplitTop(Cut: 5.0f + RowHeight + 1.0f, pTop: &AddSoundButton, pBottom: &ToolBox);
3327 if(s_ScrollRegion.AddRect(Rect: AddSoundButton))
3328 {
3329 AddSoundButton.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &AddSoundButton);
3330 AddSoundButton.HSplitTop(Cut: RowHeight, pTop: &AddSoundButton, pBottom: nullptr);
3331 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."))
3332 m_FileBrowser.ShowFileDialog(StorageType: IStorage::TYPE_ALL, FileType: CFileBrowser::EFileType::SOUND, pTitle: "Add sound", pButtonText: "Add", pInitialPath: "mapres", pInitialFilename: "", pfnOpenCallback: AddSound, pOpenCallbackUser: this);
3333 }
3334 s_ScrollRegion.End();
3335}
3336
3337bool CEditor::CStringKeyComparator::operator()(const char *pLhs, const char *pRhs) const
3338{
3339 return str_comp(a: pLhs, b: pRhs) < 0;
3340}
3341
3342void CEditor::ShowFileDialogError(const char *pFormat, ...)
3343{
3344 char aMessage[1024];
3345 va_list VarArgs;
3346 va_start(VarArgs, pFormat);
3347 str_format_v(buffer: aMessage, buffer_size: sizeof(aMessage), format: pFormat, args: VarArgs);
3348 va_end(VarArgs);
3349
3350 auto ContextIterator = m_PopupMessageContexts.find(x: aMessage);
3351 CUi::SMessagePopupContext *pContext;
3352 if(ContextIterator != m_PopupMessageContexts.end())
3353 {
3354 pContext = ContextIterator->second;
3355 Ui()->ClosePopupMenu(pId: pContext);
3356 }
3357 else
3358 {
3359 pContext = new CUi::SMessagePopupContext();
3360 pContext->ErrorColor();
3361 str_copy(dst&: pContext->m_aMessage, src: aMessage);
3362 m_PopupMessageContexts[pContext->m_aMessage] = pContext;
3363 }
3364 Ui()->ShowPopupMessage(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext);
3365}
3366
3367void CEditor::RenderModebar(CUIRect View)
3368{
3369 CUIRect Mentions, IngameMoved, ModeButtons, ModeButton;
3370 View.HSplitTop(Cut: 12.0f, pTop: &Mentions, pBottom: &View);
3371 View.HSplitTop(Cut: 12.0f, pTop: &IngameMoved, pBottom: &View);
3372 View.HSplitBottom(Cut: 22.0f, pTop: nullptr, pBottom: &ModeButtons);
3373 const float Width = m_ToolBoxWidth - 5.0f;
3374 ModeButtons.VSplitLeft(Cut: Width, pLeft: &ModeButtons, pRight: nullptr);
3375 const float ButtonWidth = Width / 3;
3376
3377 // mentions
3378 if(m_Mentions)
3379 {
3380 char aBuf[64];
3381 if(m_Mentions == 1)
3382 str_copy(dst&: aBuf, src: Localize(pStr: "1 new mention"));
3383 else if(m_Mentions <= 9)
3384 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d new mentions"), m_Mentions);
3385 else
3386 str_copy(dst&: aBuf, src: Localize(pStr: "9+ new mentions"));
3387
3388 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
3389 Ui()->DoLabel(pRect: &Mentions, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MC);
3390 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
3391 }
3392
3393 // ingame moved warning
3394 if(m_IngameMoved)
3395 {
3396 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
3397 Ui()->DoLabel(pRect: &IngameMoved, pText: Localize(pStr: "Moved ingame"), Size: 10.0f, Align: TEXTALIGN_MC);
3398 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
3399 }
3400
3401 // mode buttons
3402 {
3403 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
3404 static int s_LayersButton = 0;
3405 if(DoButton_FontIcon(pId: &s_LayersButton, pText: FontIcon::LAYER_GROUP, Checked: m_Mode == MODE_LAYERS, pRect: &ModeButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Go to layers management.", Corners: IGraphics::CORNER_L))
3406 {
3407 m_Mode = MODE_LAYERS;
3408 }
3409
3410 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
3411 static int s_ImagesButton = 0;
3412 if(DoButton_FontIcon(pId: &s_ImagesButton, pText: FontIcon::IMAGE, Checked: m_Mode == MODE_IMAGES, pRect: &ModeButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Go to images management.", Corners: IGraphics::CORNER_NONE))
3413 {
3414 m_Mode = MODE_IMAGES;
3415 }
3416
3417 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
3418 static int s_SoundsButton = 0;
3419 if(DoButton_FontIcon(pId: &s_SoundsButton, pText: FontIcon::MUSIC, Checked: m_Mode == MODE_SOUNDS, pRect: &ModeButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Go to sounds management.", Corners: IGraphics::CORNER_R))
3420 {
3421 m_Mode = MODE_SOUNDS;
3422 }
3423
3424 if(Input()->KeyPress(Key: KEY_LEFT) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
3425 {
3426 m_Mode = (m_Mode + NUM_MODES - 1) % NUM_MODES;
3427 }
3428 else if(Input()->KeyPress(Key: KEY_RIGHT) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
3429 {
3430 m_Mode = (m_Mode + 1) % NUM_MODES;
3431 }
3432 }
3433}
3434
3435void CEditor::RenderStatusbar(CUIRect View, CUIRect *pTooltipRect)
3436{
3437 CUIRect Button;
3438 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
3439 if(DoButton_Editor(pId: &m_QuickActionEnvelopes, pText: m_QuickActionEnvelopes.Label(), Checked: m_QuickActionEnvelopes.Color(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionEnvelopes.Description()))
3440 {
3441 m_QuickActionEnvelopes.Call();
3442 }
3443
3444 View.VSplitRight(Cut: 10.0f, pLeft: &View, pRight: nullptr);
3445 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
3446 if(DoButton_Editor(pId: &m_QuickActionServerSettings, pText: m_QuickActionServerSettings.Label(), Checked: m_QuickActionServerSettings.Color(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionServerSettings.Description()))
3447 {
3448 m_QuickActionServerSettings.Call();
3449 }
3450
3451 View.VSplitRight(Cut: 10.0f, pLeft: &View, pRight: nullptr);
3452 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
3453 if(DoButton_Editor(pId: &m_QuickActionHistory, pText: m_QuickActionHistory.Label(), Checked: m_QuickActionHistory.Color(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionHistory.Description()))
3454 {
3455 m_QuickActionHistory.Call();
3456 }
3457
3458 View.VSplitRight(Cut: 10.0f, pLeft: pTooltipRect, pRight: nullptr);
3459}
3460
3461void CEditor::RenderTooltip(CUIRect TooltipRect)
3462{
3463 if(str_comp(a: m_aTooltip, b: "") == 0)
3464 return;
3465
3466 char aBuf[256];
3467 if(m_pUiGotContext && m_pUiGotContext == Ui()->HotItem())
3468 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s Right click for context menu.", m_aTooltip);
3469 else
3470 str_copy(dst&: aBuf, src: m_aTooltip);
3471
3472 SLabelProperties Props;
3473 Props.m_MaxWidth = TooltipRect.w;
3474 Props.m_EllipsisAtEnd = true;
3475 Ui()->DoLabel(pRect: &TooltipRect, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
3476}
3477
3478void CEditor::DoEditorDragBar(CUIRect View, CUIRect *pDragBar, EDragSide Side, float *pValue, float MinValue, float MaxValue)
3479{
3480 enum EDragOperation
3481 {
3482 OP_NONE,
3483 OP_DRAGGING,
3484 OP_CLICKED
3485 };
3486 static EDragOperation s_Operation = OP_NONE;
3487 static float s_InitialMouseY = 0.0f;
3488 static float s_InitialMouseOffsetY = 0.0f;
3489 static float s_InitialMouseX = 0.0f;
3490 static float s_InitialMouseOffsetX = 0.0f;
3491
3492 bool IsVertical = Side == EDragSide::TOP || Side == EDragSide::BOTTOM;
3493
3494 if(Ui()->MouseInside(pRect: pDragBar) && Ui()->HotItem() == pDragBar)
3495 m_CursorType = IsVertical ? CURSOR_RESIZE_V : CURSOR_RESIZE_H;
3496
3497 bool Clicked;
3498 bool Abrupted;
3499 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."))
3500 {
3501 if(s_Operation == OP_NONE && Result == 1)
3502 {
3503 s_InitialMouseY = Ui()->MouseY();
3504 s_InitialMouseOffsetY = Ui()->MouseY() - pDragBar->y;
3505 s_InitialMouseX = Ui()->MouseX();
3506 s_InitialMouseOffsetX = Ui()->MouseX() - pDragBar->x;
3507 s_Operation = OP_CLICKED;
3508 }
3509
3510 if(Clicked || Abrupted)
3511 s_Operation = OP_NONE;
3512
3513 if(s_Operation == OP_CLICKED && absolute(a: IsVertical ? Ui()->MouseY() - s_InitialMouseY : Ui()->MouseX() - s_InitialMouseX) > 5.0f)
3514 s_Operation = OP_DRAGGING;
3515
3516 if(s_Operation == OP_DRAGGING)
3517 {
3518 if(Side == EDragSide::TOP)
3519 *pValue = std::clamp(val: s_InitialMouseOffsetY + View.y + View.h - Ui()->MouseY(), lo: MinValue, hi: MaxValue);
3520 else if(Side == EDragSide::RIGHT)
3521 *pValue = std::clamp(val: Ui()->MouseX() - s_InitialMouseOffsetX - View.x + pDragBar->w, lo: MinValue, hi: MaxValue);
3522 else if(Side == EDragSide::BOTTOM)
3523 *pValue = std::clamp(val: Ui()->MouseY() - s_InitialMouseOffsetY - View.y + pDragBar->h, lo: MinValue, hi: MaxValue);
3524 else if(Side == EDragSide::LEFT)
3525 *pValue = std::clamp(val: s_InitialMouseOffsetX + View.x + View.w - Ui()->MouseX(), lo: MinValue, hi: MaxValue);
3526
3527 m_CursorType = IsVertical ? CURSOR_RESIZE_V : CURSOR_RESIZE_H;
3528 }
3529 }
3530}
3531
3532void CEditor::RenderMenubar(CUIRect MenuBar)
3533{
3534 SPopupMenuProperties PopupProperties;
3535 PopupProperties.m_Corners = IGraphics::CORNER_R | IGraphics::CORNER_B;
3536
3537 CUIRect FileButton;
3538 static int s_FileButton = 0;
3539 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &FileButton, pRight: &MenuBar);
3540 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))
3541 {
3542 static SPopupMenuId s_PopupMenuFileId;
3543 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);
3544 }
3545
3546 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
3547
3548 CUIRect ToolsButton;
3549 static int s_ToolsButton = 0;
3550 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &ToolsButton, pRight: &MenuBar);
3551 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))
3552 {
3553 static SPopupMenuId s_PopupMenuToolsId;
3554 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);
3555 }
3556
3557 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
3558
3559 CUIRect SettingsButton;
3560 static int s_SettingsButton = 0;
3561 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &SettingsButton, pRight: &MenuBar);
3562 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))
3563 {
3564 static SPopupMenuId s_PopupMenuSettingsId;
3565 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);
3566 }
3567
3568 CUIRect ChangedIndicator, Info, Help, Close;
3569 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
3570 MenuBar.VSplitLeft(Cut: MenuBar.h, pLeft: &ChangedIndicator, pRight: &MenuBar);
3571 MenuBar.VSplitRight(Cut: 15.0f, pLeft: &MenuBar, pRight: &Close);
3572 MenuBar.VSplitRight(Cut: 5.0f, pLeft: &MenuBar, pRight: nullptr);
3573 MenuBar.VSplitRight(Cut: 15.0f, pLeft: &MenuBar, pRight: &Help);
3574 MenuBar.VSplitRight(Cut: 5.0f, pLeft: &MenuBar, pRight: nullptr);
3575 MenuBar.VSplitLeft(Cut: MenuBar.w * 0.6f, pLeft: &MenuBar, pRight: &Info);
3576 MenuBar.VSplitRight(Cut: 5.0f, pLeft: &MenuBar, pRight: nullptr);
3577
3578 if(Map()->m_Modified)
3579 {
3580 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
3581 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);
3582 Ui()->DoLabel(pRect: &ChangedIndicator, pText: FontIcon::CIRCLE, Size: 8.0f, Align: TEXTALIGN_MC);
3583 TextRender()->SetRenderFlags(0);
3584 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
3585 static int s_ChangedIndicator;
3586 DoButtonLogic(pId: &s_ChangedIndicator, Checked: 0, pRect: &ChangedIndicator, Flags: BUTTONFLAG_NONE, pToolTip: "This map has unsaved changes."); // just for the tooltip, result unused
3587 }
3588
3589 char aBuf[IO_MAX_PATH_LENGTH + 32];
3590 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "File: %s", Map()->m_aFilename);
3591 SLabelProperties Props;
3592 Props.m_MaxWidth = MenuBar.w;
3593 Props.m_EllipsisAtEnd = true;
3594 Ui()->DoLabel(pRect: &MenuBar, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
3595
3596 char aTimeStr[6];
3597 str_timestamp_format(buffer: aTimeStr, buffer_size: sizeof(aTimeStr), format: "%H:%M");
3598
3599 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "X: %.1f, Y: %.1f, Z: %.1f, T: %.1f, A: %.1f, G: %i %s", MapView()->MouseWorldPos().x / 32.0f, MapView()->MouseWorldPos().y / 32.0f, MapView()->Zoom()->GetValue(), Map()->m_EnvelopeEvaluator.m_AnimateTime * Map()->m_EnvelopeEvaluator.m_AnimateSpeed, Map()->m_EnvelopeEvaluator.m_AnimateSpeed, MapView()->MapGrid()->Factor(), aTimeStr);
3600 Ui()->DoLabel(pRect: &Info, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MR);
3601
3602 static int s_HelpButton = 0;
3603 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."))
3604 {
3605 m_QuickActionShowHelp.Call();
3606 }
3607
3608 static int s_CloseButton = 0;
3609 if(DoButton_Editor(pId: &s_CloseButton, pText: "×", Checked: 0, pRect: &Close, Flags: BUTTONFLAG_LEFT, pToolTip: "[Escape] Exit from the editor."))
3610 {
3611 OnClose();
3612 g_Config.m_ClEditor = 0;
3613 }
3614}
3615
3616void CEditor::ShowHelp()
3617{
3618 const char *pLink = Localize(pStr: "https://wiki.ddnet.org/wiki/Mapping");
3619 if(!Client()->ViewLink(pLink))
3620 {
3621 ShowFileDialogError(pFormat: "Failed to open the link '%s' in the default web browser.", pLink);
3622 }
3623}
3624
3625void CEditor::Render()
3626{
3627 // basic start
3628 Graphics()->Clear(r: 0.0f, g: 0.0f, b: 0.0f);
3629 CUIRect View = *Ui()->Screen();
3630 Ui()->MapScreen();
3631 m_CursorType = CURSOR_NORMAL;
3632
3633 float Width = View.w;
3634 float Height = View.h;
3635
3636 // reset tip
3637 str_copy(dst&: m_aTooltip, src: "");
3638
3639 // render checker
3640 RenderBackground(View, Texture: m_CheckerTexture, Size: 32.0f, Brightness: 1.0f);
3641
3642 UpdateBrushPicker();
3643
3644 CUIRect MenuBar, ModeBar, ToolBar, StatusBar, ExtraEditor, ToolBox;
3645 if(m_GuiActive)
3646 {
3647 View.HSplitTop(Cut: 20.0f, pTop: &MenuBar, pBottom: &View);
3648 View.HSplitTop(Cut: 53.0f, pTop: &ToolBar, pBottom: &View);
3649 View.VSplitLeft(Cut: m_ToolBoxWidth, pLeft: &ToolBox, pRight: &View);
3650
3651 View.HSplitBottom(Cut: 16.0f, pTop: &View, pBottom: &StatusBar);
3652 if(!m_ShowPicker && m_ActiveExtraEditor != EXTRAEDITOR_NONE)
3653 View.HSplitBottom(Cut: m_aExtraEditorSplits[(int)m_ActiveExtraEditor], pTop: &View, pBottom: &ExtraEditor);
3654 }
3655 else
3656 {
3657 // hack to get keyboard inputs from toolbar even when GUI is not active
3658 ToolBar.x = -100;
3659 ToolBar.y = -100;
3660 ToolBar.w = 50;
3661 ToolBar.h = 50;
3662 }
3663
3664 // a little hack for now
3665 if(m_Mode == MODE_LAYERS)
3666 MapView()->Render(View);
3667
3668 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
3669 {
3670 // handle undo/redo hotkeys
3671 if(Ui()->CheckActiveItem(pId: nullptr))
3672 {
3673 if(Input()->KeyPress(Key: KEY_Z) && Input()->ModifierIsPressed() && !Input()->ShiftIsPressed())
3674 ActiveHistory().Undo();
3675 if((Input()->KeyPress(Key: KEY_Y) && Input()->ModifierIsPressed()) || (Input()->KeyPress(Key: KEY_Z) && Input()->ModifierIsPressed() && Input()->ShiftIsPressed()))
3676 ActiveHistory().Redo();
3677 }
3678
3679 // handle brush save/load hotkeys
3680 for(int i = KEY_1; i <= KEY_0; i++)
3681 {
3682 if(Input()->KeyPress(Key: i))
3683 {
3684 int Slot = i - KEY_1;
3685 if(Input()->ModifierIsPressed() && !m_pBrush->IsEmpty())
3686 {
3687 dbg_msg(sys: "editor", fmt: "saving current brush to %d", Slot);
3688 m_apSavedBrushes[Slot] = std::make_shared<CLayerGroup>(args&: *m_pBrush);
3689 }
3690 else if(m_apSavedBrushes[Slot])
3691 {
3692 dbg_msg(sys: "editor", fmt: "loading brush from slot %d", Slot);
3693 m_pBrush = std::make_shared<CLayerGroup>(args&: *m_apSavedBrushes[Slot]);
3694 }
3695 }
3696 }
3697 }
3698
3699 const float BackgroundBrightness = 0.26f;
3700 const float BackgroundScale = 80.0f;
3701
3702 if(m_GuiActive)
3703 {
3704 RenderBackground(View: MenuBar, Texture: IGraphics::CTextureHandle(), Size: BackgroundScale, Brightness: 0.0f);
3705 MenuBar.Margin(Cut: 2.0f, pOtherRect: &MenuBar);
3706
3707 RenderBackground(View: ToolBox, Texture: g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, Size: BackgroundScale, Brightness: BackgroundBrightness);
3708 ToolBox.Margin(Cut: 2.0f, pOtherRect: &ToolBox);
3709
3710 RenderBackground(View: ToolBar, Texture: g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, Size: BackgroundScale, Brightness: BackgroundBrightness);
3711 ToolBar.Margin(Cut: 2.0f, pOtherRect: &ToolBar);
3712 ToolBar.VSplitLeft(Cut: m_ToolBoxWidth, pLeft: &ModeBar, pRight: &ToolBar);
3713
3714 RenderBackground(View: StatusBar, Texture: g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, Size: BackgroundScale, Brightness: BackgroundBrightness);
3715 StatusBar.Margin(Cut: 2.0f, pOtherRect: &StatusBar);
3716 }
3717
3718 // do the toolbar
3719 if(m_Mode == MODE_LAYERS)
3720 DoToolbarLayers(ToolBar);
3721 else if(m_Mode == MODE_IMAGES)
3722 DoToolbarImages(ToolBar);
3723 else if(m_Mode == MODE_SOUNDS)
3724 DoToolbarSounds(ToolBar);
3725
3726 if(m_Dialog == DIALOG_NONE)
3727 {
3728 const bool ModPressed = Input()->ModifierIsPressed();
3729 const bool ShiftPressed = Input()->ShiftIsPressed();
3730 const bool AltPressed = Input()->AltIsPressed();
3731
3732 if(CLineInput::GetActiveInput() == nullptr)
3733 {
3734 // ctrl+a to append map
3735 if(Input()->KeyPress(Key: KEY_A) && ModPressed)
3736 {
3737 m_FileBrowser.ShowFileDialog(StorageType: IStorage::TYPE_ALL, FileType: CFileBrowser::EFileType::MAP, pTitle: "Append map", pButtonText: "Append", pInitialPath: "maps", pInitialFilename: "", pfnOpenCallback: CallbackAppendMap, pOpenCallbackUser: this);
3738 }
3739 }
3740
3741 // ctrl+n to create new map
3742 if(Input()->KeyPress(Key: KEY_N) && ModPressed)
3743 {
3744 if(HasUnsavedData())
3745 {
3746 if(!m_PopupEventWasActivated)
3747 {
3748 m_PopupEventType = POPEVENT_NEW;
3749 m_PopupEventActivated = true;
3750 }
3751 }
3752 else
3753 {
3754 Reset();
3755 }
3756 }
3757 // ctrl+o or ctrl+l to open
3758 if((Input()->KeyPress(Key: KEY_O) || Input()->KeyPress(Key: KEY_L)) && ModPressed)
3759 {
3760 if(ShiftPressed)
3761 {
3762 if(!m_QuickActionLoadCurrentMap.Disabled())
3763 {
3764 m_QuickActionLoadCurrentMap.Call();
3765 }
3766 }
3767 else
3768 {
3769 if(HasUnsavedData())
3770 {
3771 if(!m_PopupEventWasActivated)
3772 {
3773 m_PopupEventType = POPEVENT_LOAD;
3774 m_PopupEventActivated = true;
3775 }
3776 }
3777 else
3778 {
3779 m_FileBrowser.ShowFileDialog(StorageType: IStorage::TYPE_ALL, FileType: CFileBrowser::EFileType::MAP, pTitle: "Load map", pButtonText: "Load", pInitialPath: "maps", pInitialFilename: "", pfnOpenCallback: CallbackOpenMap, pOpenCallbackUser: this);
3780 }
3781 }
3782 }
3783
3784 // ctrl+shift+alt+s to save copy
3785 if(Input()->KeyPress(Key: KEY_S) && ModPressed && ShiftPressed && AltPressed)
3786 {
3787 char aDefaultName[IO_MAX_PATH_LENGTH];
3788 fs_split_file_extension(filename: fs_filename(path: Map()->m_aFilename), name: aDefaultName, name_size: sizeof(aDefaultName));
3789 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);
3790 }
3791 // ctrl+shift+s to save as
3792 else if(Input()->KeyPress(Key: KEY_S) && ModPressed && ShiftPressed)
3793 {
3794 m_QuickActionSaveAs.Call();
3795 }
3796 // ctrl+s to save
3797 else if(Input()->KeyPress(Key: KEY_S) && ModPressed)
3798 {
3799 if(Map()->m_aFilename[0] != '\0' && Map()->m_ValidSaveFilename)
3800 {
3801 CallbackSaveMap(pFilename: Map()->m_aFilename, StorageType: IStorage::TYPE_SAVE, pUser: this);
3802 }
3803 else
3804 {
3805 m_FileBrowser.ShowFileDialog(StorageType: IStorage::TYPE_SAVE, FileType: CFileBrowser::EFileType::MAP, pTitle: "Save map", pButtonText: "Save", pInitialPath: "maps", pInitialFilename: "", pfnOpenCallback: CallbackSaveMap, pOpenCallbackUser: this);
3806 }
3807 }
3808 }
3809
3810 if(m_GuiActive)
3811 {
3812 CUIRect DragBar;
3813 ToolBox.VSplitRight(Cut: 1.0f, pLeft: &ToolBox, pRight: &DragBar);
3814 DragBar.x -= 2.0f;
3815 DragBar.w += 4.0f;
3816 DoEditorDragBar(View: ToolBox, pDragBar: &DragBar, Side: EDragSide::RIGHT, pValue: &m_ToolBoxWidth);
3817
3818 if(m_Mode == MODE_LAYERS)
3819 RenderLayers(LayersBox: ToolBox);
3820 else if(m_Mode == MODE_IMAGES)
3821 {
3822 RenderImagesList(ToolBox);
3823 RenderSelectedImage(View);
3824 }
3825 else if(m_Mode == MODE_SOUNDS)
3826 RenderSounds(ToolBox);
3827 }
3828
3829 Ui()->MapScreen();
3830
3831 CUIRect TooltipRect;
3832 if(m_GuiActive)
3833 {
3834 RenderMenubar(MenuBar);
3835 RenderModebar(View: ModeBar);
3836 if(!m_ShowPicker)
3837 {
3838 if(m_ActiveExtraEditor != EXTRAEDITOR_NONE)
3839 {
3840 RenderBackground(View: ExtraEditor, Texture: g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, Size: BackgroundScale, Brightness: BackgroundBrightness);
3841 ExtraEditor.HMargin(Cut: 2.0f, pOtherRect: &ExtraEditor);
3842 ExtraEditor.VSplitRight(Cut: 2.0f, pLeft: &ExtraEditor, pRight: nullptr);
3843 }
3844
3845 static bool s_ShowServerSettingsEditorLast = false;
3846 if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES)
3847 {
3848 RenderEnvelopeEditor(View: ExtraEditor);
3849 }
3850 else if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS)
3851 {
3852 RenderServerSettingsEditor(View: ExtraEditor, ShowServerSettingsEditorLast: s_ShowServerSettingsEditorLast);
3853 }
3854 else if(m_ActiveExtraEditor == EXTRAEDITOR_HISTORY)
3855 {
3856 RenderEditorHistory(View: ExtraEditor);
3857 }
3858 s_ShowServerSettingsEditorLast = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS;
3859 }
3860 RenderStatusbar(View: StatusBar, pTooltipRect: &TooltipRect);
3861 }
3862
3863 RenderPressedKeys(View);
3864 RenderSavingIndicator(View);
3865
3866 if(m_Dialog == DIALOG_MAPSETTINGS_ERROR)
3867 {
3868 static int s_NullUiTarget = 0;
3869 Ui()->SetHotItem(&s_NullUiTarget);
3870 RenderMapSettingsErrorDialog();
3871 }
3872
3873 if(m_PopupEventActivated)
3874 {
3875 static SPopupMenuId s_PopupEventId;
3876 constexpr float PopupWidth = 400.0f;
3877 constexpr float PopupHeight = 150.0f;
3878 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);
3879 m_PopupEventActivated = false;
3880 m_PopupEventWasActivated = true;
3881 }
3882
3883 if(m_Dialog == DIALOG_NONE && !Ui()->IsPopupHovered() && Ui()->MouseInside(pRect: &View))
3884 {
3885 // handle zoom hotkeys
3886 if(CLineInput::GetActiveInput() == nullptr)
3887 {
3888 // one keypress should be equivalent to zooming
3889 // three times using mousewheel: 1.1^3 = 1.331
3890 if(Input()->KeyPress(Key: KEY_KP_MINUS))
3891 MapView()->Zoom()->ScaleValue(Factor: 1.331f);
3892 if(Input()->KeyPress(Key: KEY_KP_PLUS))
3893 MapView()->Zoom()->ScaleValue(Factor: 1.0f / 1.331f);
3894 if(Input()->KeyPress(Key: KEY_KP_MULTIPLY))
3895 MapView()->ResetZoom();
3896 }
3897
3898 if(m_pBrush->IsEmpty() || !Input()->ShiftIsPressed())
3899 {
3900 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
3901 MapView()->Zoom()->ScaleValue(Factor: 1.1f);
3902 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
3903 MapView()->Zoom()->ScaleValue(Factor: 1.0f / 1.1f);
3904 }
3905 if(!m_pBrush->IsEmpty())
3906 {
3907 bool HasTileAdjustLayer = false;
3908 bool HasSpeedupLayer = false;
3909 for(const auto &pLayer : m_pBrush->m_vpLayers)
3910 {
3911 if(pLayer->m_Type != LAYERTYPE_TILES)
3912 {
3913 continue;
3914 }
3915 std::shared_ptr<CLayerTiles> pTiles = std::static_pointer_cast<CLayerTiles>(r: pLayer);
3916 HasTileAdjustLayer |= pTiles->m_HasTele || pTiles->m_HasSwitch || pTiles->m_HasTune;
3917 HasSpeedupLayer |= pTiles->m_HasSpeedup;
3918 }
3919 if(HasTileAdjustLayer)
3920 {
3921 str_copy(dst&: m_aTooltip, src: "Use Shift+Mouse wheel up/down to adjust the tile numbers. Use Ctrl+F to change all tile numbers to the first unused number.");
3922 }
3923 else if(HasSpeedupLayer)
3924 {
3925 str_copy(dst&: m_aTooltip, src: "Use Shift+Mouse wheel up/down to adjust the angle.");
3926 }
3927
3928 if(Input()->ShiftIsPressed())
3929 {
3930 const int AdjustModifiers = Input()->ModifierIsPressed() ? (Input()->AltIsPressed() ? 2 : 1) : 0;
3931 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
3932 AdjustBrushSpecialTiles(UseNextFree: false, AdjustModifiers, AdjustValue: -1);
3933 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
3934 AdjustBrushSpecialTiles(UseNextFree: false, AdjustModifiers, AdjustValue: 1);
3935 }
3936
3937 // Use ctrl+f to replace number in brush with next free
3938 if(Input()->ModifierIsPressed() && Input()->KeyPress(Key: KEY_F))
3939 AdjustBrushSpecialTiles(UseNextFree: true, AdjustModifiers: 0, AdjustValue: 0);
3940 }
3941 }
3942
3943 m_FileBrowser.Render();
3944 m_Prompt.Render();
3945 m_FontTyper.Render();
3946
3947 MapView()->UpdateZoom();
3948
3949 // Cancel color pipette with escape before closing popup menus with escape
3950 if(m_ColorPipetteActive && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
3951 {
3952 m_ColorPipetteActive = false;
3953 }
3954
3955 Ui()->RenderPopupMenus();
3956 FreeDynamicPopupMenus();
3957
3958 UpdateColorPipette();
3959
3960 if(m_Dialog == DIALOG_NONE && !m_PopupEventActivated && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
3961 {
3962 OnClose();
3963 g_Config.m_ClEditor = 0;
3964 }
3965
3966 // The tooltip can be set in popup menus so we have to render the tooltip after the popup menus.
3967 if(m_GuiActive)
3968 RenderTooltip(TooltipRect);
3969
3970 Ui()->RenderBackButton();
3971 RenderMousePointer();
3972}
3973
3974void CEditor::UpdateBrushPicker()
3975{
3976 if(!m_QuickActionBrushPicker.Disabled() &&
3977 m_Dialog == DIALOG_NONE &&
3978 CLineInput::GetActiveInput() == nullptr)
3979 {
3980 if(Input()->ModifierIsPressed())
3981 {
3982 if(Input()->KeyPress(Key: KEY_SPACE))
3983 {
3984 m_ShowPickerToggle = !m_ShowPickerToggle;
3985 }
3986 m_ShowPicker = m_ShowPickerToggle;
3987 }
3988 else
3989 {
3990 const bool SpacePressed = Input()->KeyIsPressed(Key: KEY_SPACE);
3991 m_ShowPicker = m_ShowPickerToggle || SpacePressed;
3992 if(SpacePressed)
3993 {
3994 m_ShowPickerToggle = false;
3995 }
3996 }
3997 }
3998 else
3999 {
4000 m_ShowPicker = false;
4001 m_ShowPickerToggle = false;
4002 }
4003}
4004
4005void CEditor::RenderPressedKeys(CUIRect View)
4006{
4007 if(!g_Config.m_EdShowkeys)
4008 return;
4009
4010 Ui()->MapScreen();
4011 CTextCursor Cursor;
4012 Cursor.SetPosition(vec2(View.x + 10, View.y + View.h - 24 - 10));
4013 Cursor.m_FontSize = 24.0f;
4014
4015 int NKeys = 0;
4016 for(int i = 0; i < KEY_LAST; i++)
4017 {
4018 if(Input()->KeyIsPressed(Key: i))
4019 {
4020 if(NKeys)
4021 TextRender()->TextEx(pCursor: &Cursor, pText: " + ", Length: -1);
4022 TextRender()->TextEx(pCursor: &Cursor, pText: Input()->KeyName(Key: i), Length: -1);
4023 NKeys++;
4024 }
4025 }
4026}
4027
4028void CEditor::RenderSavingIndicator(CUIRect View)
4029{
4030 if(m_WriterFinishJobs.empty())
4031 return;
4032
4033 const char *pText = "Saving…";
4034 const float FontSize = 24.0f;
4035
4036 Ui()->MapScreen();
4037 CUIRect Label, Spinner;
4038 View.Margin(Cut: 20.0f, pOtherRect: &View);
4039 View.HSplitBottom(Cut: FontSize, pTop: nullptr, pBottom: &View);
4040 View.VSplitRight(Cut: TextRender()->TextWidth(Size: FontSize, pText) + 2.0f, pLeft: &Spinner, pRight: &Label);
4041 Spinner.VSplitRight(Cut: Spinner.h, pLeft: nullptr, pRight: &Spinner);
4042 Ui()->DoLabel(pRect: &Label, pText, Size: FontSize, Align: TEXTALIGN_MR);
4043 Ui()->RenderProgressSpinner(Center: Spinner.Center(), OuterRadius: 8.0f);
4044}
4045
4046void CEditor::FreeDynamicPopupMenus()
4047{
4048 auto Iterator = m_PopupMessageContexts.begin();
4049 while(Iterator != m_PopupMessageContexts.end())
4050 {
4051 if(!Ui()->IsPopupOpen(pId: Iterator->second))
4052 {
4053 CUi::SMessagePopupContext *pContext = Iterator->second;
4054 Iterator = m_PopupMessageContexts.erase(position: Iterator);
4055 delete pContext;
4056 }
4057 else
4058 ++Iterator;
4059 }
4060}
4061
4062void CEditor::UpdateColorPipette()
4063{
4064 if(!m_ColorPipetteActive)
4065 return;
4066
4067 static char s_PipetteScreenButton;
4068 if(Ui()->HotItem() == &s_PipetteScreenButton)
4069 {
4070 // Read color one pixel to the top and left as we would otherwise not read the correct
4071 // color due to the cursor sprite being rendered over the current mouse position.
4072 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);
4073 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);
4074 Graphics()->ReadPixel(Position: ivec2(PixelX, PixelY), pColor: &m_PipetteColor);
4075 }
4076
4077 // Simulate button overlaying the entire screen to intercept all clicks for color pipette.
4078 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.");
4079 // Don't handle clicks if we are panning, so the pipette stays active while panning.
4080 // Checking m_pContainerPanned alone is not enough, as this variable is reset when
4081 // panning ends before this function is called.
4082 if(m_pContainerPanned == nullptr && m_pContainerPannedLast == nullptr)
4083 {
4084 if(ButtonResult == 1)
4085 {
4086 char aClipboard[9];
4087 str_format(buffer: aClipboard, buffer_size: sizeof(aClipboard), format: "%08X", m_PipetteColor.PackAlphaLast());
4088 Input()->SetClipboardText(aClipboard);
4089
4090 // Check if any of the saved colors is equal to the picked color and
4091 // bring it to the front of the list instead of adding a duplicate.
4092 int ShiftEnd = (int)std::size(m_aSavedColors) - 1;
4093 for(int i = 0; i < (int)std::size(m_aSavedColors); ++i)
4094 {
4095 if(m_aSavedColors[i].Pack() == m_PipetteColor.Pack())
4096 {
4097 ShiftEnd = i;
4098 break;
4099 }
4100 }
4101 for(int i = ShiftEnd; i > 0; --i)
4102 {
4103 m_aSavedColors[i] = m_aSavedColors[i - 1];
4104 }
4105 m_aSavedColors[0] = m_PipetteColor;
4106 }
4107 if(ButtonResult > 0)
4108 {
4109 m_ColorPipetteActive = false;
4110 }
4111 }
4112}
4113
4114void CEditor::RenderMousePointer()
4115{
4116 if(!m_ShowMousePointer)
4117 return;
4118
4119 if(m_MouseAxisLockState == EAxisLock::HORIZONTAL)
4120 {
4121 m_CursorType = CURSOR_RESIZE_H;
4122 }
4123 else if(m_MouseAxisLockState == EAxisLock::VERTICAL)
4124 {
4125 m_CursorType = CURSOR_RESIZE_V;
4126 }
4127
4128 constexpr float CursorSize = 16.0f;
4129
4130 // Cursor
4131 Graphics()->WrapClamp();
4132 Graphics()->TextureSet(Texture: m_aCursorTextures[m_CursorType]);
4133 Graphics()->QuadsBegin();
4134 if(m_CursorType == CURSOR_RESIZE_V)
4135 {
4136 Graphics()->QuadsSetRotation(Angle: pi / 2.0f);
4137 }
4138 if(m_pUiGotContext == Ui()->HotItem())
4139 {
4140 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 1.0f);
4141 }
4142 const float CursorOffset = m_CursorType == CURSOR_RESIZE_V || m_CursorType == CURSOR_RESIZE_H ? -CursorSize / 2.0f : 0.0f;
4143 IGraphics::CQuadItem QuadItem(Ui()->MouseX() + CursorOffset, Ui()->MouseY() + CursorOffset, CursorSize, CursorSize);
4144 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
4145 Graphics()->QuadsEnd();
4146 Graphics()->WrapNormal();
4147
4148 // Pipette color
4149 if(m_ColorPipetteActive)
4150 {
4151 CUIRect PipetteRect = {.x: Ui()->MouseX() + CursorSize, .y: Ui()->MouseY() + CursorSize, .w: 80.0f, .h: 20.0f};
4152 if(PipetteRect.x + PipetteRect.w + 2.0f > Ui()->Screen()->w)
4153 {
4154 PipetteRect.x = Ui()->MouseX() - PipetteRect.w - CursorSize / 2.0f;
4155 }
4156 if(PipetteRect.y + PipetteRect.h + 2.0f > Ui()->Screen()->h)
4157 {
4158 PipetteRect.y = Ui()->MouseY() - PipetteRect.h - CursorSize / 2.0f;
4159 }
4160 PipetteRect.Draw(Color: ColorRGBA(0.2f, 0.2f, 0.2f, 0.7f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
4161
4162 CUIRect Pipette, Label;
4163 PipetteRect.VSplitLeft(Cut: PipetteRect.h, pLeft: &Pipette, pRight: &Label);
4164 Pipette.Margin(Cut: 2.0f, pOtherRect: &Pipette);
4165 Pipette.Draw(Color: m_PipetteColor, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
4166
4167 char aLabel[8];
4168 str_format(buffer: aLabel, buffer_size: sizeof(aLabel), format: "#%06X", m_PipetteColor.PackAlphaLast(Alpha: false));
4169 Ui()->DoLabel(pRect: &Label, pText: aLabel, Size: 10.0f, Align: TEXTALIGN_MC);
4170 }
4171}
4172
4173void CEditor::RenderIngameEntities(const CLayerGroup &Group, const CLayerTiles &TilesLayer)
4174{
4175 const CGameClient *pGameClient = (CGameClient *)Kernel()->RequestInterface<IGameClient>();
4176 const float TileSize = 32.f;
4177
4178 const bool DDNetOrCustomEntities = std::find_if(first: std::begin(arr: gs_apModEntitiesNames), last: std::end(arr: gs_apModEntitiesNames),
4179 pred: [&](const char *pEntitiesName) { return str_comp_nocase(a: m_SelectEntitiesImage.c_str(), b: pEntitiesName) == 0 &&
4180 str_comp_nocase(a: pEntitiesName, b: "ddnet") != 0; }) == std::end(arr: gs_apModEntitiesNames);
4181
4182 const bool IsSwitch = TilesLayer.m_HasSwitch;
4183 std::function<std::tuple<unsigned char, unsigned char>(int, int)> GetTile;
4184 std::function<unsigned char(int, int)> GetIndexChecked;
4185 if(IsSwitch)
4186 {
4187 const CLayerSwitch &SwitchLayer = static_cast<const CLayerSwitch &>(TilesLayer);
4188 GetTile = [&](int x, int y) -> std::tuple<unsigned char, unsigned char> {
4189 const CSwitchTile Tile = SwitchLayer.m_pSwitchTile[y * SwitchLayer.m_Width + x];
4190 return {Tile.m_Type - ENTITY_OFFSET, Tile.m_Flags};
4191 };
4192 GetIndexChecked = [&](int x, int y) -> unsigned char {
4193 if(x < 0 || y < 0 || x >= SwitchLayer.m_Width || y >= SwitchLayer.m_Height)
4194 {
4195 return 0;
4196 }
4197 return SwitchLayer.m_pSwitchTile[y * SwitchLayer.m_Width + x].m_Type - ENTITY_OFFSET;
4198 };
4199 }
4200 else
4201 {
4202 GetTile = [&](int x, int y) -> std::tuple<unsigned char, unsigned char> {
4203 const CTile Tile = TilesLayer.m_pTiles[y * TilesLayer.m_Width + x];
4204 return {Tile.m_Index - ENTITY_OFFSET, Tile.m_Flags};
4205 };
4206 GetIndexChecked = [&](int x, int y) -> unsigned char {
4207 if(x < 0 || y < 0 || x >= TilesLayer.m_Width || y >= TilesLayer.m_Height)
4208 {
4209 return 0;
4210 }
4211 return TilesLayer.m_pTiles[y * TilesLayer.m_Width + x].m_Index - ENTITY_OFFSET;
4212 };
4213 }
4214
4215 static const ivec2 DOOR_OFFSETS[] = {{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
4216 const ColorRGBA DoorOuterColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClLaserDoorOutlineColor));
4217 const ColorRGBA DoorInnerColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClLaserDoorInnerColor));
4218
4219 float aPoints[4];
4220 Group.Mapping(pPoints: aPoints);
4221 const int ExtraBorder = 9; // doors extend beyond the tile on which they are placed
4222 const int StartX = std::max(a: 0, b: (int)std::floor(x: aPoints[0] / TileSize) - ExtraBorder);
4223 const int EndX = std::min(a: TilesLayer.m_Width, b: (int)std::ceil(x: aPoints[2] / TileSize) + ExtraBorder);
4224 const int StartY = std::max(a: 0, b: (int)std::floor(x: aPoints[1] / TileSize) - ExtraBorder);
4225 const int EndY = std::min(a: TilesLayer.m_Height, b: (int)std::ceil(x: aPoints[3] / TileSize) + ExtraBorder);
4226 for(int y = StartY; y < EndY; y++)
4227 {
4228 for(int x = StartX; x < EndX; x++)
4229 {
4230 const auto [Index, Flags] = GetTile(x, y);
4231
4232 if(Index == ENTITY_DOOR)
4233 {
4234 for(const ivec2 Offset : DOOR_OFFSETS)
4235 {
4236 const unsigned char IndexDoorLength = GetIndexChecked(x + Offset.x, y + Offset.y);
4237 if(IndexDoorLength >= ENTITY_LASER_SHORT && IndexDoorLength <= ENTITY_LASER_LONG)
4238 {
4239 const int Length = (IndexDoorLength - ENTITY_LASER_SHORT + 1) * 3;
4240 const vec2 Pos = vec2(x + 0.5f, y + 0.5f);
4241 const vec2 To = Pos + normalize(v: vec2(Offset.x, Offset.y)) * Length;
4242 pGameClient->m_Items.RenderLaser(From: To * TileSize, Pos: Pos * TileSize, OuterColor: DoorOuterColor, InnerColor: DoorInnerColor, TicksBody: 0.0f, TicksHead: 0.0f, Type: LASERTYPE_DOOR);
4243 }
4244 }
4245 }
4246 else if((!IsSwitch && Index >= ENTITY_FLAGSTAND_RED && Index <= ENTITY_FLAGSTAND_BLUE) ||
4247 (Index >= ENTITY_ARMOR_1 && Index <= ENTITY_WEAPON_LASER) ||
4248 (DDNetOrCustomEntities && Index >= ENTITY_ARMOR_SHOTGUN && Index <= ENTITY_ARMOR_LASER))
4249 {
4250 vec2 Pos = vec2(x, y) * TileSize;
4251 vec2 Scale;
4252 int VisualSize;
4253
4254 if(Index == ENTITY_FLAGSTAND_RED)
4255 {
4256 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpriteFlagRed);
4257 Scale = vec2(42, 84);
4258 VisualSize = 1;
4259 Pos.y -= (Scale.y / 2.f) * 0.75f;
4260 }
4261 else if(Index == ENTITY_FLAGSTAND_BLUE)
4262 {
4263 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpriteFlagBlue);
4264 Scale = vec2(42, 84);
4265 VisualSize = 1;
4266 Pos.y -= (Scale.y / 2.f) * 0.75f;
4267 }
4268 else if(Index == ENTITY_ARMOR_1)
4269 {
4270 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmor);
4271 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_HEALTH, ScaleX&: Scale.x, ScaleY&: Scale.y);
4272 VisualSize = 64;
4273 }
4274 else if(Index == ENTITY_HEALTH_1)
4275 {
4276 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupHealth);
4277 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_HEALTH, ScaleX&: Scale.x, ScaleY&: Scale.y);
4278 VisualSize = 64;
4279 }
4280 else if(Index == ENTITY_WEAPON_SHOTGUN)
4281 {
4282 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_aSpritePickupWeapons[WEAPON_SHOTGUN]);
4283 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_SHOTGUN, ScaleX&: Scale.x, ScaleY&: Scale.y);
4284 VisualSize = g_pData->m_Weapons.m_aId[WEAPON_SHOTGUN].m_VisualSize;
4285 }
4286 else if(Index == ENTITY_WEAPON_GRENADE)
4287 {
4288 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_aSpritePickupWeapons[WEAPON_GRENADE]);
4289 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_GRENADE, ScaleX&: Scale.x, ScaleY&: Scale.y);
4290 VisualSize = g_pData->m_Weapons.m_aId[WEAPON_GRENADE].m_VisualSize;
4291 }
4292 else if(Index == ENTITY_POWERUP_NINJA)
4293 {
4294 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_aSpritePickupWeapons[WEAPON_NINJA]);
4295 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_NINJA, ScaleX&: Scale.x, ScaleY&: Scale.y);
4296 VisualSize = 128;
4297 }
4298 else if(Index == ENTITY_WEAPON_LASER)
4299 {
4300 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_aSpritePickupWeapons[WEAPON_LASER]);
4301 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_LASER, ScaleX&: Scale.x, ScaleY&: Scale.y);
4302 VisualSize = g_pData->m_Weapons.m_aId[WEAPON_LASER].m_VisualSize;
4303 }
4304 else if(Index == ENTITY_ARMOR_SHOTGUN)
4305 {
4306 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmorShotgun);
4307 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_SHOTGUN, ScaleX&: Scale.x, ScaleY&: Scale.y);
4308 VisualSize = 64;
4309 }
4310 else if(Index == ENTITY_ARMOR_GRENADE)
4311 {
4312 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmorGrenade);
4313 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_GRENADE, ScaleX&: Scale.x, ScaleY&: Scale.y);
4314 VisualSize = 64;
4315 }
4316 else if(Index == ENTITY_ARMOR_NINJA)
4317 {
4318 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmorNinja);
4319 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_NINJA, ScaleX&: Scale.x, ScaleY&: Scale.y);
4320 VisualSize = 64;
4321 }
4322 else if(Index == ENTITY_ARMOR_LASER)
4323 {
4324 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmorLaser);
4325 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_LASER, ScaleX&: Scale.x, ScaleY&: Scale.y);
4326 VisualSize = 64;
4327 }
4328 else
4329 {
4330 dbg_assert_failed("Unhandled ingame entities index: %d", Index);
4331 }
4332
4333 Graphics()->QuadsBegin();
4334
4335 if(Index != ENTITY_FLAGSTAND_RED &&
4336 Index != ENTITY_FLAGSTAND_BLUE)
4337 {
4338 if(Flags & TILEFLAG_XFLIP)
4339 {
4340 Scale.x = -Scale.x;
4341 }
4342
4343 if(Flags & TILEFLAG_YFLIP)
4344 {
4345 Scale.y = -Scale.y;
4346 }
4347
4348 if(Flags & TILEFLAG_ROTATE)
4349 {
4350 Graphics()->QuadsSetRotation(Angle: 90.f * (pi / 180));
4351
4352 if(Index == ENTITY_POWERUP_NINJA)
4353 {
4354 Pos.y += (Flags & TILEFLAG_XFLIP) ? 10.0f : -10.0f;
4355 }
4356 }
4357 else
4358 {
4359 if(Index == ENTITY_POWERUP_NINJA)
4360 {
4361 Pos.x += (Flags & TILEFLAG_XFLIP) ? 10.0f : -10.0f;
4362 }
4363 }
4364 }
4365
4366 Scale *= VisualSize;
4367 Pos -= (Scale - vec2(TileSize, TileSize)) / 2.0f;
4368 Pos += direction(angle: Client()->GlobalTime() * 2.0f + x + y) * 2.5f;
4369
4370 IGraphics::CQuadItem Quad(Pos.x, Pos.y, Scale.x, Scale.y);
4371 Graphics()->QuadsDrawTL(pArray: &Quad, Num: 1);
4372 Graphics()->QuadsEnd();
4373 }
4374 }
4375 }
4376}
4377
4378void CEditor::Reset(bool CreateDefault)
4379{
4380 Ui()->ClosePopupMenus();
4381 Map()->Clean();
4382
4383 for(CEditorComponent &Component : m_vComponents)
4384 Component.OnReset();
4385
4386 m_ToolbarPreviewSound = -1;
4387
4388 // create default layers
4389 if(CreateDefault)
4390 {
4391 m_EditorWasUsedBefore = true;
4392 Map()->CreateDefault();
4393 }
4394
4395 m_pContainerPanned = nullptr;
4396 m_pContainerPannedLast = nullptr;
4397
4398 m_ActiveEnvelopePreview = EEnvelopePreview::NONE;
4399 m_QuadEnvelopePointOperation = EQuadEnvelopePointOperation::NONE;
4400
4401 m_ResetZoomEnvelope = true;
4402 m_SettingsCommandInput.Clear();
4403 m_MapSettingsCommandContext.Reset();
4404 m_RenderLayersState.Reset();
4405}
4406
4407IGraphics::CTextureHandle CEditor::GetFrontTexture()
4408{
4409 if(!m_FrontTexture.IsValid())
4410 m_FrontTexture = Graphics()->LoadTexture(pFilename: "editor/front.png", StorageType: IStorage::TYPE_ALL, Flags: Graphics()->TextureLoadFlags());
4411 return m_FrontTexture;
4412}
4413
4414IGraphics::CTextureHandle CEditor::GetTeleTexture()
4415{
4416 if(!m_TeleTexture.IsValid())
4417 m_TeleTexture = Graphics()->LoadTexture(pFilename: "editor/tele.png", StorageType: IStorage::TYPE_ALL, Flags: Graphics()->TextureLoadFlags());
4418 return m_TeleTexture;
4419}
4420
4421IGraphics::CTextureHandle CEditor::GetSpeedupTexture()
4422{
4423 if(!m_SpeedupTexture.IsValid())
4424 m_SpeedupTexture = Graphics()->LoadTexture(pFilename: "editor/speedup.png", StorageType: IStorage::TYPE_ALL, Flags: Graphics()->TextureLoadFlags());
4425 return m_SpeedupTexture;
4426}
4427
4428IGraphics::CTextureHandle CEditor::GetSwitchTexture()
4429{
4430 if(!m_SwitchTexture.IsValid())
4431 m_SwitchTexture = Graphics()->LoadTexture(pFilename: "editor/switch.png", StorageType: IStorage::TYPE_ALL, Flags: Graphics()->TextureLoadFlags());
4432 return m_SwitchTexture;
4433}
4434
4435IGraphics::CTextureHandle CEditor::GetTuneTexture()
4436{
4437 if(!m_TuneTexture.IsValid())
4438 m_TuneTexture = Graphics()->LoadTexture(pFilename: "editor/tune.png", StorageType: IStorage::TYPE_ALL, Flags: Graphics()->TextureLoadFlags());
4439 return m_TuneTexture;
4440}
4441
4442IGraphics::CTextureHandle CEditor::GetEntitiesTexture()
4443{
4444 if(!m_EntitiesTexture.IsValid())
4445 m_EntitiesTexture = Graphics()->LoadTexture(pFilename: "editor/entities/DDNet.png", StorageType: IStorage::TYPE_ALL, Flags: Graphics()->TextureLoadFlags());
4446 return m_EntitiesTexture;
4447}
4448
4449void CEditor::Init()
4450{
4451 m_pInput = Kernel()->RequestInterface<IInput>();
4452 m_pClient = Kernel()->RequestInterface<IClient>();
4453 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
4454 m_pConfig = m_pConfigManager->Values();
4455 m_pEngine = Kernel()->RequestInterface<IEngine>();
4456 m_pGraphics = Kernel()->RequestInterface<IGraphics>();
4457 m_pTextRender = Kernel()->RequestInterface<ITextRender>();
4458 m_pStorage = Kernel()->RequestInterface<IStorage>();
4459 m_pSound = Kernel()->RequestInterface<ISound>();
4460 m_UI.Init(pKernel: Kernel());
4461 m_UI.SetPopupMenuClosedCallback([this]() {
4462 m_PopupEventWasActivated = false;
4463 });
4464 m_UI.SetDispatchInputCallback([this](const IInput::CEvent &Event) {
4465 OnInput(Event);
4466 });
4467 m_RenderMap.Init(pGraphics: m_pGraphics, pTextRender: m_pTextRender);
4468 m_ZoomEnvelopeX.OnInit(pEditor: this);
4469 m_ZoomEnvelopeY.OnInit(pEditor: this);
4470
4471 m_vComponents.emplace_back(args&: m_MapView);
4472 m_vComponents.emplace_back(args&: m_MapSettingsBackend);
4473 m_vComponents.emplace_back(args&: m_LayerSelector);
4474 m_vComponents.emplace_back(args&: m_FileBrowser);
4475 m_vComponents.emplace_back(args&: m_Prompt);
4476 m_vComponents.emplace_back(args&: m_FontTyper);
4477 m_vComponents.emplace_back(args&: m_QuadKnife);
4478 for(CEditorComponent &Component : m_vComponents)
4479 Component.OnInit(pEditor: this);
4480
4481 m_CheckerTexture = Graphics()->LoadTexture(pFilename: "editor/checker.png", StorageType: IStorage::TYPE_ALL);
4482 m_aCursorTextures[CURSOR_NORMAL] = Graphics()->LoadTexture(pFilename: "editor/cursor.png", StorageType: IStorage::TYPE_ALL);
4483 m_aCursorTextures[CURSOR_RESIZE_H] = Graphics()->LoadTexture(pFilename: "editor/cursor_resize.png", StorageType: IStorage::TYPE_ALL);
4484 m_aCursorTextures[CURSOR_RESIZE_V] = m_aCursorTextures[CURSOR_RESIZE_H];
4485
4486 m_pTilesetPicker = std::make_shared<CLayerTiles>(args: Map(), args: 16, args: 16);
4487 m_pTilesetPicker->MakePalette();
4488 m_pTilesetPicker->m_Readonly = true;
4489
4490 m_pQuadsetPicker = std::make_shared<CLayerQuads>(args: Map());
4491 m_pQuadsetPicker->NewQuad(x: 0, y: 0, Width: 64, Height: 64);
4492 m_pQuadsetPicker->m_Readonly = true;
4493
4494 m_pBrush = std::make_shared<CLayerGroup>(args: Map());
4495
4496 Reset(CreateDefault: false);
4497}
4498
4499void CEditor::MouseAxisLock(vec2 &CursorRel)
4500{
4501 if(Input()->AltIsPressed())
4502 {
4503 // only lock with the paint brush and inside editor map area to avoid duplicate Alt behavior
4504 if(m_pBrush->IsEmpty() || Ui()->HotItem() != MapView())
4505 return;
4506
4507 const vec2 CurrentWorldPos = MapView()->MouseWorldPos() / 32.0f;
4508
4509 if(m_MouseAxisLockState == EAxisLock::START)
4510 {
4511 m_MouseAxisInitialPos = CurrentWorldPos;
4512 m_MouseAxisLockState = EAxisLock::NONE;
4513 return; // delta would be 0, calculate it in next frame
4514 }
4515
4516 const vec2 Delta = CurrentWorldPos - m_MouseAxisInitialPos;
4517
4518 // lock to axis if moved mouse by 1 block
4519 if(m_MouseAxisLockState == EAxisLock::NONE && (std::abs(x: Delta.x) > 1.0f || std::abs(x: Delta.y) > 1.0f))
4520 {
4521 m_MouseAxisLockState = (std::abs(x: Delta.x) > std::abs(x: Delta.y)) ? EAxisLock::HORIZONTAL : EAxisLock::VERTICAL;
4522 }
4523
4524 if(m_MouseAxisLockState == EAxisLock::HORIZONTAL)
4525 {
4526 CursorRel.y = 0;
4527 }
4528 else if(m_MouseAxisLockState == EAxisLock::VERTICAL)
4529 {
4530 CursorRel.x = 0;
4531 }
4532 }
4533 else
4534 {
4535 m_MouseAxisLockState = EAxisLock::START;
4536 }
4537}
4538
4539void CEditor::HandleAutosave()
4540{
4541 const float Time = Client()->GlobalTime();
4542 const float LastAutosaveUpdateTime = m_LastAutosaveUpdateTime;
4543 m_LastAutosaveUpdateTime = Time;
4544
4545 if(g_Config.m_EdAutosaveInterval == 0)
4546 return; // autosave disabled
4547 if(!Map()->m_ModifiedAuto || Map()->m_LastModifiedTime < 0.0f)
4548 return; // no unsaved changes
4549
4550 // Add time to autosave timer if the editor was disabled for more than 10 seconds,
4551 // to prevent autosave from immediately activating when the editor is activated
4552 // after being deactivated for some time.
4553 if(LastAutosaveUpdateTime >= 0.0f && Time - LastAutosaveUpdateTime > 10.0f)
4554 {
4555 Map()->m_LastSaveTime += Time - LastAutosaveUpdateTime;
4556 }
4557
4558 // Check if autosave timer has expired.
4559 if(Map()->m_LastSaveTime >= Time || Time - Map()->m_LastSaveTime < 60 * g_Config.m_EdAutosaveInterval)
4560 return;
4561
4562 // Wait for 5 seconds of no modification before saving, to prevent autosave
4563 // from immediately activating when a map is first modified or while user is
4564 // modifying the map, but don't delay the autosave for more than 1 minute.
4565 if(Time - Map()->m_LastModifiedTime < 5.0f && Time - Map()->m_LastSaveTime < 60 * (g_Config.m_EdAutosaveInterval + 1))
4566 return;
4567
4568 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
4569 ShowFileDialogError(pFormat: "%s", pErrorMessage);
4570 log_error("editor/autosave", "%s", pErrorMessage);
4571 };
4572 Map()->PerformAutosave(ErrorHandler);
4573}
4574
4575void CEditor::HandleWriterFinishJobs()
4576{
4577 if(m_WriterFinishJobs.empty())
4578 return;
4579
4580 std::shared_ptr<CDataFileWriterFinishJob> pJob = m_WriterFinishJobs.front();
4581 if(!pJob->Done())
4582 return;
4583 m_WriterFinishJobs.pop_front();
4584
4585 const char *pErrorMessage = pJob->ErrorMessage();
4586 if(pErrorMessage[0] != '\0')
4587 {
4588 ShowFileDialogError(pFormat: "%s", pErrorMessage);
4589 return;
4590 }
4591
4592 // send rcon.. if we can
4593 if(Client()->RconAuthed() && g_Config.m_EdAutoMapReload)
4594 {
4595 CServerInfo CurrentServerInfo;
4596 Client()->GetServerInfo(pServerInfo: &CurrentServerInfo);
4597
4598 if(net_addr_is_local(addr: &Client()->ServerAddress()))
4599 {
4600 char aMapName[MAX_MAP_LENGTH];
4601 fs_split_file_extension(filename: fs_filename(path: pJob->RealFilename()), name: aMapName, name_size: sizeof(aMapName));
4602 if(!str_comp(a: aMapName, b: CurrentServerInfo.m_aMap))
4603 Client()->Rcon(pLine: "hot_reload");
4604 }
4605 }
4606}
4607
4608void CEditor::OnUpdate()
4609{
4610 CUIElementBase::Init(pUI: Ui()); // update static pointer because game and editor use separate UI
4611
4612 if(!m_EditorWasUsedBefore)
4613 {
4614 m_EditorWasUsedBefore = true;
4615 Reset();
4616 }
4617
4618 m_pContainerPannedLast = m_pContainerPanned;
4619
4620 // handle mouse movement
4621 vec2 CursorRel = vec2(0.0f, 0.0f);
4622 IInput::ECursorType CursorType = Input()->CursorRelative(pX: &CursorRel.x, pY: &CursorRel.y);
4623 if(CursorType != IInput::CURSOR_NONE)
4624 {
4625 Ui()->ConvertMouseMove(pX: &CursorRel.x, pY: &CursorRel.y, CursorType);
4626 MouseAxisLock(CursorRel);
4627 Ui()->OnCursorMove(X: CursorRel.x, Y: CursorRel.y);
4628 }
4629
4630 // handle key presses
4631 Input()->ConsumeEvents(Consumer: [&](const IInput::CEvent &Event) {
4632 OnInput(Event);
4633 });
4634
4635 MapView()->UpdateMouseWorld();
4636 LayerSelector()->UpdateHoveredTiles();
4637 HandleAutosave();
4638 HandleWriterFinishJobs();
4639
4640 for(CEditorComponent &Component : m_vComponents)
4641 Component.OnUpdate();
4642}
4643
4644void CEditor::OnInput(const IInput::CEvent &Event)
4645{
4646 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Event.m_Key == KEY_F1)
4647 {
4648 if((Event.m_Flags & IInput::FLAG_PRESS) != 0 && (Event.m_Flags & IInput::FLAG_REPEAT) == 0)
4649 {
4650 m_QuickActionShowHelp.Call();
4651 }
4652 return;
4653 }
4654
4655 for(CEditorComponent &Component : m_vComponents)
4656 {
4657 // Events with flag `FLAG_RELEASE` must always be forwarded to all components so keys being
4658 // released can be handled in all components also after some components have been disabled.
4659 if(Component.OnInput(Event) && (Event.m_Flags & ~IInput::FLAG_RELEASE) != 0)
4660 return;
4661 }
4662 Ui()->OnInput(Event);
4663}
4664
4665void CEditor::OnRender()
4666{
4667 Ui()->SetMouseSlow(false);
4668
4669 // toggle gui
4670 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_TAB))
4671 m_GuiActive = !m_GuiActive;
4672
4673 if(Input()->KeyPress(Key: KEY_F10))
4674 m_ShowMousePointer = false;
4675
4676 if(Map()->m_EnvelopeEvaluator.m_Animate)
4677 Map()->m_EnvelopeEvaluator.m_AnimateTime = Client()->GlobalTime() - Map()->m_EnvelopeEvaluator.m_AnimateStart;
4678
4679 m_pUiGotContext = nullptr;
4680 Ui()->StartCheck();
4681
4682 Ui()->Update();
4683
4684 Ui()->DoBackButton();
4685
4686 Render();
4687
4688 MapView()->ResetMouseDeltaWorld();
4689
4690 if(Input()->KeyPress(Key: KEY_F10))
4691 {
4692 Graphics()->TakeScreenshot(pFilename: nullptr);
4693 m_ShowMousePointer = true;
4694 }
4695
4696 if(g_Config.m_Debug)
4697 Ui()->DebugRender(X: 2.0f, Y: Ui()->Screen()->h - 27.0f);
4698
4699 Ui()->FinishCheck();
4700 Ui()->ClearHotkeys();
4701 Input()->Clear();
4702
4703 CLineInput::RenderCandidates();
4704
4705#if defined(CONF_DEBUG)
4706 Map()->CheckIntegrity();
4707#endif
4708}
4709
4710void CEditor::OnActivate()
4711{
4712 ResetMentions();
4713 ResetIngameMoved();
4714}
4715
4716void CEditor::OnWindowResize()
4717{
4718 Ui()->OnWindowResize();
4719}
4720
4721void CEditor::OnClose()
4722{
4723 m_ColorPipetteActive = false;
4724
4725 if(m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
4726 Sound()->Pause(SampleId: m_ToolbarPreviewSound);
4727
4728 m_FileBrowser.OnEditorClose();
4729}
4730
4731void CEditor::OnDialogClose()
4732{
4733 m_Dialog = DIALOG_NONE;
4734 m_FileBrowser.OnDialogClose();
4735}
4736
4737void CEditor::LoadCurrentMap()
4738{
4739 CGameClient *pGameClient = (CGameClient *)Kernel()->RequestInterface<IGameClient>();
4740
4741 if(Load(pFilename: pGameClient->Map()->Path(), StorageType: IStorage::TYPE_SAVE))
4742 {
4743 Map()->m_ValidSaveFilename = !str_startswith(str: pGameClient->Map()->Path(), prefix: "downloadedmaps/");
4744 }
4745 else
4746 {
4747 Load(pFilename: pGameClient->Map()->Path(), StorageType: IStorage::TYPE_ALL);
4748 Map()->m_ValidSaveFilename = false;
4749 }
4750
4751 vec2 Center = pGameClient->m_Camera.m_Center;
4752 MapView()->SetWorldOffset(Center);
4753}
4754
4755bool CEditor::Save(const char *pFilename)
4756{
4757 // Check if file with this name is already being saved at the moment
4758 if(std::any_of(first: std::begin(cont&: m_WriterFinishJobs), last: std::end(cont&: m_WriterFinishJobs), pred: [pFilename](const std::shared_ptr<CDataFileWriterFinishJob> &Job) {
4759 return str_comp(a: pFilename, b: Job->RealFilename()) == 0;
4760 }))
4761 {
4762 return false;
4763 }
4764
4765 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
4766 ShowFileDialogError(pFormat: "%s", pErrorMessage);
4767 log_error("editor/save", "%s", pErrorMessage);
4768 };
4769 return Map()->Save(pFilename, ErrorHandler);
4770}
4771
4772bool CEditor::HandleMapDrop(const char *pFilename, int StorageType)
4773{
4774 OnDialogClose();
4775 if(HasUnsavedData())
4776 {
4777 str_copy(dst&: m_aFilenamePendingLoad, src: pFilename);
4778 m_PopupEventType = CEditor::POPEVENT_LOADDROP;
4779 m_PopupEventActivated = true;
4780 return true;
4781 }
4782 else
4783 {
4784 return Load(pFilename, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE);
4785 }
4786}
4787
4788bool CEditor::Load(const char *pFilename, int StorageType)
4789{
4790 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
4791 ShowFileDialogError(pFormat: "%s", pErrorMessage);
4792 log_error("editor/load", "%s", pErrorMessage);
4793 };
4794
4795 Reset();
4796 bool Result = Map()->Load(pFilename, StorageType, ErrorHandler: std::move(ErrorHandler));
4797 if(Result)
4798 {
4799 for(CEditorComponent &Component : m_vComponents)
4800 Component.OnMapLoad();
4801
4802 log_info("editor/load", "Loaded map '%s'", Map()->m_aFilename);
4803 }
4804 return Result;
4805}
4806
4807CEditorHistory &CEditor::ActiveHistory()
4808{
4809 if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS)
4810 {
4811 return Map()->m_ServerSettingsHistory;
4812 }
4813 else if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES)
4814 {
4815 return Map()->m_EnvelopeEditorHistory;
4816 }
4817 else
4818 {
4819 return Map()->m_EditorHistory;
4820 }
4821}
4822
4823void CEditor::AdjustBrushSpecialTiles(bool UseNextFree, int AdjustModifiers, int AdjustValue)
4824{
4825 // Adjust m_Angle of speedup or m_Number field of tune, switch and tele tiles by `Adjust` if `UseNextFree` is false
4826 // If `AdjustValue` is 0 and `UseNextFree` is false, then update numbers of brush tiles to global values
4827 // If true, then use the next free number instead
4828
4829 dbg_assert(AdjustValue == -1 || AdjustValue == 0 || AdjustValue == 1, "Invalid AdjustValue: %d", AdjustValue);
4830 auto &&AdjustNumber = [AdjustValue](auto &Number, int Min, int Max) {
4831 const int NumberInt = Number + AdjustValue; // Cast to int so this does not overflow unsigned char for some tiles
4832 if(NumberInt < Min)
4833 {
4834 Number = Max;
4835 }
4836 else if(NumberInt > Max)
4837 {
4838 Number = Min;
4839 }
4840 else
4841 {
4842 Number = NumberInt;
4843 }
4844 };
4845
4846 for(auto &pLayer : m_pBrush->m_vpLayers)
4847 {
4848 if(pLayer->m_Type != LAYERTYPE_TILES)
4849 continue;
4850
4851 std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(r: pLayer);
4852
4853 if(pLayerTiles->m_HasTele && (!UseNextFree || Map()->m_pTeleLayer != nullptr))
4854 {
4855 const int NextFreeTeleNumber = UseNextFree ? Map()->m_pTeleLayer->FindNextFreeNumber(Checkpoint: false) : 0;
4856 const int NextFreeCheckpointNumber = UseNextFree ? Map()->m_pTeleLayer->FindNextFreeNumber(Checkpoint: true) : 0;
4857 std::shared_ptr<CLayerTele> pTeleLayer = std::static_pointer_cast<CLayerTele>(r: pLayer);
4858 for(int y = 0; y < pTeleLayer->m_Height; y++)
4859 {
4860 for(int x = 0; x < pTeleLayer->m_Width; x++)
4861 {
4862 int i = y * pTeleLayer->m_Width + x;
4863 if(!IsValidTeleTile(Index: pTeleLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pTeleLayer->m_pTeleTile[i].m_Number))
4864 continue;
4865
4866 if(UseNextFree)
4867 {
4868 if(IsTeleTileCheckpoint(Index: pTeleLayer->m_pTiles[i].m_Index))
4869 pTeleLayer->m_pTeleTile[i].m_Number = NextFreeCheckpointNumber;
4870 else if(IsTeleTileNumberUsedAny(Index: pTeleLayer->m_pTiles[i].m_Index))
4871 pTeleLayer->m_pTeleTile[i].m_Number = NextFreeTeleNumber;
4872 }
4873 else if(AdjustValue == 0)
4874 {
4875 if(IsTeleTileCheckpoint(Index: pTeleLayer->m_pTiles[i].m_Index))
4876 pTeleLayer->m_pTeleTile[i].m_Number = m_TeleCheckpointNumber;
4877 else if(IsTeleTileNumberUsedAny(Index: pTeleLayer->m_pTiles[i].m_Index))
4878 pTeleLayer->m_pTeleTile[i].m_Number = m_TeleNumber;
4879 }
4880 else if(AdjustModifiers == 0)
4881 {
4882 AdjustNumber(pTeleLayer->m_pTeleTile[i].m_Number, 1, 255);
4883 }
4884 }
4885 }
4886 }
4887 else if(pLayerTiles->m_HasTune && (!UseNextFree || Map()->m_pTuneLayer != nullptr))
4888 {
4889 const int NextFreeNumber = UseNextFree ? Map()->m_pTuneLayer->FindNextFreeNumber() : 0;
4890 std::shared_ptr<CLayerTune> pTuneLayer = std::static_pointer_cast<CLayerTune>(r: pLayer);
4891 for(int y = 0; y < pTuneLayer->m_Height; y++)
4892 {
4893 for(int x = 0; x < pTuneLayer->m_Width; x++)
4894 {
4895 int i = y * pTuneLayer->m_Width + x;
4896 if(!IsValidTuneTile(Index: pTuneLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pTuneLayer->m_pTuneTile[i].m_Number))
4897 continue;
4898
4899 if(UseNextFree)
4900 {
4901 pTuneLayer->m_pTuneTile[i].m_Number = NextFreeNumber;
4902 }
4903 else if(AdjustModifiers == 0)
4904 {
4905 AdjustNumber(pTuneLayer->m_pTuneTile[i].m_Number, 1, 255);
4906 }
4907 }
4908 }
4909 }
4910 else if(pLayerTiles->m_HasSwitch && (!UseNextFree || Map()->m_pSwitchLayer != nullptr))
4911 {
4912 const int NextFreeNumber = UseNextFree ? Map()->m_pSwitchLayer->FindNextFreeNumber() : 0;
4913 std::shared_ptr<CLayerSwitch> pSwitchLayer = std::static_pointer_cast<CLayerSwitch>(r: pLayer);
4914 for(int y = 0; y < pSwitchLayer->m_Height; y++)
4915 {
4916 for(int x = 0; x < pSwitchLayer->m_Width; x++)
4917 {
4918 int i = y * pSwitchLayer->m_Width + x;
4919 if(!IsValidSwitchTile(Index: pSwitchLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pSwitchLayer->m_pSwitchTile[i].m_Number))
4920 continue;
4921
4922 if(UseNextFree)
4923 {
4924 pSwitchLayer->m_pSwitchTile[i].m_Number = NextFreeNumber;
4925 }
4926 else if(AdjustModifiers == 0)
4927 {
4928 AdjustNumber(pSwitchLayer->m_pSwitchTile[i].m_Number, 1, 255);
4929 }
4930 else if(AdjustModifiers == 1)
4931 {
4932 AdjustNumber(pSwitchLayer->m_pSwitchTile[i].m_Delay, 0, 255);
4933 }
4934 }
4935 }
4936 }
4937 else if(pLayerTiles->m_HasSpeedup && !UseNextFree)
4938 {
4939 std::shared_ptr<CLayerSpeedup> pSpeedupLayer = std::static_pointer_cast<CLayerSpeedup>(r: pLayer);
4940 for(int y = 0; y < pSpeedupLayer->m_Height; y++)
4941 {
4942 for(int x = 0; x < pSpeedupLayer->m_Width; x++)
4943 {
4944 int i = y * pSpeedupLayer->m_Width + x;
4945 if(!IsValidSpeedupTile(Index: pSpeedupLayer->m_pTiles[i].m_Index))
4946 continue;
4947
4948 if(AdjustValue == 0)
4949 {
4950 pSpeedupLayer->m_pSpeedupTile[i].m_Angle = m_SpeedupAngle;
4951 pSpeedupLayer->m_SpeedupAngle = m_SpeedupAngle;
4952 }
4953 else if(AdjustModifiers == 0)
4954 {
4955 AdjustNumber(pSpeedupLayer->m_pSpeedupTile[i].m_Angle, 0, 359);
4956 }
4957 else if(AdjustModifiers == 1)
4958 {
4959 AdjustNumber(pSpeedupLayer->m_pSpeedupTile[i].m_Force, 1, 255);
4960 }
4961 else if(AdjustModifiers == 2)
4962 {
4963 AdjustNumber(pSpeedupLayer->m_pSpeedupTile[i].m_MaxSpeed, 0, 255);
4964 }
4965 }
4966 }
4967 }
4968 }
4969}
4970
4971IEditor *CreateEditor() { return new CEditor; }
4972