diff --git a/obs/data/locale/en-US.ini b/obs/data/locale/en-US.ini
index 476904a80..f2da3fee9 100644
--- a/obs/data/locale/en-US.ini
+++ b/obs/data/locale/en-US.ini
@@ -358,6 +358,7 @@ Basic.Settings.General.WarnBeforeStoppingStream="Show confirmation dialog when s
Basic.Settings.General.Snapping="Source Alignment Snapping"
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 mode 'stream' settings
diff --git a/obs/forms/OBSBasicSettings.ui b/obs/forms/OBSBasicSettings.ui
index f94c7d013..c2d69774b 100644
--- a/obs/forms/OBSBasicSettings.ui
+++ b/obs/forms/OBSBasicSettings.ui
@@ -243,6 +243,16 @@
+ -
+
+
+ Basic.Settings.General.SourceSnapping
+
+
+ true
+
+
+
-
@@ -3626,6 +3636,22 @@
476
202
+
+ 466
+ 253
+
+
+
+
+ snappingEnabled
+ toggled(bool)
+ sourceSnapping
+ setEnabled(bool)
+
+
+ 518
+ 204
+
515
277
diff --git a/obs/obs-app.cpp b/obs/obs-app.cpp
index f435a22a4..9a5654923 100644
--- a/obs/obs-app.cpp
+++ b/obs/obs-app.cpp
@@ -343,6 +343,8 @@ bool OBSApp::InitGlobalConfigDefaults()
"SnappingEnabled", true);
config_set_default_bool(globalConfig, "BasicWindow",
"ScreenSnapping", true);
+ config_set_default_bool(globalConfig, "BasicWindow",
+ "SourceSnapping", true);
config_set_default_bool(globalConfig, "BasicWindow",
"CenterSnapping", false);
config_set_default_double(globalConfig, "BasicWindow",
diff --git a/obs/window-basic-preview.cpp b/obs/window-basic-preview.cpp
index 7d6ebdf84..1b995c421 100644
--- a/obs/window-basic-preview.cpp
+++ b/obs/window-basic-preview.cpp
@@ -502,6 +502,66 @@ static bool AddItemBounds(obs_scene_t *scene, obs_sceneitem_t *item,
return true;
}
+struct OffsetData {
+ float clampDist;
+ vec3 tl, br, offset;
+};
+
+static bool GetSourceSnapOffset(obs_scene_t *scene, obs_sceneitem_t *item,
+ void *param)
+{
+ OffsetData *data = reinterpret_cast(param);
+
+ if (obs_sceneitem_selected(item))
+ return true;
+
+ matrix4 boxTransform;
+ obs_sceneitem_get_box_transform(item, &boxTransform);
+
+ vec3 t[4] = {
+ GetTransformedPos(0.0f, 0.0f, boxTransform),
+ GetTransformedPos(1.0f, 0.0f, boxTransform),
+ GetTransformedPos(0.0f, 1.0f, boxTransform),
+ GetTransformedPos(1.0f, 1.0f, boxTransform)
+ };
+
+ bool first = true;
+ vec3 tl, br;
+ vec3_zero(&tl);
+ vec3_zero(&br);
+ for (const vec3 &v : t) {
+ if (first) {
+ vec3_copy(&tl, &v);
+ vec3_copy(&br, &v);
+ first = false;
+ } else {
+ vec3_min(&tl, &tl, &v);
+ vec3_max(&br, &br, &v);
+ }
+ }
+
+ // 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)
+
+ 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
+
+ UNUSED_PARAMETER(scene);
+ return true;
+}
+
void OBSBasicPreview::SnapItemMovement(vec2 &offset)
{
OBSBasic *main = reinterpret_cast(App()->GetMainWindow());
@@ -519,11 +579,35 @@ void OBSBasicPreview::SnapItemMovement(vec2 &offset)
const bool snap = config_get_bool(GetGlobalConfig(),
"BasicWindow", "SnappingEnabled");
+ const bool sourcesSnap = config_get_bool(GetGlobalConfig(),
+ "BasicWindow", "SourceSnapping");
if (snap == false)
return;
+ if (sourcesSnap == false) {
+ offset.x += snapOffset.x;
+ offset.y += snapOffset.y;
+ return;
+ }
- offset.x += snapOffset.x;
- offset.y += snapOffset.y;
+ const float clampDist = config_get_double(GetGlobalConfig(),
+ "BasicWindow", "SnapDistance") / main->previewScale;
+
+ OffsetData offsetData;
+ offsetData.clampDist = clampDist;
+ offsetData.tl = data.tl;
+ offsetData.br = data.br;
+ vec3_copy(&offsetData.offset, &snapOffset);
+
+ obs_scene_enum_items(scene, GetSourceSnapOffset, &offsetData);
+
+ if (fabsf(offsetData.offset.x) > EPSILON ||
+ fabsf(offsetData.offset.y) > EPSILON) {
+ offset.x += offsetData.offset.x;
+ offset.y += offsetData.offset.y;
+ } else {
+ offset.x += snapOffset.x;
+ offset.y += snapOffset.y;
+ }
}
static bool move_items(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
diff --git a/obs/window-basic-settings.cpp b/obs/window-basic-settings.cpp
index c3302e886..6a8f3e4a9 100644
--- a/obs/window-basic-settings.cpp
+++ b/obs/window-basic-settings.cpp
@@ -269,6 +269,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
HookWidget(ui->snappingEnabled, CHECK_CHANGED, GENERAL_CHANGED);
HookWidget(ui->screenSnapping, CHECK_CHANGED, GENERAL_CHANGED);
HookWidget(ui->centerSnapping, CHECK_CHANGED, GENERAL_CHANGED);
+ HookWidget(ui->sourceSnapping, CHECK_CHANGED, GENERAL_CHANGED);
HookWidget(ui->snapDistance, SCROLL_CHANGED, GENERAL_CHANGED);
HookWidget(ui->outputMode, COMBO_CHANGED, OUTPUTS_CHANGED);
HookWidget(ui->streamType, COMBO_CHANGED, STREAM1_CHANGED);
@@ -779,6 +780,10 @@ void OBSBasicSettings::LoadGeneralSettings()
"BasicWindow", "CenterSnapping");
ui->centerSnapping->setChecked(centerSnapping);
+ bool sourceSnapping = config_get_bool(GetGlobalConfig(),
+ "BasicWindow", "SourceSnapping");
+ ui->sourceSnapping->setChecked(sourceSnapping);
+
double snapDistance = config_get_double(GetGlobalConfig(),
"BasicWindow", "SnapDistance");
ui->snapDistance->setValue(snapDistance);
@@ -2071,6 +2076,10 @@ void OBSBasicSettings::SaveGeneralSettings()
config_set_bool(GetGlobalConfig(), "BasicWindow",
"CenterSnapping",
ui->centerSnapping->isChecked());
+ if (WidgetChanged(ui->sourceSnapping))
+ config_set_bool(GetGlobalConfig(), "BasicWindow",
+ "SourceSnapping",
+ ui->sourceSnapping->isChecked());
if (WidgetChanged(ui->snapDistance))
config_set_double(GetGlobalConfig(), "BasicWindow",
"SnapDistance",