From efdb6b464c985f3d3a08da461784d33627b6af5f Mon Sep 17 00:00:00 2001
From: wheaney <42350981+wheaney@users.noreply.github.com>
Date: Wed, 12 Feb 2025 14:04:44 -0800
Subject: [PATCH] Add support for testing without glasses connected, fix zoom
so it always comes towards the camera
---
gnome/src/devicedatastream.js | 86 ++++++++++++++++++-
gnome/src/extension.js | 6 +-
gnome/src/virtualmonitorsactor.js | 80 +++++++++++++----
.../com.xronlinux.BreezyDesktop.gschema.xml | 9 ++
ui/src/window.py | 9 ++
5 files changed, 170 insertions(+), 20 deletions(-)
diff --git a/gnome/src/devicedatastream.js b/gnome/src/devicedatastream.js
index 161e9ff..03cd97f 100644
--- a/gnome/src/devicedatastream.js
+++ b/gnome/src/devicedatastream.js
@@ -51,6 +51,28 @@ function checkParityByte(dataView) {
return parityByte === parity;
}
+const COUNTER_MAX = 150;
+function nextDebugIMUQuaternion(counter) {
+ const angle = counter / COUNTER_MAX * 2 * Math.PI;
+ const yaw = 10 * Math.PI / 180 * Math.cos(angle);
+ const roll = 0;
+ const pitch = 10 * Math.PI / 180 * Math.sin(angle);
+
+ const cy = Math.cos(yaw * 0.5);
+ const sy = Math.sin(yaw * 0.5);
+ const cp = Math.cos(pitch * 0.5);
+ const sp = Math.sin(pitch * 0.5);
+ const cr = Math.cos(roll * 0.5);
+ const sr = Math.sin(roll * 0.5);
+
+ const w = cr * cp * cy + sr * sp * sy;
+ const x = sr * cp * cy - cr * sp * sy;
+ const y = cr * sp * cy + sr * cp * sy;
+ const z = cr * cp * sy - sr * sp * cy;
+
+ return [x, y, z, w];
+}
+
export const DeviceDataStream = GObject.registerClass({
Properties: {
'breezy-desktop-running': GObject.ParamSpec.boolean(
@@ -72,6 +94,13 @@ export const DeviceDataStream = GObject.registerClass({
'IMU Snapshots',
'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
GObject.ParamFlags.READWRITE
+ ),
+ 'debug-no-device': GObject.ParamSpec.boolean(
+ 'debug-no-device',
+ 'Debug without device',
+ 'Debug mode that allows for testing with moving IMU values without a device connected',
+ GObject.ParamFlags.READWRITE,
+ false
)
}
}, class DeviceDataStream extends GObject.Object {
@@ -90,6 +119,7 @@ export const DeviceDataStream = GObject.registerClass({
stop() {
this._running = false;
+ this.device_data = null;
}
// polling is just intended to keep breezy_desktop_running current, anything needing up-to-date imu data should
@@ -104,6 +134,47 @@ export const DeviceDataStream = GObject.registerClass({
// Refresh the data from the IPC file. if keepalive_only is true, we'll only check and update breezy_desktop_running if it
// hasn't been checked within KEEPALIVE_REFRESH_INTERVAL_SEC.
refresh_data(keepalive_only = false) {
+ if (this.debug_no_device) {
+ this.was_debug_no_device = true;
+ if (!this.device_data) {
+ this.device_data = {
+ version: 1.0,
+ enabled: true,
+ imuResetState: false,
+ displayRes: [1920.0, 1080.0],
+ sbsEnabled: false,
+ displayFov: 46.0,
+ lookAheadCfg: [0.0, 0.0, 0.0, 0.0]
+ }
+ }
+
+ if (!keepalive_only) {
+ this._counter = ((this._counter ?? -1)+1)%COUNTER_MAX;
+
+ const imuDataFirst = nextDebugIMUQuaternion(this._counter);
+ const imuData = [
+ ...imuDataFirst,
+ ...imuDataFirst,
+ ...imuDataFirst,
+ 2.0, 1.0, 0.0, 0.0
+ ]
+ const imuDateMs = Date.now();
+ this.device_data.imuData = imuData;
+ this.device_data.imuDateMs = imuDateMs;
+ this.imu_snapshots = {
+ imu_data: imuData,
+ timestamp_ms: imuDateMs
+ };
+ }
+ this.breezy_desktop_running = true;
+ return;
+ } else if (this.was_debug_no_device) {
+ this.was_debug_no_device = false;
+ this.device_data = null;
+ this.breezy_desktop_running = false;
+ this.imu_snapshots = null;
+ }
+
if (this._ipc_file.query_exists(null) && (
!this.device_data?.imuData ||
!keepalive_only ||
@@ -114,9 +185,9 @@ export const DeviceDataStream = GObject.registerClass({
let buffer = new Uint8Array(data[1]).buffer;
let dataView = new DataView(buffer);
if (dataView.byteLength === DATA_VIEW_LENGTH) {
- const imuDateMs = dataViewBigUint(dataView, EPOCH_MS);
+ let imuDateMs = dataViewBigUint(dataView, EPOCH_MS);
const validKeepalive = isValidKeepAlive(toSec(imuDateMs));
- const imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA);
+ let imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA);
const imuResetState = validKeepalive && imuData[0] === 0.0 && imuData[1] === 0.0 && imuData[2] === 0.0 && imuData[3] === 1.0;
const version = dataViewUint8(dataView, VERSION);
const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive;
@@ -132,9 +203,16 @@ export const DeviceDataStream = GObject.registerClass({
imuResetState,
displayRes: dataViewUint32Array(dataView, DISPLAY_RES),
sbsEnabled,
- displayFov: dataViewFloat(dataView, DISPLAY_FOV),
+ displayFov: 44.0, // dataViewFloat(dataView, DISPLAY_FOV),
lookAheadCfg: dataViewFloatArray(dataView, LOOK_AHEAD_CFG),
};
+ } else if (keepalive_only) {
+ this.device_data = {
+ ...this.device_data,
+ imuResetState,
+ enabled,
+ sbsEnabled
+ }
}
let success = keepalive_only;
@@ -159,6 +237,8 @@ export const DeviceDataStream = GObject.registerClass({
if (data[0]) {
buffer = new Uint8Array(data[1]).buffer;
dataView = new DataView(buffer);
+ imuDateMs = dataViewBigUint(dataView, EPOCH_MS);
+ imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA);
}
}
}
diff --git a/gnome/src/extension.js b/gnome/src/extension.js
index 4d4daa5..ea8bd5c 100644
--- a/gnome/src/extension.js
+++ b/gnome/src/extension.js
@@ -79,7 +79,10 @@ export default class BreezyDesktopExtension extends Extension {
}
if (!Globals.data_stream) {
- Globals.data_stream = new DeviceDataStream();
+ Globals.data_stream = new DeviceDataStream({
+ debug_no_device: this.settings.get_boolean('debug-no-device')
+ });
+ this.settings.bind('debug-no-device', Globals.data_stream, 'debug-no-device', Gio.SettingsBindFlags.DEFAULT);
}
}
@@ -286,6 +289,7 @@ export default class BreezyDesktopExtension extends Extension {
this._cursor_manager = new CursorManager(Main.layoutManager.uiGroup, [targetMonitor, ...virtualMonitors], refreshRate);
this._cursor_manager.enable();
+ // use rgba(255, 4, 144, 1) for chroma key background
this._overlay = new St.Bin({ style: 'background-color: rgba(0, 0, 0, 1);', reactive: false, clip_to_allocation: true });
this._overlay.opacity = 255;
this._overlay.set_position(targetMonitor.x, targetMonitor.y);
diff --git a/gnome/src/virtualmonitorsactor.js b/gnome/src/virtualmonitorsactor.js
index d95f25e..1addf63 100644
--- a/gnome/src/virtualmonitorsactor.js
+++ b/gnome/src/virtualmonitorsactor.js
@@ -66,7 +66,7 @@ function degreesToRadians(degrees) {
}
/***
- * @returns {Object} - containing `start`, `center`, and `end` radians for rotating the given monitor
+ * @returns {Object} - containing `begin`, `center`, and `end` radians for rotating the given monitor
*/
function monitorWrap(cachedMonitorWrap, radiusPixels, monitorSpacingPixels, monitorBeginPixel, monitorLengthPixels) {
let closestWrap = cachedMonitorWrap.reduce((previous, current) => {
@@ -125,17 +125,33 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch
// distance to a horizontal edge is the hypothenuse of the triangle where the opposite side is half the width of the reference fov screen
const edgeRadius = fovDetails.widthPixels / 2 / Math.sin(fovHorizontalRadians / 2);
+ const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels;
cachedMonitorWrap.push({ pixel: 0, radians: -fovHorizontalRadians / 2 });
monitorDetailsList.forEach(monitorDetails => {
- const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorSpacing * fovDetails.widthPixels, monitorDetails.x, monitorDetails.width);
- const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.width / 2, 2))
+ const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorSpacingPixels, monitorDetails.x, monitorDetails.width);
+ const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.width / 2, 2));
+ const upTopPixels = monitorDetails.y + (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels;
+ const upCenterPixels = upTopPixels + monitorDetails.height / 2 - fovDetails.heightPixels / 2;
monitorPlacements.push({
topLeftNoRotate: [
monitorCenterRadius,
+
+ // west stays aligned with (0, 0), will apply rotationAngleRadians value during rendering
-(monitorDetails.width - fovDetails.widthPixels) / 2,
- -monitorDetails.y
+
+ // up is flat when wrapping horizontally, apply it here as a constant, not touched by rendering
+ -upTopPixels
+ ],
+ centerNoRotate: [
+ monitorCenterRadius,
+
+ // west centered about the FOV center
+ 0,
+
+ // up is flat when wrapping horizontally
+ -upCenterPixels
],
center: [
// north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
@@ -145,7 +161,7 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch
-monitorCenterRadius * Math.sin(monitorWrapDetails.center),
// up is flat when wrapping horizontally
- -(monitorDetails.y + monitorDetails.height / 2 - fovDetails.heightPixels / 2)
+ -upCenterPixels
],
rotationAngleRadians: {
x: 0,
@@ -158,24 +174,40 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch
// distance to a vertical edge is the hypothenuse of the triangle where the opposite side is half the height of the reference fov screen
const edgeRadius = fovDetails.heightPixels / 2 / Math.sin(fovVerticalRadians / 2);
+ const monitorSpacingPixels = monitorSpacing * fovDetails.heightPixels;
cachedMonitorWrap.push({ pixel: 0, radians: -fovVerticalRadians / 2 });
monitorDetailsList.forEach(monitorDetails => {
- const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorSpacing * fovDetails.heightPixels, monitorDetails.y, monitorDetails.height);
- const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.height / 2, 2)) ;
+ const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorSpacingPixels, monitorDetails.y, monitorDetails.height);
+ const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.height / 2, 2));
+ const westPixels = monitorDetails.x + (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels;
+ const westCenterPixels = westPixels + monitorDetails.width / 2 - fovDetails.widthPixels / 2;
monitorPlacements.push({
topLeftNoRotate: [
monitorCenterRadius,
- monitorDetails.x,
+
+ // west is flat when wrapping vertically, apply it here as a constant, not touched by rendering
+ westPixels,
+
+ // up stays aligned with (0, 0), will apply rotationAngleRadians value during rendering
(monitorDetails.height - fovDetails.heightPixels) / 2
],
+ centerNoRotate: [
+ monitorCenterRadius,
+
+ // west is flat when wrapping horizontally
+ westCenterPixels,
+
+ // west centered about the FOV center
+ 0
+ ],
center: [
// north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
// west is flat when wrapping vertically
- -(monitorDetails.x + monitorDetails.width / 2 - fovDetails.widthPixels / 2),
+ -westCenterPixels,
// up is opposite where radius is the hypotenuse, using monitorWrapDetails.center as the radians
-monitorCenterRadius * Math.sin(monitorWrapDetails.center)
@@ -187,18 +219,29 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch
});
});
} else {
+ const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels;
+
// monitors make a flat wall in front of us, no wrapping
monitorDetailsList.forEach(monitorDetails => {
+ const upPixels = monitorDetails.y + (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels;
+ const westPixels = monitorDetails.x + (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels;
+ const westCenterPixels = westPixels + monitorDetails.width / 2 - fovDetails.widthPixels / 2;
+ const upCenterPixels = upPixels + monitorDetails.height / 2 - fovDetails.heightPixels / 2;
monitorPlacements.push({
topLeftNoRotate: [
centerRadius,
- monitorDetails.x,
- -monitorDetails.y
+ westPixels,
+ -upPixels
+ ],
+ centerNoRotate: [
+ centerRadius,
+ westCenterPixels,
+ -upCenterPixels
],
center: [
centerRadius,
- -(monitorDetails.x + monitorDetails.width / 2 - fovDetails.widthPixels / 2),
- -(monitorDetails.y + monitorDetails.height / 2 - fovDetails.heightPixels / 2)
+ -westCenterPixels,
+ -upCenterPixels
],
rotationAngleRadians: {
x: 0,
@@ -387,12 +430,17 @@ export const VirtualMonitorEffect = GObject.registerClass({
_update_display_position_uniforms() {
// this is in NWU coordinates
- const noRotationVector = this.monitor_placements[this.monitor_index].topLeftNoRotate;
- Globals.logger.log_debug(`\t\t\tMonitor ${this.monitor_index} vectors: ${JSON.stringify(this.monitor_placements[this.monitor_index])}`);
+ const monitorPlacement = this.monitor_placements[this.monitor_index];
+ // Globals.logger.log_debug(`\t\t\tMonitor ${this.monitor_index} vectors: ${JSON.stringify(monitorPlacement)}`);
+
+ // use the center vector with the distance applied to determine how much to move each coordinate, so they all move uniformly
+ const inverseAppliedDistance = 1.0 - this._current_display_distance / this.display_distance_default;
+ const distanceDelta = monitorPlacement.centerNoRotate.map(coord => coord * inverseAppliedDistance);
+ const noRotationVector = monitorPlacement.topLeftNoRotate.map((coord, index) => coord - distanceDelta[index]);
// convert to CoGL's east-down-south coordinates and apply display distance
this.set_uniform_float(this.get_uniform_location("u_display_position"), 3,
- [-noRotationVector[1], -noRotationVector[2], this._current_display_distance / this.display_distance_default * -noRotationVector[0]]);
+ [-noRotationVector[1], -noRotationVector[2], -noRotationVector[0]]);
const rotation_radians = this.monitor_placements[this.monitor_index].rotationAngleRadians;
this.set_uniform_float(this.get_uniform_location("u_rotation_x_radians"), 1, [rotation_radians.x]);
diff --git a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
index 429d0e9..a1a2fea 100644
--- a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
+++ b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
@@ -208,6 +208,15 @@
Log debug messages
+
+
+ false
+
+ Debug no device
+
+ Debug no device
+
+
""
diff --git a/ui/src/window.py b/ui/src/window.py
index 502f7dc..03e3cb1 100644
--- a/ui/src/window.py
+++ b/ui/src/window.py
@@ -22,6 +22,7 @@ from .extensionsmanager import ExtensionsManager
from .license import BREEZY_GNOME_FEATURES
from .licensedialog import LicenseDialog
from .statemanager import StateManager
+from .settingsmanager import SettingsManager
from .connecteddevice import ConnectedDevice
from .failedverification import FailedVerification
from .nodevice import NoDevice
@@ -45,11 +46,13 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
self._skip_verification = skip_verification
+ self.settings = SettingsManager.get_instance().settings
self.state_manager = StateManager.get_instance()
self.state_manager.connect('device-update', self._handle_state_update)
self.state_manager.connect('notify::license-action-needed', self._handle_state_update)
self.state_manager.connect('notify::license-present', self._handle_state_update)
self.state_manager.connect('notify::enabled-features-list', self._handle_state_update)
+ self.settings.connect('changed::debug-no-device', self._handle_settings_update)
self.connected_device = ConnectedDevice()
self.failed_verification = FailedVerification()
@@ -67,6 +70,9 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
self.connect("destroy", self._on_window_destroy)
+ def _handle_settings_update(self, settings_manager, key):
+ self._handle_state_update(self.state_manager, None)
+
def _handle_state_update(self, state_manager, val):
GLib.idle_add(self._handle_state_update_gui, state_manager)
@@ -83,6 +89,9 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
self.main_content.append(self.failed_verification)
elif not ExtensionsManager.get_instance().is_installed():
self.main_content.append(self.no_extension)
+ elif self.settings.get_boolean('debug-no-device'):
+ self.main_content.append(self.connected_device)
+ self.connected_device.set_device_name('Fake device')
elif not self.state_manager.driver_running:
self.main_content.append(self.no_driver)
elif not self.state_manager.license_present: