From 02623d0803dbd798b03464ca1f787a2f81f3337a Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Mon, 15 Jun 2026 17:39:14 -0400 Subject: [PATCH 1/2] frontend: Improve preview source snapping --- frontend/widgets/OBSBasic.hpp | 1 + frontend/widgets/OBSBasicPreview.cpp | 218 +++++++++++++++++++++----- frontend/widgets/OBSBasicPreview.hpp | 14 ++ frontend/widgets/OBSBasic_Preview.cpp | 7 + 4 files changed, 202 insertions(+), 38 deletions(-) diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index 4343e16054c4da..6bdf6bab9a3aaf 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -873,6 +873,7 @@ private slots: void TogglePreview(); public: + void addSnapGuide(SnapGuide guide); inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) { x = previewX; diff --git a/frontend/widgets/OBSBasicPreview.cpp b/frontend/widgets/OBSBasicPreview.cpp index 4fcd3f5423c958..32cf28d0eb89df 100644 --- a/frontend/widgets/OBSBasicPreview.cpp +++ b/frontend/widgets/OBSBasicPreview.cpp @@ -9,6 +9,19 @@ #define HANDLE_SEL_RADIUS (HANDLE_RADIUS * 1.5f) #define HELPER_ROT_BREAKPOINT 45.0f +namespace { +bool checkEdgeSnap(float moveAxis, float checkAxis, float clampDistance, float &offset) +{ + double dist = fabsf(checkAxis - moveAxis); + if (dist < clampDistance && fabsf(offset) < EPSILON) { + offset = checkAxis - moveAxis; + return true; + } + + return false; +} +} // namespace + /* TODO: make C++ math classes and clean up code here later */ OBSBasicPreview::OBSBasicPreview(QWidget *parent, Qt::WindowFlags flags) : OBSQTDisplay(parent, flags) @@ -130,6 +143,9 @@ static bool FindItemAtPos(obs_scene_t * /* scene */, obs_sceneitem_t *item, void if (obs_sceneitem_locked(item)) { return true; } + if (!obs_sceneitem_visible(item)) { + return true; + } vec3_set(&pos3, data->pos.x, data->pos.y, 0.0f); @@ -177,6 +193,11 @@ static inline vec2 GetOBSScreenSize() return size; } +void OBSBasicPreview::addSnapGuide(SnapGuide guide) +{ + snapGuides.push_back(guide); +} + vec3 OBSBasicPreview::GetSnapOffset(const vec3 &tl, const vec3 &br) { OBSBasic *main = OBSBasic::Get(); @@ -198,32 +219,42 @@ vec3 OBSBasicPreview::GetSnapOffset(const vec3 &tl, const vec3 &br) const float centerX = br.x - (br.x - tl.x) / 2.0f; const float centerY = br.y - (br.y - tl.y) / 2.0f; - // Left screen edge. - if (screenSnap && fabsf(tl.x) < clampDist) { - clampOffset.x = -tl.x; - } - // Right screen edge. - if (screenSnap && fabsf(clampOffset.x) < EPSILON && fabsf(screenSize.x - br.x) < clampDist) { - clampOffset.x = screenSize.x - br.x; - } - // Horizontal center. - if (centerSnap && fabsf(screenSize.x - (br.x - tl.x)) > clampDist && - fabsf(screenSize.x / 2.0f - centerX) < clampDist) { - clampOffset.x = screenSize.x / 2.0f - centerX; - } + if (screenSnap) { + // Left screen edge. + if (checkEdgeSnap(tl.x, 0, clampDist, clampOffset.x)) { + main->addSnapGuide(SnapGuide(0, 0, 0, screenSize.y)); + } - // Top screen edge. - if (screenSnap && fabsf(tl.y) < clampDist) { - clampOffset.y = -tl.y; - } - // Bottom screen edge. - if (screenSnap && fabsf(clampOffset.y) < EPSILON && fabsf(screenSize.y - br.y) < clampDist) { - clampOffset.y = screenSize.y - br.y; + // Right screen edge. + if (checkEdgeSnap(br.x, screenSize.x, clampDist, clampOffset.x)) { + main->addSnapGuide(SnapGuide(screenSize.x, 0, screenSize.x, screenSize.y)); + } + + // Top screen edge. + if (checkEdgeSnap(tl.y, 0, clampDist, clampOffset.y)) { + main->addSnapGuide(SnapGuide(0, 0, screenSize.x, 0)); + } + + // Bottom screen edge. + if (checkEdgeSnap(br.y, screenSize.y, clampDist, clampOffset.y)) { + main->addSnapGuide(SnapGuide(0, screenSize.y, screenSize.x, screenSize.y)); + } } - // Vertical center. - if (centerSnap && fabsf(screenSize.y - (br.y - tl.y)) > clampDist && - fabsf(screenSize.y / 2.0f - centerY) < clampDist) { - clampOffset.y = screenSize.y / 2.0f - centerY; + + if (centerSnap) { + // Horizontal center. + if (fabsf(screenSize.x - (br.x - tl.x)) > clampDist && + fabsf(screenSize.x / 2.0f - centerX) < clampDist) { + clampOffset.x = screenSize.x / 2.0f - centerX; + main->addSnapGuide(SnapGuide(screenSize.x / 2.0f, 0, screenSize.x / 2.0f, screenSize.y)); + } + + // Vertical center. + if (fabsf(screenSize.y - (br.y - tl.y)) > clampDist && + fabsf(screenSize.y / 2.0f - centerY) < clampDist) { + clampOffset.y = screenSize.y / 2.0f - centerY; + main->addSnapGuide(SnapGuide(0, screenSize.y / 2.0f, screenSize.x, screenSize.y / 2.0f)); + } } return clampOffset; @@ -754,6 +785,7 @@ void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event) return; } + OBSBasic *main = OBSBasic::Get(); if (mouseDown) { vec2 pos = GetMouseEventPos(event); @@ -807,8 +839,9 @@ void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event) hoveredPreviewItems.clear(); hoveredPreviewItems.push_back(item); selectedItems.clear(); + snapGuides.clear(); } - OBSBasic *main = OBSBasic::Get(); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(main->GetCurrentScene(), true); auto undo_redo = [](const std::string &data) { @@ -893,15 +926,32 @@ static bool AddItemBounds(obs_scene_t * /* scene */, obs_sceneitem_t *item, void struct OffsetData { float clampDist; vec3 tl, br, offset; + + float left() { return tl.x; } + float top() { return tl.y; } + float right() { return br.x; } + float bottom() { return br.y; } + + vec2 center() + { + const float centerX = right() - (right() - left()) / 2.0f; + const float centerY = bottom() - (bottom() - top()) / 2.0f; + + return {centerX, centerY}; + } }; static bool GetSourceSnapOffset(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *param) { + OBSBasic *main = OBSBasic::Get(); OffsetData *data = static_cast(param); if (obs_sceneitem_selected(item)) { return true; } + if (!obs_sceneitem_visible(item)) { + return true; + } matrix4 boxTransform; obs_sceneitem_get_box_transform(item, &boxTransform); @@ -924,20 +974,58 @@ static bool GetSourceSnapOffset(obs_scene_t * /* scene */, obs_sceneitem_t *item } } - // Snap to other source edges -#define EDGE_SNAP(l, r, x, y) \ - do { \ - double dist = fabsf(l.x - data->r.x); \ - if (dist < data->clampDist && fabsf(data->offset.x) < EPSILON && data->tl.y < br.y && \ - data->br.y > tl.y && (fabsf(data->offset.x) > dist || data->offset.x < EPSILON)) \ - data->offset.x = l.x - data->r.x; \ - } while (false) + const auto screen = GetOBSScreenSize(); + + QRectF moveRect{data->left(), data->top(), data->right() - data->left(), data->bottom() - data->top()}; + QRectF itemRect{tl.x, tl.y, br.x - tl.x, br.y - tl.y}; + + const QPointF centerDelta{moveRect.center().x() - itemRect.center().x(), + moveRect.center().y() - itemRect.center().y()}; + + QPoint itemEdge{}; + QPoint movingEdge{}; + + itemEdge.rx() = centerDelta.x() < 0 ? itemRect.left() : itemRect.right(); + movingEdge.rx() = centerDelta.x() < 0 ? moveRect.left() : moveRect.right(); + + itemEdge.ry() = centerDelta.y() < 0 ? itemRect.top() : itemRect.bottom(); + movingEdge.ry() = centerDelta.y() < 0 ? moveRect.top() : moveRect.bottom(); + + // Horizontal snapping + // Check if the delta between centers is larger than the sum of half widths minus the clamp distance. + // Subtracting the clamp distance allows snapping from the "inner" edges when overlapping. + if (std::fabs(centerDelta.x()) > (moveRect.width() + itemRect.width() - (data->clampDist * 2)) / 2) { + // Moving item is not overlapping + movingEdge.rx() = centerDelta.x() < 0 ? moveRect.right() : moveRect.left(); + } else if (moveRect.width() > itemRect.width()) { + // Moving item is overlapping but larger, invert checked edges + itemEdge.rx() = centerDelta.x() > 0 ? itemRect.left() : itemRect.right(); + movingEdge.rx() = centerDelta.x() > 0 ? moveRect.left() : moveRect.right(); + } + + // Vertical snapping + // Check if the delta between centers is larger than the sum of half widths minus the clamp distance. + // Subtracting the clamp distance allows snapping from the "inner" edges when overlapping. + if (std::fabs(centerDelta.y()) > (moveRect.height() + itemRect.height() - (data->clampDist * 2)) / 2) { + // Moving item is not overlapping + movingEdge.ry() = centerDelta.y() < 0 ? moveRect.bottom() : moveRect.top(); + } else if (moveRect.height() > itemRect.height()) { + // Moving item is overlapping but larger, invert checked edges + itemEdge.ry() = centerDelta.y() > 0 ? itemRect.top() : itemRect.bottom(); + movingEdge.ry() = centerDelta.y() > 0 ? moveRect.top() : moveRect.bottom(); + } + + if (checkEdgeSnap(movingEdge.x(), itemEdge.x(), data->clampDist, data->offset.x)) { + main->addSnapGuide(SnapGuide(itemEdge.x(), 0.0f, itemEdge.x(), screen.y)); + } else if (checkEdgeSnap(moveRect.center().x(), itemRect.center().x(), data->clampDist, data->offset.x)) { + main->addSnapGuide(SnapGuide(itemRect.center().x(), 0.0f, itemRect.center().x(), screen.y)); + } - EDGE_SNAP(tl, br, x, y); - EDGE_SNAP(tl, br, y, x); - EDGE_SNAP(br, tl, x, y); - EDGE_SNAP(br, tl, y, x); -#undef EDGE_SNAP + if (checkEdgeSnap(movingEdge.y(), itemEdge.y(), data->clampDist, data->offset.y)) { + main->addSnapGuide(SnapGuide(0.0f, itemEdge.y(), screen.x, itemEdge.y())); + } else if (checkEdgeSnap(moveRect.center().y(), itemRect.center().y(), data->clampDist, data->offset.y)) { + main->addSnapGuide(SnapGuide(0.0f, itemRect.center().y(), screen.x, itemRect.center().y())); + } return true; } @@ -1632,6 +1720,8 @@ void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event) pos.x = std::round(pos.x); pos.y = std::round(pos.y); + snapGuides.clear(); + if (stretchHandle != ItemHandle::None) { if (obs_sceneitem_locked(stretchItem)) { return; @@ -2180,6 +2270,58 @@ bool OBSBasicPreview::DrawSelectionBox(float x1, float y1, float x2, float y2, g return true; } +void OBSBasicPreview::DrawSnapGuides() +{ + if (snapGuides.empty()) { + return; + } + + OBSBasic *main = OBSBasic::Get(); + + vec2 viewport; + vec2_set(&viewport, main->previewCX, main->previewCY); + + float pixelRatio = main->GetDevicePixelRatio(); + + 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 snapColor; + vec4_set(&snapColor, 0.0f, 1.0f, 1.0f, 0.8f); + + gs_effect_set_vec4(gs_effect_get_param_by_name(solid, "color"), &snapColor); + + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + + gs_matrix_push(); + gs_matrix_mul(&transform); + + for (const SnapGuide &guide : snapGuides) { + vec2 start, end; + + vec2_div(&start, &guide.start, &viewport); + vec2_div(&end, &guide.end, &viewport); + + vec2_mulf(&start, &start, main->previewScale); + vec2_mulf(&end, &end, main->previewScale); + + DrawLine(start.x, start.y, end.x, end.y, HANDLE_RADIUS * pixelRatio / 2, viewport); + } + + gs_matrix_pop(); + + gs_load_vertexbuffer(nullptr); + + gs_technique_end_pass(tech); + gs_technique_end(tech); +} + void OBSBasicPreview::DrawOverflow() { if (locked) { diff --git a/frontend/widgets/OBSBasicPreview.hpp b/frontend/widgets/OBSBasicPreview.hpp index fa22cf1e9e5b58..1b194dead07ef4 100644 --- a/frontend/widgets/OBSBasicPreview.hpp +++ b/frontend/widgets/OBSBasicPreview.hpp @@ -31,6 +31,17 @@ enum class ItemHandle : uint32_t { Rot = ITEM_ROT }; +struct SnapGuide { + vec2 start; + vec2 end; + + SnapGuide(float x1, float y1, float x2, float y2) + { + start = {x1, y1}; + end = {x2, y2}; + } +}; + class OBSBasicPreview : public OBSQTDisplay { Q_OBJECT @@ -80,6 +91,7 @@ class OBSBasicPreview : public OBSQTDisplay { bool updatingXScrollBar = false; bool updatingYScrollBar = false; + std::vector snapGuides; std::vector hoveredPreviewItems; std::vector selectedItems; std::mutex selectMutex; @@ -190,6 +202,8 @@ class OBSBasicPreview : public OBSQTDisplay { OBSSourceAutoRelease spacerLabel[4]; int spacerPx[4] = {0}; + void addSnapGuide(SnapGuide guide); + void DrawSnapGuides(); void DrawSpacingHelpers(); void ClampScrollingOffsets(); void UpdateXScrollBar(float cx); diff --git a/frontend/widgets/OBSBasic_Preview.cpp b/frontend/widgets/OBSBasic_Preview.cpp index 79e458d8d77329..04d7d9bd0bb3f6 100644 --- a/frontend/widgets/OBSBasic_Preview.cpp +++ b/frontend/widgets/OBSBasic_Preview.cpp @@ -195,6 +195,8 @@ void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) window->ui->preview->DrawSpacingHelpers(); } + window->ui->preview->DrawSnapGuides(); + /* --------------------------------------- */ gs_projection_pop(); @@ -273,6 +275,11 @@ void OBSBasic::TogglePreview() EnablePreviewDisplay(previewEnabled); } +void OBSBasic::addSnapGuide(SnapGuide guide) +{ + ui->preview->addSnapGuide(guide); +} + void OBSBasic::EnablePreview() { if (previewProgramMode) { From 5dc9453eb38022b20e0e7aa59f0203b701172dcd Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Mon, 15 Jun 2026 19:10:12 -0400 Subject: [PATCH 2/2] frontend: Update default snap distance --- frontend/OBSApp.cpp | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index 0d6d0e28e35fbd..a34790f449273d 100644 --- a/frontend/OBSApp.cpp +++ b/frontend/OBSApp.cpp @@ -81,6 +81,8 @@ extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 1; extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; #endif +static constexpr double kDefaultSnapDistance = 5.0; + namespace { typedef struct UncleanLaunchAction { @@ -361,7 +363,7 @@ void OBSApp::InitUserConfigDefaults() config_set_default_bool(userConfig, "BasicWindow", "ScreenSnapping", true); config_set_default_bool(userConfig, "BasicWindow", "SourceSnapping", true); config_set_default_bool(userConfig, "BasicWindow", "CenterSnapping", false); - config_set_default_double(userConfig, "BasicWindow", "SnapDistance", 10.0); + config_set_default_double(userConfig, "BasicWindow", "SnapDistance", kDefaultSnapDistance); config_set_default_bool(userConfig, "BasicWindow", "SpacingHelpersEnabled", true); config_set_default_bool(userConfig, "BasicWindow", "RecordWhenStreaming", false); config_set_default_bool(userConfig, "BasicWindow", "KeepRecordingWhenStreamStops", false); @@ -628,7 +630,8 @@ bool OBSApp::InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t void OBSApp::MigrateLegacySettings(const uint32_t lastVersion) { - bool hasChanges = false; + bool hasUserConfigChanges = false; + bool hasGlobalConfigChanges = false; const uint32_t v19 = MAKE_SEMANTIC_VERSION(19, 0, 0); const uint32_t v21 = MAKE_SEMANTIC_VERSION(21, 0, 0); @@ -644,7 +647,7 @@ void OBSApp::MigrateLegacySettings(const uint32_t lastVersion) bool useOldDefaults = lastVersion && lastVersion < version; config_set_bool(userConfig, "General", configKey.c_str(), useOldDefaults); - hasChanges = true; + hasUserConfigChanges = true; } } @@ -653,7 +656,7 @@ void OBSApp::MigrateLegacySettings(const uint32_t lastVersion) bool layoutUpdated = UpdatePre22MultiviewLayout(layout); - hasChanges = hasChanges | layoutUpdated; + hasUserConfigChanges = hasUserConfigChanges | layoutUpdated; } if (lastVersion && lastVersion < v24) { @@ -663,12 +666,34 @@ void OBSApp::MigrateLegacySettings(const uint32_t lastVersion) config_set_string(userConfig, "General", "HotkeyFocusType", "DisableHotkeysInFocus"); } - hasChanges = true; + hasUserConfigChanges = true; + } + + if (lastVersion && lastVersion < MAKE_SEMANTIC_VERSION(32, 2, 0)) { + bool migratedUserSettings = config_has_user_value(appConfig, "General", "Pre32.2Migrated"); + + if (!migratedUserSettings) { + double currentSnapDistance = config_get_double(userConfig, "BasicWindow", "SnapDistance"); + + if (currentSnapDistance == 10.0) { + double newDefaultSnapDistance = kDefaultSnapDistance; + config_set_double(userConfig, "BasicWindow", "SnapDistance", newDefaultSnapDistance); + } + + config_set_bool(appConfig, "General", "Pre32.2Migrated", true); + + hasUserConfigChanges = true; + hasGlobalConfigChanges = true; + } } - if (hasChanges) { + if (hasUserConfigChanges) { userConfig.SaveSafe("tmp"); } + + if (hasGlobalConfigChanges) { + appConfig.SaveSafe("tmp"); + } } static constexpr string_view OBSGlobalIniPath = "/obs-studio/global.ini";