diff --git a/src/Utils.cpp b/src/Utils.cpp
index 570e676d..ecd6039f 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -28,6 +28,25 @@ with this program. If not, see
Q_DECLARE_METATYPE(OBSScene);
+const QHash boundTypeNames = {
+ { OBS_BOUNDS_STRETCH, "OBS_BOUNDS_STRETCH" },
+ { OBS_BOUNDS_SCALE_INNER, "OBS_BOUNDS_SCALE_INNER" },
+ { OBS_BOUNDS_SCALE_OUTER, "OBS_BOUNDS_SCALE_OUTER" },
+ { OBS_BOUNDS_SCALE_TO_WIDTH, "OBS_BOUNDS_SCALE_TO_WIDTH" },
+ { OBS_BOUNDS_SCALE_TO_HEIGHT, "OBS_BOUNDS_SCALE_TO_HEIGHT" },
+ { OBS_BOUNDS_MAX_ONLY, "OBS_BOUNDS_MAX_ONLY" },
+ { OBS_BOUNDS_NONE, "OBS_BOUNDS_NONE" },
+};
+
+QString getBoundsNameFromType(obs_bounds_type type) {
+ QString fallback = boundTypeNames.value(OBS_BOUNDS_NONE);
+ return boundTypeNames.value(type, fallback);
+}
+
+obs_bounds_type getBoundsTypeFromName(QString name) {
+ return boundTypeNames.key(name);
+}
+
obs_data_array_t* Utils::StringListToArray(char** strings, char* key) {
if (!strings)
return obs_data_array_create();
@@ -110,7 +129,7 @@ obs_sceneitem_t* Utils::GetSceneItemFromItem(obs_source_t* source, obs_data_t* i
if (obs_data_has_user_value(item, "name") &&
(QString)obs_source_get_name(obs_sceneitem_get_source(sceneItem)) !=
(QString)obs_data_get_string(item, "name")) {
- return nullptr;
+ return nullptr;
}
}
else if (obs_data_has_user_value(item, "name")) {
@@ -553,3 +572,72 @@ bool Utils::SetFilenameFormatting(const char* filenameFormatting) {
config_save(profile);
return true;
}
+
+// Transform properties copy-pasted from WSRequestHandler_SceneItems.cpp because typedefs can't be extended yet
+
+/**
+ * @typedef {Object} `SceneItemTransform`
+ * @property {int} `position.x` The x position of the scene item from the left.
+ * @property {int} `position.y` The y position of the scene item from the top.
+ * @property {int} `position.alignment` The point on the scene item that the item is manipulated from.
+ * @property {double} `rotation` The clockwise rotation of the scene item in degrees around the point of alignment.
+ * @property {double} `scale.x` The x-scale factor of the scene item.
+ * @property {double} `scale.y` The y-scale factor of the scene item.
+ * @property {int} `crop.top` The number of pixels cropped off the top of the scene item before scaling.
+ * @property {int} `crop.right` The number of pixels cropped off the right of the scene item before scaling.
+ * @property {int} `crop.bottom` The number of pixels cropped off the bottom of the scene item before scaling.
+ * @property {int} `crop.left` The number of pixels cropped off the left of the scene item before scaling.
+ * @property {bool} `visible` If the scene item is visible.
+ * @property {String} `bounds.type` Type of bounding box. Can be "OBS_BOUNDS_STRETCH", "OBS_BOUNDS_SCALE_INNER", "OBS_BOUNDS_SCALE_OUTER", "OBS_BOUNDS_SCALE_TO_WIDTH", "OBS_BOUNDS_SCALE_TO_HEIGHT", "OBS_BOUNDS_MAX_ONLY" or "OBS_BOUNDS_NONE".
+ * @property {int} `bounds.alignment` Alignment of the bounding box.
+ * @property {double} `bounds.x` Width of the bounding box.
+ * @property {double} `bounds.y` Height of the bounding box.
+ */
+obs_data_t* Utils::GetSceneItemPropertiesData(obs_sceneitem_t* sceneItem) {
+ vec2 pos, scale, bounds;
+ obs_sceneitem_crop crop;
+
+ obs_sceneitem_get_pos(sceneItem, &pos);
+ obs_sceneitem_get_scale(sceneItem, &scale);
+ obs_sceneitem_get_crop(sceneItem, &crop);
+ obs_sceneitem_get_bounds(sceneItem, &bounds);
+
+ uint32_t alignment = obs_sceneitem_get_alignment(sceneItem);
+ float rotation = obs_sceneitem_get_rot(sceneItem);
+ bool isVisible = obs_sceneitem_visible(sceneItem);
+
+ obs_bounds_type boundsType = obs_sceneitem_get_bounds_type(sceneItem);
+ uint32_t boundsAlignment = obs_sceneitem_get_bounds_alignment(sceneItem);
+ QString boundsTypeName = getBoundsNameFromType(boundsType);
+
+ OBSDataAutoRelease posData = obs_data_create();
+ obs_data_set_double(posData, "x", pos.x);
+ obs_data_set_double(posData, "y", pos.y);
+ obs_data_set_int(posData, "alignment", alignment);
+
+ OBSDataAutoRelease scaleData = obs_data_create();
+ obs_data_set_double(scaleData, "x", scale.x);
+ obs_data_set_double(scaleData, "y", scale.y);
+
+ OBSDataAutoRelease cropData = obs_data_create();
+ obs_data_set_int(cropData, "left", crop.left);
+ obs_data_set_int(cropData, "top", crop.top);
+ obs_data_set_int(cropData, "right", crop.right);
+ obs_data_set_int(cropData, "bottom", crop.bottom);
+
+ OBSDataAutoRelease boundsData = obs_data_create();
+ obs_data_set_string(boundsData, "type", boundsTypeName.toUtf8());
+ obs_data_set_int(boundsData, "alignment", boundsAlignment);
+ obs_data_set_double(boundsData, "x", bounds.x);
+ obs_data_set_double(boundsData, "y", bounds.y);
+
+ obs_data_t* data = obs_data_create();
+ obs_data_set_obj(data, "position", posData);
+ obs_data_set_double(data, "rotation", rotation);
+ obs_data_set_obj(data, "scale", scaleData);
+ obs_data_set_obj(data, "crop", cropData);
+ obs_data_set_bool(data, "visible", isVisible);
+ obs_data_set_obj(data, "bounds", boundsData);
+
+ return data;
+}
diff --git a/src/Utils.h b/src/Utils.h
index 040f6d01..3c2d1b5b 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -41,6 +41,7 @@ class Utils {
static obs_sceneitem_t* GetSceneItemFromItem(obs_source_t* source, obs_data_t* item);
static obs_source_t* GetTransitionFromName(QString transitionName);
static obs_source_t* GetSceneFromNameOrCurrent(QString sceneName);
+ static obs_data_t* GetSceneItemPropertiesData(obs_sceneitem_t* item);
static bool IsValidAlignment(const uint32_t alignment);
diff --git a/src/WSEvents.cpp b/src/WSEvents.cpp
index 29aa8428..ca3d496d 100644
--- a/src/WSEvents.cpp
+++ b/src/WSEvents.cpp
@@ -256,6 +256,8 @@ void WSEvents::connectSceneSignals(obs_source_t* scene) {
"item_remove", OnSceneItemDelete, this);
signal_handler_disconnect(sh,
"item_visible", OnSceneItemVisibilityChanged, this);
+ signal_handler_disconnect(sh,
+ "item_transform", OnSceneItemTransform, this);
}
currentScene = scene;
@@ -271,6 +273,8 @@ void WSEvents::connectSceneSignals(obs_source_t* scene) {
"item_remove", OnSceneItemDelete, this);
signal_handler_connect(sh,
"item_visible", OnSceneItemVisibilityChanged, this);
+ signal_handler_connect(sh,
+ "item_transform", OnSceneItemTransform, this);
}
}
@@ -911,6 +915,42 @@ void WSEvents::OnSceneItemVisibilityChanged(void* param, calldata_t* data) {
instance->broadcastUpdate("SceneItemVisibilityChanged", fields);
}
+/**
+ * An item's transfrom has been changed.
+ *
+ * @return {String} `scene-name` Name of the scene.
+ * @return {String} `item-name` Name of the item in the scene.
+ * @return {SceneItemProperties} `transform` Scene item transform properties
+ *
+ * @api events
+ * @name SceneItemTransformChanged
+ * @category sources
+ * @since unreleased
+ */
+void WSEvents::OnSceneItemTransform(void* param, calldata_t* data) {
+ WSEvents* instance = static_cast(param);
+
+ obs_scene_t* scene = nullptr;
+ calldata_get_ptr(data, "scene", &scene);
+
+ obs_sceneitem_t* sceneItem = nullptr;
+ calldata_get_ptr(data, "item", &sceneItem);
+
+ const char* sceneName =
+ obs_source_get_name(obs_scene_get_source(scene));
+ const char* sceneItemName =
+ obs_source_get_name(obs_sceneitem_get_source(sceneItem));
+
+ OBSDataAutoRelease transform = Utils::GetSceneItemPropertiesData(sceneItem);
+
+ OBSDataAutoRelease fields = obs_data_create();
+ obs_data_set_string(fields, "scene-name", sceneName);
+ obs_data_set_string(fields, "item-name", sceneItemName);
+ obs_data_set_obj(fields, "transform", transform);
+
+ instance->broadcastUpdate("SceneItemTransformChanged", fields);
+}
+
/**
* The selected preview scene has changed (only available in Studio Mode).
*
diff --git a/src/WSEvents.h b/src/WSEvents.h
index 48de519d..c9f370fd 100644
--- a/src/WSEvents.h
+++ b/src/WSEvents.h
@@ -116,4 +116,5 @@ private:
static void OnSceneItemAdd(void* param, calldata_t* data);
static void OnSceneItemDelete(void* param, calldata_t* data);
static void OnSceneItemVisibilityChanged(void* param, calldata_t* data);
+ static void OnSceneItemTransform(void* param, calldata_t* data);
};
diff --git a/src/WSRequestHandler_SceneItems.cpp b/src/WSRequestHandler_SceneItems.cpp
index ddc8d336..6b19ae97 100644
--- a/src/WSRequestHandler_SceneItems.cpp
+++ b/src/WSRequestHandler_SceneItems.cpp
@@ -21,7 +21,7 @@
* @return {int} `crop.bottom` The number of pixels cropped off the bottom of the source before scaling.
* @return {int} `crop.left` The number of pixels cropped off the left of the source before scaling.
* @return {bool} `visible` If the source is visible.
-* @return {String} `bounds.type` Type of bounding box.
+* @return {String} `bounds.type` Type of bounding box. Can be "OBS_BOUNDS_STRETCH", "OBS_BOUNDS_SCALE_INNER", "OBS_BOUNDS_SCALE_OUTER", "OBS_BOUNDS_SCALE_TO_WIDTH", "OBS_BOUNDS_SCALE_TO_HEIGHT", "OBS_BOUNDS_MAX_ONLY" or "OBS_BOUNDS_NONE".
* @return {int} `bounds.alignment` Alignment of the bounding box.
* @return {double} `bounds.x` Width of the bounding box.
* @return {double} `bounds.y` Height of the bounding box.
@@ -53,77 +53,9 @@ HandlerResponse WSRequestHandler::HandleGetSceneItemProperties(WSRequestHandler*
return req->SendErrorResponse("specified scene item doesn't exist");
}
- OBSDataAutoRelease data = obs_data_create();
+ OBSDataAutoRelease data = Utils::GetSceneItemPropertiesData(sceneItem);
obs_data_set_string(data, "name", itemName.toUtf8());
- OBSDataAutoRelease posData = obs_data_create();
- vec2 pos;
- obs_sceneitem_get_pos(sceneItem, &pos);
- obs_data_set_double(posData, "x", pos.x);
- obs_data_set_double(posData, "y", pos.y);
- obs_data_set_int(posData, "alignment", obs_sceneitem_get_alignment(sceneItem));
- obs_data_set_obj(data, "position", posData);
-
- obs_data_set_double(data, "rotation", obs_sceneitem_get_rot(sceneItem));
-
- OBSDataAutoRelease scaleData = obs_data_create();
- vec2 scale;
- obs_sceneitem_get_scale(sceneItem, &scale);
- obs_data_set_double(scaleData, "x", scale.x);
- obs_data_set_double(scaleData, "y", scale.y);
- obs_data_set_obj(data, "scale", scaleData);
-
- OBSDataAutoRelease cropData = obs_data_create();
- obs_sceneitem_crop crop;
- obs_sceneitem_get_crop(sceneItem, &crop);
- obs_data_set_int(cropData, "left", crop.left);
- obs_data_set_int(cropData, "top", crop.top);
- obs_data_set_int(cropData, "right", crop.right);
- obs_data_set_int(cropData, "bottom", crop.bottom);
- obs_data_set_obj(data, "crop", cropData);
-
- obs_data_set_bool(data, "visible", obs_sceneitem_visible(sceneItem));
-
- OBSDataAutoRelease boundsData = obs_data_create();
- obs_bounds_type boundsType = obs_sceneitem_get_bounds_type(sceneItem);
- if (boundsType == OBS_BOUNDS_NONE) {
- obs_data_set_string(boundsData, "type", "OBS_BOUNDS_NONE");
- }
- else {
- switch (boundsType) {
- case OBS_BOUNDS_STRETCH: {
- obs_data_set_string(boundsData, "type", "OBS_BOUNDS_STRETCH");
- break;
- }
- case OBS_BOUNDS_SCALE_INNER: {
- obs_data_set_string(boundsData, "type", "OBS_BOUNDS_SCALE_INNER");
- break;
- }
- case OBS_BOUNDS_SCALE_OUTER: {
- obs_data_set_string(boundsData, "type", "OBS_BOUNDS_SCALE_OUTER");
- break;
- }
- case OBS_BOUNDS_SCALE_TO_WIDTH: {
- obs_data_set_string(boundsData, "type", "OBS_BOUNDS_SCALE_TO_WIDTH");
- break;
- }
- case OBS_BOUNDS_SCALE_TO_HEIGHT: {
- obs_data_set_string(boundsData, "type", "OBS_BOUNDS_SCALE_TO_HEIGHT");
- break;
- }
- case OBS_BOUNDS_MAX_ONLY: {
- obs_data_set_string(boundsData, "type", "OBS_BOUNDS_MAX_ONLY");
- break;
- }
- }
- obs_data_set_int(boundsData, "alignment", obs_sceneitem_get_bounds_alignment(sceneItem));
- vec2 bounds;
- obs_sceneitem_get_bounds(sceneItem, &bounds);
- obs_data_set_double(boundsData, "x", bounds.x);
- obs_data_set_double(boundsData, "y", bounds.y);
- }
- obs_data_set_obj(data, "bounds", boundsData);
-
return req->SendOKResponse(data);
}
@@ -143,7 +75,7 @@ HandlerResponse WSRequestHandler::HandleGetSceneItemProperties(WSRequestHandler*
* @param {int} `crop.left` The new amount of pixels cropped off the left of the source before scaling.
* @param {int} `crop.right` The new amount of pixels cropped off the right of the source before scaling.
* @param {bool} `visible` The new visibility of the source. 'true' shows source, 'false' hides source.
-* @param {String} `bounds.type` The new bounds type of the source.
+* @param {String} `bounds.type` The new bounds type of the source. Can be "OBS_BOUNDS_STRETCH", "OBS_BOUNDS_SCALE_INNER", "OBS_BOUNDS_SCALE_OUTER", "OBS_BOUNDS_SCALE_TO_WIDTH", "OBS_BOUNDS_SCALE_TO_HEIGHT", "OBS_BOUNDS_MAX_ONLY" or "OBS_BOUNDS_NONE".
* @param {int} `bounds.alignment` The new alignment of the bounding box. (0-2, 4-6, 8-10)
* @param {double} `bounds.x` The new width of the bounding box.
* @param {double} `bounds.y` The new height of the bounding box.
@@ -306,7 +238,7 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemProperties(WSRequestHandler*
if (badRequest) {
return req->SendErrorResponse(errorMessage);
}
-
+
return req->SendOKResponse();
}
@@ -431,7 +363,7 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemPosition(WSRequestHandler* r
if (!sceneItem) {
return req->SendErrorResponse("specified scene item doesn't exist");
}
-
+
vec2 item_position = { 0 };
item_position.x = obs_data_get_double(req->data, "x");
item_position.y = obs_data_get_double(req->data, "y");
@@ -598,7 +530,7 @@ static void DuplicateSceneItem(void *_data, obs_scene_t *scene) {
* @return {Object} `item` New item info
* @return {int} `̀€item.id` New item ID
* @return {String} `item.name` New item name
- *
+ *
* @api requests
* @name DuplicateSceneItem
* @category scene items