frontend: Improve preview source snapping

This commit is contained in:
Warchamp7
2026-09-22 17:27:09 -04:00
committed by Ryan Foster
parent d654632c0f
commit bb2e23e160
4 changed files with 230 additions and 38 deletions
+1
View File
@@ -878,6 +878,7 @@ private slots:
void TogglePreview();
public:
void addSnapGuide(SnapGuide guide);
inline void GetDisplayRect(int &x, int &y, int &cx, int &cy)
{
x = previewX;
+208 -38
View File
@@ -9,6 +9,19 @@
#define HANDLE_SEL_RADIUS (HANDLE_RADIUS * 1.5f)
#define HELPER_ROT_BREAKPOINT 45.0f
namespace {
bool tryEdgeSnapForOffset(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,62 @@ 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 (tryEdgeSnapForOffset(tl.x, 0, clampDist, clampOffset.x)) {
vec2 startPoint{0, 0};
vec2 endPoint{0, screenSize.y};
SnapGuide guide{startPoint, endPoint};
main->addSnapGuide(guide);
}
// Right screen edge.
if (tryEdgeSnapForOffset(br.x, screenSize.x, clampDist, clampOffset.x)) {
vec2 startPoint{screenSize.x, 0};
vec2 endPoint{screenSize.x, screenSize.y};
SnapGuide guide{startPoint, endPoint};
main->addSnapGuide(guide);
}
// Top screen edge.
if (tryEdgeSnapForOffset(tl.y, 0, clampDist, clampOffset.y)) {
vec2 startPoint{0, 0};
vec2 endPoint{screenSize.x, 0};
SnapGuide guide{startPoint, endPoint};
main->addSnapGuide(guide);
}
// Bottom screen edge.
if (tryEdgeSnapForOffset(br.y, screenSize.y, clampDist, clampOffset.y)) {
vec2 startPoint{0, screenSize.y};
vec2 endPoint{screenSize.x, screenSize.y};
SnapGuide guide{startPoint, endPoint};
main->addSnapGuide(guide);
}
}
// 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;
}
// 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;
vec2 startPoint{screenSize.x / 2.0f, 0};
vec2 endPoint{screenSize.x / 2.0f, screenSize.y};
SnapGuide guide{startPoint, endPoint};
main->addSnapGuide(guide);
}
// 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;
vec2 startPoint{0, screenSize.y / 2.0f};
vec2 endPoint{screenSize.x, screenSize.y / 2.0f};
SnapGuide guide{startPoint, endPoint};
main->addSnapGuide(guide);
}
}
return clampOffset;
@@ -754,6 +805,7 @@ void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event)
return;
}
OBSBasic *main = OBSBasic::Get();
if (mouseDown) {
vec2 pos = GetMouseEventPos(event);
@@ -807,8 +859,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 +946,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<OffsetData *>(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 +994,66 @@ 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();
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
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()};
vec2 itemEdge{};
vec2 movingEdge{};
itemEdge.x = centerDelta.x() < 0 ? itemRect.left() : itemRect.right();
movingEdge.x = centerDelta.x() < 0 ? moveRect.left() : moveRect.right();
itemEdge.y = centerDelta.y() < 0 ? itemRect.top() : itemRect.bottom();
movingEdge.y = 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.x = centerDelta.x() < 0 ? moveRect.right() : moveRect.left();
} else if (moveRect.width() > itemRect.width()) {
// Moving item is overlapping but larger, invert checked edges
itemEdge.x = centerDelta.x() > 0 ? itemRect.left() : itemRect.right();
movingEdge.x = 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.y = centerDelta.y() < 0 ? moveRect.bottom() : moveRect.top();
} else if (moveRect.height() > itemRect.height()) {
// Moving item is overlapping but larger, invert checked edges
itemEdge.y = centerDelta.y() > 0 ? itemRect.top() : itemRect.bottom();
movingEdge.y = centerDelta.y() > 0 ? moveRect.top() : moveRect.bottom();
}
if (tryEdgeSnapForOffset(movingEdge.x, itemEdge.x, data->clampDist, data->offset.x)) {
SnapGuide guide{{itemEdge.x, 0.0f}, {itemEdge.x, screen.y}};
main->addSnapGuide(guide);
} else if (tryEdgeSnapForOffset(moveRect.center().x(), itemRect.center().x(), data->clampDist,
data->offset.x)) {
float itemCenterX = static_cast<float>(itemRect.center().x());
SnapGuide guide{{itemCenterX, 0.0f}, {itemCenterX, screen.y}};
main->addSnapGuide(guide);
}
if (tryEdgeSnapForOffset(movingEdge.y, itemEdge.y, data->clampDist, data->offset.y)) {
SnapGuide guide{{0.0f, itemEdge.y}, {screen.x, itemEdge.y}};
main->addSnapGuide(guide);
} else if (tryEdgeSnapForOffset(moveRect.center().y(), itemRect.center().y(), data->clampDist,
data->offset.y)) {
float itemCenterY = static_cast<float>(itemRect.center().y());
SnapGuide guide{{0.0f, itemCenterY}, {screen.x, itemCenterY}};
main->addSnapGuide(guide);
}
return true;
}
@@ -1632,6 +1748,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 +2298,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) {
+14
View File
@@ -31,6 +31,17 @@ enum class ItemHandle : uint32_t {
Rot = ITEM_ROT
};
struct SnapGuide {
vec2 start;
vec2 end;
explicit SnapGuide(vec2 start, vec2 end)
{
this->start = start;
this->end = end;
}
};
class OBSBasicPreview : public OBSQTDisplay {
Q_OBJECT
@@ -80,6 +91,7 @@ private:
bool updatingXScrollBar = false;
bool updatingYScrollBar = false;
std::vector<SnapGuide> snapGuides;
std::vector<obs_sceneitem_t *> hoveredPreviewItems;
std::vector<obs_sceneitem_t *> selectedItems;
std::mutex selectMutex;
@@ -190,6 +202,8 @@ public:
OBSSourceAutoRelease spacerLabel[4];
int spacerPx[4] = {0};
void addSnapGuide(SnapGuide guide);
void DrawSnapGuides();
void DrawSpacingHelpers();
void ClampScrollingOffsets();
void UpdateXScrollBar(float cx);
+7
View File
@@ -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) {