From 2d6a9c9cc16030bbef3f4d63452976fa4383e26b Mon Sep 17 00:00:00 2001 From: Clayton Groeneveld Date: Mon, 1 Nov 2021 02:16:56 -0500 Subject: [PATCH] UI: Show spacing helpers in preview This shows distance between sides of preview and edges of sources. This will allow users to more easily align sources. Co-authored-by: Palakis --- UI/data/locale/en-US.ini | 1 + UI/forms/OBSBasicSettings.ui | 7 + UI/obs-app.cpp | 2 + UI/window-basic-main.cpp | 10 ++ UI/window-basic-main.hpp | 3 + UI/window-basic-preview.cpp | 296 +++++++++++++++++++++++++++++++++++ UI/window-basic-preview.hpp | 7 + UI/window-basic-settings.cpp | 13 ++ 8 files changed, 339 insertions(+) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 0cb27d27a..64ff69aa9 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -821,6 +821,7 @@ Basic.Settings.General.ScreenSnapping="Snap Sources to edge of screen" Basic.Settings.General.CenterSnapping="Snap Sources to horizontal and vertical center" Basic.Settings.General.SourceSnapping="Snap Sources to other sources" Basic.Settings.General.SnapDistance="Snap Sensitivity" +Basic.Settings.General.SpacingHelpers="Show pixel alignment guides" Basic.Settings.General.RecordWhenStreaming="Automatically record when streaming" Basic.Settings.General.KeepRecordingWhenStreamStops="Keep recording when stream stops" Basic.Settings.General.ReplayBufferWhileStreaming="Automatically start replay buffer when streaming" diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index ab6f68cc3..df3596cbe 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -628,6 +628,13 @@ + + + + Basic.Settings.General.SpacingHelpers + + + diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index 8966afa73..94bca83a5 100644 --- a/UI/obs-app.cpp +++ b/UI/obs-app.cpp @@ -451,6 +451,8 @@ bool OBSApp::InitGlobalConfigDefaults() false); config_set_default_double(globalConfig, "BasicWindow", "SnapDistance", 10.0); + config_set_default_bool(globalConfig, "BasicWindow", + "SpacingHelpersEnabled", true); config_set_default_bool(globalConfig, "BasicWindow", "RecordWhenStreaming", false); config_set_default_bool(globalConfig, "BasicWindow", diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 47d217c3f..18c228f6d 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -469,6 +469,7 @@ OBSBasic::OBSBasic(QWidget *parent) &OBSBasic::BroadcastButtonClicked); UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); } static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, @@ -4246,6 +4247,9 @@ void OBSBasic::RenderMain(void *data, uint32_t cx, uint32_t cy) RenderSafeAreas(window->rightLine, targetCX, targetCY); } + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + /* --------------------------------------- */ gs_projection_pop(); @@ -10177,3 +10181,9 @@ QColor OBSBasic::GetHoverColor() const return QColor::fromRgb(0, 127, 255); } } + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool( + App()->GlobalConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 8540ec9d4..cc19f2b33 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -632,6 +632,9 @@ private: QColor GetCropColor() const; QColor GetHoverColor() const; + void UpdatePreviewSpacingHelpers(); + bool drawSpacingHelpers = true; + public slots: void DeferSaveBegin(); void DeferSaveEnd(); diff --git a/UI/window-basic-preview.cpp b/UI/window-basic-preview.cpp index 6ecdc05ac..41ecbee2a 100644 --- a/UI/window-basic-preview.cpp +++ b/UI/window-basic-preview.cpp @@ -12,6 +12,7 @@ #define HANDLE_RADIUS 4.0f #define HANDLE_SEL_RADIUS (HANDLE_RADIUS * 1.5f) +#define HELPER_ROT_BREAKPONT 45.0f /* TODO: make C++ math classes and clean up code here later */ @@ -2286,3 +2287,298 @@ OBSBasicPreview *OBSBasicPreview::Get() { return OBSBasic::Get()->ui->preview; } + +static obs_source_t *CreateLabel() +{ + OBSDataAutoRelease settings = obs_data_create(); + OBSDataAutoRelease font = obs_data_create(); + +#if defined(_WIN32) + obs_data_set_string(font, "face", "Arial"); +#elif defined(__APPLE__) + obs_data_set_string(font, "face", "Helvetica"); +#else + obs_data_set_string(font, "face", "Monospace"); +#endif + obs_data_set_int(font, "flags", 1); // Bold text + obs_data_set_int(font, "size", 16); + + obs_data_set_obj(settings, "font", font); + obs_data_set_bool(settings, "outline", true); + +#ifdef _WIN32 + obs_data_set_int(settings, "outline_color", 0x000000); + obs_data_set_int(settings, "outline_size", 3); + const char *text_source_id = "text_gdiplus"; +#else + const char *text_source_id = "text_ft2_source"; +#endif + + OBSSource txtSource = + obs_source_create_private(text_source_id, NULL, settings); + + return txtSource; +} + +static void SetLabelText(int sourceIndex, int px) +{ + OBSBasicPreview *prev = OBSBasicPreview::Get(); + + if (px == prev->spacerPx[sourceIndex]) + return; + + std::string text = std::to_string(px) + " px"; + + obs_source_t *source = prev->spacerLabel[sourceIndex]; + + OBSDataAutoRelease settings = obs_source_get_settings(source); + obs_data_set_string(settings, "text", text.c_str()); + obs_source_update(source, settings); + + prev->spacerPx[sourceIndex] = px; +} + +static void DrawLabel(OBSSource source, vec3 &pos, vec3 &viewport) +{ + if (!source) + return; + + vec3_mul(&pos, &pos, &viewport); + + gs_matrix_push(); + gs_matrix_identity(); + gs_matrix_translate(&pos); + obs_source_video_render(source); + gs_matrix_pop(); +} + +static void DrawSpacingLine(vec3 &start, vec3 &end, vec3 &viewport) +{ + matrix4 transform; + matrix4_identity(&transform); + transform.x.x = viewport.x; + transform.y.y = viewport.y; + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + + vec4 color; + vec4_set(&color, 1.0f, 0.0f, 0.0f, 1.0f); + gs_effect_set_vec4(gs_effect_get_param_by_name(solid, "color"), &color); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + + gs_matrix_push(); + gs_matrix_mul(&transform); + + vec2 scale; + vec2_set(&scale, viewport.x, viewport.y); + + DrawLine(start.x, start.y, end.x, end.y, HANDLE_RADIUS / 2, scale); + + gs_matrix_pop(); + + gs_load_vertexbuffer(nullptr); + + gs_technique_end_pass(tech); + gs_technique_end(tech); +} + +static void RenderSpacingHelper(int sourceIndex, vec3 &start, vec3 &end, + vec3 &viewport) +{ + bool horizontal = (sourceIndex == 2 || sourceIndex == 3); + + // If outside of preview, don't render + if (!((horizontal && (end.x >= start.x)) || + (!horizontal && (end.y >= start.y)))) + return; + + float length = vec3_dist(&start, &end); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + float px; + + if (horizontal) { + px = length * ovi.base_width; + } else { + px = length * ovi.base_height; + } + + if (px <= 0.0f) + return; + + OBSBasicPreview *prev = OBSBasicPreview::Get(); + obs_source_t *source = prev->spacerLabel[sourceIndex]; + vec3 labelSize, labelPos; + vec3_set(&labelSize, obs_source_get_width(source), + obs_source_get_height(source), 1.0f); + + vec3_div(&labelSize, &labelSize, &viewport); + + vec3 labelMargin; + vec3_set(&labelMargin, SPACER_LABEL_MARGIN, SPACER_LABEL_MARGIN, 1.0f); + vec3_div(&labelMargin, &labelMargin, &viewport); + + vec3_set(&labelPos, end.x, end.y, end.z); + if (horizontal) { + labelPos.x -= (end.x - start.x) / 2; + labelPos.x -= labelSize.x / 2; + labelPos.y -= labelMargin.y + (labelSize.y / 2) + + (HANDLE_RADIUS / viewport.y); + } else { + labelPos.y -= (end.y - start.y) / 2; + labelPos.y -= labelSize.y / 2; + labelPos.x += labelMargin.x; + } + + DrawSpacingLine(start, end, viewport); + SetLabelText(sourceIndex, (int)px); + DrawLabel(source, labelPos, viewport); +} + +void OBSBasicPreview::DrawSpacingHelpers() +{ + if (locked) + return; + + OBSBasic *main = OBSBasic::Get(); + + if (main->ui->sources->selectionModel()->selectedIndexes().count() > 1) + return; + + OBSSceneItem item = main->GetCurrentSceneItem(); + if (!item) + return; + + if (obs_sceneitem_locked(item)) + return; + + vec2 itemSize = GetItemSize(item); + if (itemSize.x == 0.0f || itemSize.y == 0.0f) + return; + + matrix4 boxTransform; + obs_sceneitem_get_box_transform(item, &boxTransform); + + obs_transform_info oti; + obs_sceneitem_get_info(item, &oti); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 size; + vec3_set(&size, ovi.base_width, ovi.base_height, 1.0f); + + // Init box transform side locations + vec3 left, right, top, bottom; + + vec3_set(&left, 0.0f, 0.5f, 1.0f); + vec3_set(&right, 1.0f, 0.5f, 1.0f); + vec3_set(&top, 0.5f, 0.0f, 1.0f); + vec3_set(&bottom, 0.5f, 1.0f, 1.0f); + + // Decide which side to use with box transform, based on rotation + // Seems hacky, probably a better way to do it + float rot = oti.rot; + + if (rot >= HELPER_ROT_BREAKPONT) { + for (float i = HELPER_ROT_BREAKPONT; i <= 360.0f; i += 90.0f) { + if (rot < i) + break; + + vec3 l = left; + vec3 r = right; + vec3 t = top; + vec3 b = bottom; + + vec3_copy(&top, &l); + vec3_copy(&right, &t); + vec3_copy(&bottom, &r); + vec3_copy(&left, &b); + } + } else if (rot <= -HELPER_ROT_BREAKPONT) { + for (float i = -HELPER_ROT_BREAKPONT; i >= -360.0f; + i -= 90.0f) { + if (rot > i) + break; + + vec3 l = left; + vec3 r = right; + vec3 t = top; + vec3 b = bottom; + + vec3_copy(&top, &r); + vec3_copy(&right, &b); + vec3_copy(&bottom, &l); + vec3_copy(&left, &t); + } + } + + // Switch top/bottom or right/left if scale is negative + if (oti.scale.x < 0.0f) { + vec3 l = left; + vec3 r = right; + + vec3_copy(&left, &r); + vec3_copy(&right, &l); + } + + if (oti.scale.y < 0.0f) { + vec3 t = top; + vec3 b = bottom; + + vec3_copy(&top, &b); + vec3_copy(&bottom, &t); + } + + // Get sides of box transform + left = GetTransformedPos(left.x, left.y, boxTransform); + right = GetTransformedPos(right.x, right.y, boxTransform); + top = GetTransformedPos(top.x, top.y, boxTransform); + bottom = GetTransformedPos(bottom.x, bottom.y, boxTransform); + + bottom.y = size.y - bottom.y; + right.x = size.x - right.x; + + // Init viewport + vec3 viewport; + vec3_set(&viewport, main->previewCX, main->previewCY, 1.0f); + + vec3_div(&left, &left, &viewport); + vec3_div(&right, &right, &viewport); + vec3_div(&top, &top, &viewport); + vec3_div(&bottom, &bottom, &viewport); + + vec3_mulf(&left, &left, main->previewScale); + vec3_mulf(&right, &right, main->previewScale); + vec3_mulf(&top, &top, main->previewScale); + vec3_mulf(&bottom, &bottom, main->previewScale); + + // Draw spacer lines and labels + vec3 start, end; + + for (int i = 0; i < 4; i++) { + if (!spacerLabel[i]) + spacerLabel[i] = CreateLabel(); + } + + vec3_set(&start, top.x, 0.0f, 1.0f); + vec3_set(&end, top.x, top.y, 1.0f); + RenderSpacingHelper(0, start, end, viewport); + + vec3_set(&start, bottom.x, 1.0f - bottom.y, 1.0f); + vec3_set(&end, bottom.x, 1.0f, 1.0f); + RenderSpacingHelper(1, start, end, viewport); + + vec3_set(&start, 0.0f, left.y, 1.0f); + vec3_set(&end, left.x, left.y, 1.0f); + RenderSpacingHelper(2, start, end, viewport); + + vec3_set(&start, 1.0f - right.x, right.y, 1.0f); + vec3_set(&end, 1.0f, right.y, 1.0f); + RenderSpacingHelper(3, start, end, viewport); +} diff --git a/UI/window-basic-preview.hpp b/UI/window-basic-preview.hpp index c6bc95e56..586f740a9 100644 --- a/UI/window-basic-preview.hpp +++ b/UI/window-basic-preview.hpp @@ -20,6 +20,8 @@ class QMouseEvent; #define ZOOM_SENSITIVITY 1.125f +#define SPACER_LABEL_MARGIN 6.0f + enum class ItemHandle : uint32_t { None = 0, TopLeft = ITEM_TOP | ITEM_LEFT, @@ -166,4 +168,9 @@ public: * byte boundary. */ static inline void *operator new(size_t size) { return bmalloc(size); } static inline void operator delete(void *ptr) { bfree(ptr); } + + OBSSourceAutoRelease spacerLabel[4]; + int spacerPx[4] = {0}; + + void DrawSpacingHelpers(); }; diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index c5d6a2e30..e31b6551e 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -407,6 +407,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->overflowSelectionHide,CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->previewSafeAreas, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->automaticSearch, CHECK_CHANGED, GENERAL_CHANGED); + HookWidget(ui->previewSpacingHelpers,CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->doubleClickSwitch, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->studioPortraitLayout, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->prevProgLabelToggle, CHECK_CHANGED, GENERAL_CHANGED); @@ -1310,6 +1311,10 @@ void OBSBasicSettings::LoadGeneralSettings() GetGlobalConfig(), "BasicWindow", "WarnBeforeStartingStream"); ui->warnBeforeStreamStart->setChecked(warnBeforeStreamStart); + bool spacingHelpersEnabled = config_get_bool( + GetGlobalConfig(), "BasicWindow", "SpacingHelpersEnabled"); + ui->previewSpacingHelpers->setChecked(spacingHelpersEnabled); + bool warnBeforeStreamStop = config_get_bool( GetGlobalConfig(), "BasicWindow", "WarnBeforeStoppingStream"); ui->warnBeforeStreamStop->setChecked(warnBeforeStreamStop); @@ -3121,6 +3126,14 @@ void OBSBasicSettings::SaveGeneralSettings() ui->previewSafeAreas->isChecked()); main->UpdatePreviewSafeAreas(); } + + if (WidgetChanged(ui->previewSpacingHelpers)) { + config_set_bool(GetGlobalConfig(), "BasicWindow", + "SpacingHelpersEnabled", + ui->previewSpacingHelpers->isChecked()); + main->UpdatePreviewSpacingHelpers(); + } + if (WidgetChanged(ui->doubleClickSwitch)) config_set_bool(GetGlobalConfig(), "BasicWindow", "TransitionOnDoubleClick",