mirror of
https://github.com/obsproject/obs-studio.git
synced 2026-09-26 01:10:38 -04:00
frontend: Improve preview source snapping
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user