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