diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index a7fc6fc9d..571de9fcd 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -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; diff --git a/frontend/widgets/OBSBasicPreview.cpp b/frontend/widgets/OBSBasicPreview.cpp index 4fcd3f542..fc57ef883 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 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(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(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(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) { diff --git a/frontend/widgets/OBSBasicPreview.hpp b/frontend/widgets/OBSBasicPreview.hpp index fa22cf1e9..ff86d41d5 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; + + 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 snapGuides; std::vector hoveredPreviewItems; std::vector 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); diff --git a/frontend/widgets/OBSBasic_Preview.cpp b/frontend/widgets/OBSBasic_Preview.cpp index 79e458d8d..04d7d9bd0 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) {