Finalize Vulkan and GNOME 6DoF (#144)

* Pull in the latest driver with VITURE SDK and 6DoF improvements
* Add SteamOS vulkan build artifact
* Unifies "display size" setting
This commit is contained in:
Wayne Heaney 2026-01-08 10:30:00 -08:00 committed by GitHub
parent a0388d074c
commit f4fecdc3e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 447 additions and 181 deletions

View File

@ -1 +1 @@
2.5.3
2.6.2

View File

@ -59,6 +59,10 @@ cp -r $VULKAN_DIR/config $PACKAGE_DIR
# build XR driver
XR_DRIVER_BINARY=$XR_DRIVER_DIR/out/xrDriver-$ARCH.tar.gz
if [ -n "$STEAMOS" ]; then
XR_DRIVER_BINARY=$XR_DRIVER_DIR/out/xrDriver-$ARCH.steamos.tar.gz
BUILD_FILE_NAME=breezyVulkan-$ARCH.steamos.tar.gz
fi
if [ ! -e "$XR_DRIVER_BINARY" ] || [ "$1" == "--rebuild-driver" ]; then
# if a file exists at custom_banner_config.yml, copy it to the xrealAirLinuxDriver directory
@ -70,7 +74,10 @@ if [ ! -e "$XR_DRIVER_BINARY" ] || [ "$1" == "--rebuild-driver" ]; then
# strange issue where the base library produces a .so file if the build is not cleaned
rm -rf build/
if [ -n "${STEAMOS:-}" ]; then
export STEAMOS
fi
docker-build/init.sh
docker-build/run-build.sh $ARCH
popd

View File

@ -35,6 +35,7 @@ export default class BreezyDesktopExtension extends Extension {
this._data_stream_bindings = [];
this._show_banner_connection = null;
this._distance_connection = null;
this._display_size_connection = null;
this._focused_monitor_distance_connection = null;
this._follow_threshold_connection = null;
this._breezy_desktop_running_connection = null;
@ -228,6 +229,13 @@ export default class BreezyDesktopExtension extends Extension {
this._virtual_displays_overlay.set_position(targetMonitor.x, targetMonitor.y);
this._virtual_displays_overlay.set_size(targetMonitor.width, targetMonitor.height);
const state = this._read_state();
const pose_has_position = state['connected_device_pose_has_position'] === 'true';
Globals.logger.log_debug(
`connected_device_pose_has_position=${pose_has_position}`
);
Globals.data_stream.refresh_data();
this._virtual_displays_actor = new VirtualDisplaysActor({
width: targetMonitor.width,
@ -241,12 +249,14 @@ export default class BreezyDesktopExtension extends Extension {
viewport_offset_x: this.settings.get_double('viewport-offset-x'),
viewport_offset_y: this.settings.get_double('viewport-offset-y'),
display_distance: this.settings.get_double('display-distance'),
display_size: this.settings.get_double('display-size'),
toggle_display_distance_start: this.settings.get_double('toggle-display-distance-start'),
toggle_display_distance_end: this.settings.get_double('toggle-display-distance-end'),
framerate_cap: this.settings.get_double('framerate-cap'),
imu_snapshots: Globals.data_stream.imu_snapshots,
show_banner: Globals.data_stream.show_banner,
custom_banner_enabled: Globals.data_stream.custom_banner_enabled
custom_banner_enabled: Globals.data_stream.custom_banner_enabled,
pose_has_position
});
this._virtual_displays_overlay.set_child(this._virtual_displays_actor);
@ -299,6 +309,9 @@ export default class BreezyDesktopExtension extends Extension {
);
this._distance_connection = this.settings.connect('changed::display-distance', this._update_display_distance.bind(this));
this._toggle_distance_start_connection = this.settings.connect('changed::toggle-display-distance-start', this._update_display_distance.bind(this));
this._toggle_distance_end_connection = this.settings.connect('changed::toggle-display-distance-end', this._update_display_distance.bind(this));
this._display_size_connection = this.settings.connect('changed::display-size', this._update_display_distance.bind(this));
this._focused_monitor_distance_connection =
this._virtual_displays_actor.connect('notify::focused-monitor-details', this._update_display_distance.bind(this));
this._follow_threshold_connection = this.settings.connect('changed::follow-threshold', this._update_follow_threshold.bind(this));
@ -398,17 +411,23 @@ export default class BreezyDesktopExtension extends Extension {
}
_update_display_distance(object, event) {
const value = this.settings.get_double('display-distance');
Globals.logger.log_debug(`BreezyDesktopExtension _update_display_distance ${value}`);
if (value !== undefined) {
let focusedMonitorSizeAdjustment = 1.0;
const distance = this.settings.get_double('display-distance');
const size = this.settings.get_double('display-size');
Globals.logger.log_debug(`BreezyDesktopExtension _update_display_distance ${distance} ${size}`);
if (distance !== undefined && size !== undefined) {
const defaultDistance = Math.max(
distance,
this.settings.get_double('toggle-display-distance-start'),
this.settings.get_double('toggle-display-distance-end')
);
let focusedMonitorSizeAdjustment = size * defaultDistance;
if (this._virtual_displays_actor?.focused_monitor_details && this._target_monitor) {
const fovMonitor = this._target_monitor.monitor;
const focusedMonitor = this._virtual_displays_actor.focused_monitor_details;
focusedMonitorSizeAdjustment =
focusedMonitorSizeAdjustment *=
Math.max(focusedMonitor.width / fovMonitor.width, focusedMonitor.height / fovMonitor.height);
}
this._write_control('breezy_desktop_display_distance', value / focusedMonitorSizeAdjustment);
this._write_control('breezy_desktop_display_distance', distance / focusedMonitorSizeAdjustment);
}
}
@ -586,6 +605,18 @@ export default class BreezyDesktopExtension extends Extension {
this.settings.disconnect(this._distance_connection);
this._distance_connection = null;
}
if (this._toggle_distance_start_connection) {
this.settings.disconnect(this._toggle_distance_start_connection);
this._toggle_distance_start_connection = null;
}
if (this._toggle_distance_end_connection) {
this.settings.disconnect(this._toggle_distance_end_connection);
this._toggle_distance_end_connection = null;
}
if (this._display_size_connection) {
this.settings.disconnect(this._display_size_connection);
this._display_size_connection = null;
}
if (this._focused_monitor_distance_connection) {
this._virtual_displays_actor.disconnect(this._focused_monitor_distance_connection);
this._focused_monitor_distance_connection = null;

View File

@ -4,18 +4,23 @@ export function degreeToRadian(degree) {
// FOV in radians is spherical, so doesn't follow Pythagoras' theorem
export function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) {
// first convert from a spherical FOV to a diagonal FOV on a flat plane at a generic distance of 1.0
const flatDiagonalFOV = 2 * Math.tan(diagonalFOVRadians / 2);
// first convert from a spherical FOV to a diagonal FOV on a flat plane at a unit distance of 1.0
const diagonalLengthUnitDistance = 2 * Math.tan(diagonalFOVRadians / 2);
// then convert to flat plane horizontal and vertical FOVs
const flatVerticalFOV = flatDiagonalFOV / Math.sqrt(1 + aspectRatio * aspectRatio);
const flatHorizontalFOV = flatVerticalFOV * aspectRatio;
const heightUnitDistance = diagonalLengthUnitDistance / Math.sqrt(1 + aspectRatio * aspectRatio);
const widthUnitDistance = heightUnitDistance * aspectRatio;
// then convert back to spherical FOV
return {
diagonal: diagonalFOVRadians,
horizontal: 2 * Math.atan(flatHorizontalFOV / 2),
vertical: 2 * Math.atan(flatVerticalFOV / 2)
// then convert back to spherical FOV
diagonalRadians: diagonalFOVRadians,
horizontalRadians: 2 * Math.atan(widthUnitDistance / 2),
verticalRadians: 2 * Math.atan(heightUnitDistance / 2),
// flat values are relative to a unit distance of 1.0
diagonalLengthUnitDistance,
widthUnitDistance,
heightUnitDistance
}
}
@ -31,7 +36,10 @@ export const fovConversionFns = {
fovEdgeToScreenCenterDistance: (edgeDistance, screenLength) => Math.sqrt(Math.pow(edgeDistance, 2) - Math.pow(screenLength / 2, 2)),
lengthToRadians: (fovRadians, fovLength, screenEdgeDistance, toLength) => Math.asin(toLength / 2 / screenEdgeDistance) * 2,
angleToLength: (fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) => {
return toAngleOpposite / toAngleAdjacent * screenDistance
return toAngleOpposite / toAngleAdjacent * screenDistance;
},
fovRadiansAtDistance: (fovRadians, unitLength, newScreenDistance) => {
return 2 * Math.atan(unitLength / 2 / newScreenDistance);
},
radiansToSegments: (screenRadians) => 1
},
@ -42,6 +50,7 @@ export const fovConversionFns = {
fovEdgeToScreenCenterDistance: (edgeDistance, screenLength) => edgeDistance,
lengthToRadians: (fovRadians, fovLength, screenEdgeDistance, toLength) => fovRadians / fovLength * toLength,
angleToLength: (fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) => fovLength / fovRadians * Math.atan2(toAngleOpposite, toAngleAdjacent),
fovRadiansAtDistance: (fovRadians, unitLength, newScreenDistance) => fovRadians / newScreenDistance,
radiansToSegments: (screenRadians) => Math.ceil(screenRadians * segmentsPerRadian)
}
}
@ -59,7 +68,11 @@ export const applyQuaternionToVector = (vector, quaternion) => {
];
}
export const vectorMagnitude = (vector) => {
return Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
}
export const normalizeVector = (vector) => {
const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
const length = vectorMagnitude(vector);
return [vector[0] / length, vector[1] / length, vector[2] / length];
}

View File

@ -28,33 +28,34 @@ function lookAheadMS(imuDateMs, lookAheadCfg, override) {
// Create a mesh of vertices in a pattern suitable for TRIANGLE_STRIP
function createVertexMesh(fovDetails, monitorDetails, positionVectorNWU) {
let fovConversions = fovDetails.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat;
const sideEdgeDistancePixels = fovConversions.centerToFovEdgeDistance(
let horizontalWrap = fovDetails.monitorWrappingScheme === 'horizontal';
const horizontalConversions = fovDetails.curvedDisplay && horizontalWrap ? fovConversionFns.curved : fovConversionFns.flat;
const sideEdgeDistancePixels = horizontalConversions.centerToFovEdgeDistance(
fovDetails.completeScreenDistancePixels,
fovDetails.widthPixels
fovDetails.sizeAdjustedWidthPixels
);
const horizontalRadians = fovConversions.lengthToRadians(
fovDetails.defaultDistanceHorizontalRadians,
const horizontalRadians = horizontalConversions.lengthToRadians(
fovDetails.defaultDistanceHorizontalRadians,
fovDetails.widthPixels,
sideEdgeDistancePixels,
monitorDetails.width
);
const topEdgeDistancePixels = fovConversions.centerToFovEdgeDistance(
let verticalWrap = fovDetails.monitorWrappingScheme === 'vertical';
const verticalConversions = fovDetails.curvedDisplay && verticalWrap ? fovConversionFns.curved : fovConversionFns.flat;
const topEdgeDistancePixels = verticalConversions.centerToFovEdgeDistance(
fovDetails.completeScreenDistancePixels,
fovDetails.heightPixels
fovDetails.sizeAdjustedHeightPixels
);
const verticalRadians = fovConversions.lengthToRadians(
const verticalRadians = verticalConversions.lengthToRadians(
fovDetails.defaultDistanceVerticalRadians,
fovDetails.heightPixels,
topEdgeDistancePixels,
monitorDetails.height
);
let horizontalWrap = fovDetails.monitorWrappingScheme === 'horizontal';
let verticalWrap = fovDetails.monitorWrappingScheme === 'vertical';
const xSegments = horizontalWrap ? fovConversions.radiansToSegments(horizontalRadians) : 1;
const ySegments = verticalWrap ? fovConversions.radiansToSegments(verticalRadians) : 1;
const xSegments = horizontalConversions.radiansToSegments(horizontalRadians);
const ySegments = verticalConversions.radiansToSegments(verticalRadians);
const texXLeft = 0;
const texYTop = 0;
@ -148,6 +149,13 @@ export const VirtualDisplayEffect = GObject.registerClass({
'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
GObject.ParamFlags.READWRITE
),
'pose-has-position': GObject.ParamSpec.boolean(
'pose-has-position',
'Pose Has Position',
'Whether the IMU snapshots contain pose data',
GObject.ParamFlags.READWRITE,
false
),
'smooth-follow-enabled': GObject.ParamSpec.boolean(
'smooth-follow-enabled',
'Smooth follow enabled',
@ -176,12 +184,21 @@ export const VirtualDisplayEffect = GObject.registerClass({
GObject.ParamFlags.READWRITE,
true
),
'display-size': GObject.ParamSpec.double(
'display-size',
'Display size',
'Size of the display',
GObject.ParamFlags.READWRITE,
0.1,
2.5,
1.0
),
'display-distance': GObject.ParamSpec.double(
'display-distance',
'Display Distance',
'Distance of the display from the camera',
GObject.ParamFlags.READWRITE,
0.0,
0.1,
2.5,
1.0
),
@ -190,7 +207,7 @@ export const VirtualDisplayEffect = GObject.registerClass({
'Display distance default',
'Distance to use when not explicitly set, or when reset',
GObject.ParamFlags.READWRITE,
0.2,
0.1,
2.5,
1.0
),
@ -253,6 +270,8 @@ export const VirtualDisplayEffect = GObject.registerClass({
this._use_smooth_follow_origin = false;
this.connect('notify::display-distance', this._update_display_distance.bind(this));
this.connect('notify::display-distance-default', this._update_display_distance.bind(this));
this.connect('notify::display-size', this._update_display_position.bind(this));
this.connect('notify::focused-monitor-index', this._update_display_distance.bind(this));
this.connect('notify::monitor-placements', this._update_display_position.bind(this));
this.connect('notify::show-banner', this._handle_banner_update.bind(this));
@ -389,7 +408,11 @@ export const VirtualDisplayEffect = GObject.registerClass({
finalPositionVector = noRotationVector.map(coord => coord * inverse_follow_ease);
finalPositionVector[0] = noRotationVector[0];
}
this._vertices = createVertexMesh(this.fov_details, this.monitor_details, finalPositionVector);
const resizedMonitorDetails = {
width: this.monitor_details.width * this.fov_details.distanceAdjustedSize,
height: this.monitor_details.height * this.fov_details.distanceAdjustedSize
};
this._vertices = createVertexMesh(this.fov_details, resizedMonitorDetails, finalPositionVector);
const rotation_radians = this.monitor_placements[this.monitor_index].rotationAngleRadians;
if (this._initialized) {
@ -402,8 +425,8 @@ export const VirtualDisplayEffect = GObject.registerClass({
this.set_uniform_float(this.get_uniform_location("u_show_banner"), 1, [this.show_banner ? 1.0 : 0.0]);
}
perspective(fovHorizontalRadians, aspect, near, far) {
const f = 1.0 / Math.tan(fovHorizontalRadians / 2.0);
perspective(widthUnitDistance, aspect, near, far) {
const f = 2.0 / widthUnitDistance;
const range = far - near;
return [
@ -458,11 +481,11 @@ export const VirtualDisplayEffect = GObject.registerClass({
return vec3(v.x * c + v.z * s, v.y, v.z * c - v.x * s);
}
vec4 nwuToESU(vec4 v) {
vec4 nwuToEUS(vec4 v) {
return vec4(-v.y, v.z, -v.x, v.w);
}
vec3 nwuToESU(vec3 v) {
vec3 nwuToEUS(vec3 v) {
return vec3(-v.y, v.z, -v.x);
}
@ -492,15 +515,15 @@ export const VirtualDisplayEffect = GObject.registerClass({
if (!u_show_banner) {
float aspect_ratio = u_display_resolution.x / u_display_resolution.y;
vec4 quat_t0 = nwuToESU(quatConjugate(u_pose_orientation[0]));
vec3 position_vector = applyQuaternionToVector(nwuToESU(u_pose_position), quat_t0);
vec3 final_lens_position = u_lens_vector + position_vector;
vec4 quat_t0 = nwuToEUS(quatConjugate(u_pose_orientation[0]));
vec3 position_vector = applyQuaternionToVector(nwuToEUS(u_pose_position), quat_t0);
vec3 final_lens_position = nwuToEUS(u_lens_vector) + position_vector;
vec3 complete_vector = applyXRotationToVector(world_pos.xyz, u_rotation_x_radians);
complete_vector = applyYRotationToVector(complete_vector, u_rotation_y_radians);
vec3 rotated_vector_t0 = applyQuaternionToVector(complete_vector, quat_t0);
vec3 rotated_vector_t1 = applyQuaternionToVector(complete_vector, nwuToESU(quatConjugate(u_pose_orientation[1])));
vec3 rotated_vector_t1 = applyQuaternionToVector(complete_vector, nwuToEUS(quatConjugate(u_pose_orientation[1])));
float delta_time_t0 = u_pose_orientation[3][0] - u_pose_orientation[3][1];
// how quickly the vertex is moving relative to the camera
@ -545,20 +568,20 @@ export const VirtualDisplayEffect = GObject.registerClass({
this._initialized = true;
const aspect = this.target_monitor.width / this.target_monitor.height;
const fovRadians = diagonalToCrossFOVs(degreeToRadian(Globals.data_stream.device_data.displayFov), aspect);
const fovLengths = diagonalToCrossFOVs(degreeToRadian(Globals.data_stream.device_data.displayFov), aspect);
const projection_matrix = this.perspective(
fovRadians.horizontal,
fovLengths.widthUnitDistance,
aspect,
1.0,
10000.0
);
this.set_uniform_matrix(this.get_uniform_location("u_projection_matrix"), false, 4, projection_matrix);
this.set_uniform_float(this.get_uniform_location("u_fov_vertical_radians"), 1, [fovRadians.vertical]);
this.set_uniform_float(this.get_uniform_location("u_fov_vertical_radians"), 1, [fovLengths.verticalRadians]);
this.set_uniform_float(this.get_uniform_location("u_display_resolution"), 2, [this.target_monitor.width, this.target_monitor.height]);
this.set_uniform_float(this.get_uniform_location("u_look_ahead_cfg"), 4, Globals.data_stream.device_data.lookAheadCfg);
this.set_uniform_float(this.get_uniform_location("u_actor_to_display_ratios"), 2, this.actor_to_display_ratios);
this.set_uniform_float(this.get_uniform_location("u_actor_to_display_offsets"), 2, this.actor_to_display_offsets);
this.set_uniform_float(this.get_uniform_location("u_lens_vector"), 3, this.lens_vector);
this.set_uniform_float(this.get_uniform_location("u_lens_vector"), 3, this.pose_has_position ? [0.0, 0.0, 0.0] : this.lens_vector);
this._update_display_position();
this._handle_banner_update();
}
@ -571,7 +594,12 @@ export const VirtualDisplayEffect = GObject.registerClass({
this.set_uniform_float(this.get_uniform_location('u_look_ahead_ms'), 1, [0.0]);
lookAheadSet = true;
}
const posePositionPixels = this.imu_snapshots.pose_position.map(coord => coord * this.fov_details.completeScreenDistancePixels);
let posePositionPixels = [0.0, 0.0, 0.0];
if (this.pose_has_position) {
posePositionPixels = this.imu_snapshots.pose_position.map((coord, index) => {
return coord * this.fov_details.fullScreenDistancePixels + this.lens_vector[index];
});
}
this.set_uniform_matrix(this.get_uniform_location("u_pose_orientation"), false, 4, this.imu_snapshots.pose_orientation);
this.set_uniform_float(this.get_uniform_location("u_pose_position"), 3, posePositionPixels);
} else {

View File

@ -7,7 +7,7 @@ import Shell from 'gi://Shell';
import St from 'gi://St';
import { VirtualDisplayEffect, SMOOTH_FOLLOW_SLERP_TIMELINE_MS } from './virtualdisplayeffect.js';
import { applyQuaternionToVector, degreeToRadian, diagonalToCrossFOVs, fovConversionFns, normalizeVector } from './math.js';
import { applyQuaternionToVector, degreeToRadian, diagonalToCrossFOVs, fovConversionFns, vectorMagnitude } from './math.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
@ -19,38 +19,40 @@ const FOCUS_THRESHOLD = 0.95 / 2.0;
// if we leave the monitor with some margin, unfocus even if no other monitor is in focus
const UNFOCUS_THRESHOLD = 1.1 / 2.0;
// returns how far the look vector is from the center of the monitor, as a percentage of the monitor's width
// returns how far the look vector is from the center of the monitor, as a percentage of the monitor's dimensions
function getMonitorDistance(fovDetails, lookUpPixels, lookWestPixels, monitorVector, monitorDetails, upAngleToLength, westAngleToLength) {
const monitorAspectRatio = monitorDetails.width / monitorDetails.height;
// weight the up distance by the aspect ratio
// since the monitor vector has been modified to be relative to the lens position, we need to calculate its distance from the lens
// we need to adjust all angle-based lengths based on new vector distance
const monitorDistance = vectorMagnitude(monitorVector);
const distanceAdjustment = monitorDistance / fovDetails.completeScreenDistancePixels;
const vectorUpPixels = upAngleToLength(
fovDetails.defaultDistanceVerticalRadians,
fovDetails.heightPixels,
fovDetails.completeScreenDistancePixels,
monitorDistance,
monitorVector[2],
monitorVector[0]
);
const upDeltaPixels = (lookUpPixels - vectorUpPixels) * monitorAspectRatio;
) * distanceAdjustment;
const upPercentage = Math.abs(lookUpPixels * distanceAdjustment - vectorUpPixels) / monitorDetails.height;
const vectorWestPixels = westAngleToLength(
fovDetails.defaultDistanceHorizontalRadians,
fovDetails.widthPixels,
fovDetails.completeScreenDistancePixels,
monitorDistance,
monitorVector[1],
monitorVector[0]
);
const westDeltaPixels = lookWestPixels - vectorWestPixels;
const totalDeltaPixels = Math.sqrt(upDeltaPixels * upDeltaPixels + westDeltaPixels * westDeltaPixels);
) * distanceAdjustment;
const westPercentage = Math.abs(lookWestPixels * distanceAdjustment - vectorWestPixels) / monitorDetails.width;
// threshold is a percentage of width, and height was already properly weighted
return totalDeltaPixels / monitorDetails.width;
// how close we are to any edge is the largest of the two percentages
return Math.max(upPercentage, westPercentage);
}
/**
* Find the vector in the array that's closest to the quaternion rotation
*
* @param {number[]} quaternion - Reference quaternion [x, y, z, w]
* @param {number[]} position - Reference position [x, y, z] in NWU space
* @param {number[][]} monitorVectors - Array of monitor vectors [x, y, z] to search from
* @param {number} currentFocusedIndex - Index of the currently focused monitor
* @param {number} focusedMonitorDistance - Distance to the focused monitor, < 1.0 if zoomed in
@ -59,7 +61,9 @@ function getMonitorDistance(fovDetails, lookUpPixels, lookWestPixels, monitorVec
* @param {Object[]} monitorsDetails - Contains x, y, width, height (coordinates from top-left) for each monitor
* @returns {number} Index of the closest vector, if it surpasses the previous closest index by a certain margin, otherwise the previous index
*/
function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, focusedMonitorDistance, smoothFollowEnabled, fovDetails, monitorsDetails) {
function findFocusedMonitor(quaternion, position, monitorVectors, currentFocusedIndex, focusedMonitorDistance, smoothFollowEnabled, fovDetails, monitorsDetails) {
if (currentFocusedIndex !== -1 && smoothFollowEnabled) return currentFocusedIndex;
const lookVector = [1.0, 0.0, 0.0]; // NWU vector pointing to the center of the screen
const rotatedLookVector = applyQuaternionToVector(lookVector, quaternion);
@ -82,8 +86,13 @@ function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, foc
rotatedLookVector[0]
);
let closestIndex = -1;
let closestDistance = Infinity;
function vectorRelativeToLensPosition(vector) {
return [
vector[0] - position[0],
vector[1] - position[1],
vector[2] - position[2]
]
}
// the currently focused monitor is the most likely to be the closest, check it first and exit early if it is
if (currentFocusedIndex !== -1) {
@ -91,15 +100,18 @@ function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, foc
fovDetails,
lookUpPixels,
lookWestPixels,
monitorVectors[currentFocusedIndex],
vectorRelativeToLensPosition(monitorVectors[currentFocusedIndex]),
monitorsDetails[currentFocusedIndex],
upConversionFns.angleToLength,
westConversionFns.angleToLength
) * focusedMonitorDistance;
if (smoothFollowEnabled || focusedDistance < UNFOCUS_THRESHOLD) return currentFocusedIndex;
if (focusedDistance < UNFOCUS_THRESHOLD) return currentFocusedIndex;
}
let closestIndex = -1;
let closestDistance = Infinity;
// find the vector closest to the rotated look vector
monitorVectors.forEach((monitorVector, index) => {
if (index === currentFocusedIndex) return;
@ -108,7 +120,7 @@ function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, foc
fovDetails,
lookUpPixels,
lookWestPixels,
monitorVector,
vectorRelativeToLensPosition(monitorVector),
monitorsDetails[index],
upConversionFns.angleToLength,
westConversionFns.angleToLength
@ -208,8 +220,10 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
if (fovDetails.monitorWrappingScheme === 'horizontal') {
// monitors wrap around us horizontally
const sideEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.widthPixels);
const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels;
const sideEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.sizeAdjustedWidthPixels);
const monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedWidthPixels;
// targetWidth is assumed to aleady be size adjusted
const lengthToRadianFn = (targetWidth) => conversionFns.lengthToRadians(
fovDetails.defaultDistanceHorizontalRadians,
fovDetails.widthPixels,
@ -217,14 +231,14 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
targetWidth
);
cachedMonitorRadians[0] = -fovDetails.defaultDistanceHorizontalRadians / 2;
cachedMonitorRadians[0] = -lengthToRadianFn(fovDetails.sizeAdjustedWidthPixels) / 2;
horizontalMonitorSort(monitorDetailsList).forEach(({monitorDetails, originalIndex}) => {
const monitorWrapDetails = monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorDetails.x, monitorDetails.width, lengthToRadianFn);
const monitorCenterRadius = conversionFns.fovEdgeToScreenCenterDistance(sideEdgeRadius, monitorDetails.width);
const upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels;
const upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.sizeAdjustedHeightPixels) * monitorSpacingPixels;
// offset for aligning this monitor's center with the fov-sized viewport's center
const upCenterOffsetPixels = (monitorDetails.height - fovDetails.heightPixels) / 2;
const upCenterOffsetPixels = (monitorDetails.height - fovDetails.sizeAdjustedHeightPixels) / 2;
// this is where our monitor's center is in relation to an fov-sized viewport centered about (0, 0)
const upCenterPixels = upTopPixels - upCenterOffsetPixels;
@ -240,7 +254,7 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
// up is flat when wrapping horizontally
upCenterPixels
],
centerLook: normalizeVector([
centerLook: [
// north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
@ -249,7 +263,7 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
// up is flat when wrapping horizontally
upCenterPixels
]),
],
rotationAngleRadians: {
x: 0,
y: -monitorWrapDetails.center
@ -259,8 +273,10 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
} else if (fovDetails.monitorWrappingScheme === 'vertical') {
// monitors wrap around us vertically
const topEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.heightPixels);
const monitorSpacingPixels = monitorSpacing * fovDetails.heightPixels;
const topEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.sizeAdjustedHeightPixels);
const monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedHeightPixels;
// targetHeight is assumed to aleady be size adjusted
const lengthToRadianFn = (targetHeight) => conversionFns.lengthToRadians(
fovDetails.defaultDistanceVerticalRadians,
fovDetails.heightPixels,
@ -268,14 +284,14 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
targetHeight
);
cachedMonitorRadians[0] = -fovDetails.defaultDistanceVerticalRadians / 2;
cachedMonitorRadians[0] = -lengthToRadianFn(fovDetails.sizeAdjustedHeightPixels) / 2;
verticalMonitorSort(monitorDetailsList).forEach(({monitorDetails, originalIndex}) => {
const monitorWrapDetails = monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorDetails.y, monitorDetails.height, lengthToRadianFn);
const monitorCenterRadius = conversionFns.fovEdgeToScreenCenterDistance(topEdgeRadius, monitorDetails.height);
const westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels;
const westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.sizeAdjustedWidthPixels) * monitorSpacingPixels;
// offset for aligning this monitor's center with the fov-sized viewport's center
const westCenterOffsetPixels = (monitorDetails.width - fovDetails.widthPixels) / 2;
const westCenterOffsetPixels = (monitorDetails.width - fovDetails.sizeAdjustedWidthPixels) / 2;
// this is where our monitor's center is in relation to an fov-sized viewport centered about (0, 0)
const westCenterPixels = westLeftPixels - westCenterOffsetPixels;
@ -291,7 +307,7 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
// up is centered about the FOV center
0
],
centerLook: normalizeVector([
centerLook: [
// north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
@ -300,7 +316,7 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
// up is opposite where radius is the hypotenuse, using monitorWrapDetails.center as the radians
-monitorCenterRadius * Math.sin(monitorWrapDetails.center)
]),
],
rotationAngleRadians: {
x: -monitorWrapDetails.center,
y: 0
@ -308,17 +324,16 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
});
});
} else {
const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels;
const monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedWidthPixels;
// monitors make a flat wall in front of us, no wrapping
monitorDetailsList.forEach((monitorDetails, index) => {
const upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels;
const westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels;
const upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.sizeAdjustedHeightPixels) * monitorSpacingPixels;
const westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.sizeAdjustedWidthPixels) * monitorSpacingPixels;
// offsets for aligning this monitor's center with the fov-sized viewport's center
const westCenterOffsetPixels = (monitorDetails.width - fovDetails.widthPixels) / 2;
const upCenterOffsetPixels = (monitorDetails.height - fovDetails.heightPixels) / 2;
const westCenterOffsetPixels = (monitorDetails.width - fovDetails.sizeAdjustedWidthPixels) / 2;
const upCenterOffsetPixels = (monitorDetails.height - fovDetails.sizeAdjustedHeightPixels) / 2;
const westCenterPixels = westLeftPixels - westCenterOffsetPixels;
const upCenterPixels = upTopPixels - upCenterOffsetPixels;
@ -329,11 +344,11 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
westCenterPixels,
upCenterPixels
],
centerLook: normalizeVector([
centerLook: [
fovDetails.completeScreenDistancePixels,
westCenterPixels,
upCenterPixels
]),
],
rotationAngleRadians: {
x: 0,
y: 0
@ -446,6 +461,13 @@ export const VirtualDisplaysActor = GObject.registerClass({
'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
GObject.ParamFlags.READWRITE
),
'pose-has-position': GObject.ParamSpec.boolean(
'pose-has-position',
'Pose Has Position',
'Whether the IMU snapshots contain pose data',
GObject.ParamFlags.READWRITE,
false
),
'curved-display': GObject.ParamSpec.boolean(
'curved-display',
'Curved Display',
@ -499,7 +521,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
'Display size',
'Size of the display',
GObject.ParamFlags.READWRITE,
0.2,
0.1,
2.5,
1.0
),
@ -515,7 +537,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
'Display Distance',
'Distance of the display from the camera',
GObject.ParamFlags.READWRITE,
0.2,
0.1,
2.5,
1.05
),
@ -537,7 +559,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
'Display distance start',
'Start distance when using the "change distance" shortcut.',
GObject.ParamFlags.READWRITE,
0.2,
0.1,
2.5,
1.05
),
@ -546,7 +568,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
'Display distance end',
'End distance when using the "change distance" shortcut.',
GObject.ParamFlags.READWRITE,
0.2,
0.1,
2.5,
1.05
),
@ -578,7 +600,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
constructor(params = {}) {
super(params);
this._all_monitors = [
this._all_monitors_unmodified = [
this.target_monitor,
...this.virtual_monitors
];
@ -649,10 +671,10 @@ export const VirtualDisplaysActor = GObject.registerClass({
this._property_connections.push(this.connect(`notify::${property}`, fn.bind(this)));
}).bind(this);
this._distance_ease_timeline = null;
notifyToFunction('toggle-display-distance-start', this._handle_display_distance_properties_change);
notifyToFunction('toggle-display-distance-end', this._handle_display_distance_properties_change);
notifyToFunction('display-distance', this._handle_display_distance_properties_change);
notifyToFunction('toggle-display-distance-start', this._handle_display_size_distance_change);
notifyToFunction('toggle-display-distance-end', this._handle_display_size_distance_change);
notifyToFunction('display-distance', this._handle_display_size_distance_change);
notifyToFunction('display-size', this._handle_display_size_distance_change);
notifyToFunction('monitor-wrapping-scheme', this._update_monitor_placements);
notifyToFunction('monitor-spacing', this._update_monitor_placements);
notifyToFunction('headset-display-as-viewport-center', this._update_monitor_placements);
@ -663,7 +685,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
notifyToFunction('custom-banner-enabled', this._handle_banner_update);
notifyToFunction('framerate-cap', this._handle_frame_rate_cap_change);
notifyToFunction('smooth-follow-enabled', this._handle_smooth_follow_enabled_change);
this._handle_display_distance_properties_change();
this._handle_display_size_distance_change();
this._handle_frame_rate_cap_change();
const actorToDisplayRatios = [
@ -680,8 +702,8 @@ export const VirtualDisplaysActor = GObject.registerClass({
];
Globals.logger.log_debug(`\t\t\tActor to display ratios: ${actorToDisplayRatios}, offsets: ${actorToDisplayOffsets}`);
this._all_monitors.forEach(((monitor, index) => {
this._all_monitors_unmodified.forEach(((monitor, index) => {
Globals.logger.log_debug(`\t\t\tMonitor ${index}: ${monitor.x}, ${monitor.y}, ${monitor.width}, ${monitor.height}`);
const containerActor = new Clutter.Actor({
@ -706,11 +728,13 @@ export const VirtualDisplaysActor = GObject.registerClass({
const effect = new VirtualDisplayEffect({
focused_monitor_index: this.focused_monitor_index,
imu_snapshots: this.imu_snapshots,
pose_has_position: this.pose_has_position,
monitor_index: index,
monitor_details: monitor,
monitor_placements: this.monitor_placements,
fov_details: this.fov_details,
target_monitor: this.target_monitor,
display_size: this.display_size,
display_distance: this.display_distance,
display_distance_default: this._display_distance_default(),
actor_to_display_ratios: actorToDisplayRatios,
@ -735,8 +759,10 @@ export const VirtualDisplaysActor = GObject.registerClass({
[
'monitor-placements',
'display-size',
'fov-details',
'imu-snapshots',
'pose-has-position',
'smooth-follow-enabled',
'smooth-follow-toggle-epoch-ms',
'focused-monitor-index',
@ -789,8 +815,13 @@ export const VirtualDisplaysActor = GObject.registerClass({
this.imu_snapshots.smooth_follow_origin.splice(0, 4) :
this.imu_snapshots.pose_orientation.splice(0, 4);
const currentPosition = this.pose_has_position ?
this.imu_snapshots.pose_position.map(coord => coord * this.fov_details.fullScreenDistancePixels) :
[0.0, 0.0, 0.0];
const focusedMonitorIndex = findFocusedMonitor(
currentOrientationQuat,
currentPosition,
this.monitor_placements.map(monitorVectors => monitorVectors.centerLook),
this.focused_monitor_index,
this.display_distance / this._display_distance_default(),
@ -827,34 +858,61 @@ export const VirtualDisplaysActor = GObject.registerClass({
this._redraw_timeline.start();
}
_size_adjusted_target_monitor() {
return this._all_monitors[0];
}
_display_distance_default() {
return Math.max(this.display_distance, this.toggle_display_distance_start, this.toggle_display_distance_end);
}
_fov_details() {
const aspect = this.target_monitor.width / this.target_monitor.height;
const fovRadians = diagonalToCrossFOVs(degreeToRadian(Globals.data_stream.device_data.displayFov), aspect);
const fovLengths = diagonalToCrossFOVs(degreeToRadian(Globals.data_stream.device_data.displayFov), aspect);
const monitorWrappingScheme = this._actual_wrap_scheme();
const defaultDistance = this._display_distance_default();
const lensDistanceComplement = 1.0 - Globals.data_stream.device_data.lensDistanceRatio;
const lensDistanceFactor = (1.0 / lensDistanceComplement) - 1.0;
const horizontalConversions = this.curved_display && monitorWrappingScheme === 'horizontal' ? fovConversionFns.curved : fovConversionFns.flat;
const verticalConversions = this.curved_display && monitorWrappingScheme === 'vertical' ? fovConversionFns.curved : fovConversionFns.flat;
// adjust FOV to a new focal point distance while keeping screens the same size
// i.e. focus from pivot point to new screen distance, adjusted from lens at unit distance
const defaultDistanceVerticalRadians = verticalConversions.fovRadiansAtDistance(
fovLengths.verticalRadians,
fovLengths.heightUnitDistance,
defaultDistance
);
const defaultDistanceHorizontalRadians = horizontalConversions.fovRadiansAtDistance(
fovLengths.horizontalRadians,
fovLengths.widthUnitDistance,
defaultDistance
);
// adjusted angles based on how far away the screens are e.g. a closer screen takes up a larger slice of our FOV
const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / this._display_distance_default());
const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / this._display_distance_default());
// distance needed for the FOV-sized monitor to fill up the screen, as measured from the lenses
const lensToUnitDistancePixels = this.target_monitor.width / fovLengths.widthUnitDistance;
// distance needed for the FOV-sized monitor to fill up the screen
const fullScreenDistance = this.target_monitor.height / 2 / Math.tan(fovRadians.vertical / 2);
const lensDistancePixels = fullScreenDistance / (1.0 - Globals.data_stream.device_data.lensDistanceRatio) - fullScreenDistance;
// distance from pivot point to lens
const lensDistancePixels = lensToUnitDistancePixels * lensDistanceFactor;
// distance of a display at the default (most zoomed out) distance, plus the lens distance constant
const lensToScreenDistance = this.target_monitor.height / 2 / Math.tan(defaultDistanceVerticalRadians / 2);
const completeScreenDistancePixels = lensToScreenDistance + lensDistancePixels;
// distance from pivot point to full screen (monitor at unit distance from lens)
const fullScreenDistancePixels = lensToUnitDistancePixels + lensDistancePixels;
// distance of a display at the default (most zoomed out) distance from the pivot point
const completeScreenDistancePixels = fullScreenDistancePixels * defaultDistance;
return {
widthPixels: this.target_monitor.width,
distanceAdjustedSize: this._distance_adjusted_size,
sizeAdjustedWidthPixels: this.target_monitor.width * this._distance_adjusted_size,
heightPixels: this.target_monitor.height,
sizeAdjustedHeightPixels: this.target_monitor.height * this._distance_adjusted_size,
defaultDistanceVerticalRadians,
defaultDistanceHorizontalRadians,
lensDistancePixels,
fullScreenDistancePixels,
completeScreenDistancePixels,
monitorWrappingScheme: this._actual_wrap_scheme(),
monitorWrappingScheme,
curvedDisplay: this.curved_display
};
}
@ -869,7 +927,8 @@ export const VirtualDisplaysActor = GObject.registerClass({
const minY = Math.min(...this._all_monitors.map(monitor => monitor.y));
const maxY = Math.max(...this._all_monitors.map(monitor => monitor.y + monitor.height));
if ((maxX - minX) / this.target_monitor.width >= (maxY - minY) / this.target_monitor.height) {
const targetMonitor = this._size_adjusted_target_monitor();
if ((maxX - minX) / targetMonitor.width >= (maxY - minY) / targetMonitor.height) {
return 'horizontal';
} else {
return 'vertical';
@ -878,27 +937,29 @@ export const VirtualDisplaysActor = GObject.registerClass({
_update_monitor_placements() {
try {
const targetMonitor = this._size_adjusted_target_monitor();
const minX = Math.min(...this._all_monitors.map(monitor => monitor.x));
const maxX = Math.max(...this._all_monitors.map(monitor => monitor.x + monitor.width));
const minY = Math.min(...this._all_monitors.map(monitor => monitor.y));
const maxY = Math.max(...this._all_monitors.map(monitor => monitor.y + monitor.height));
// the beginning edges of the viewport if it's centered on all displays
const allDisplaysCenterXBegin = (minX + maxX) / 2 - this.target_monitor.width / 2;
const allDisplaysCenterYBegin = (minY + maxY) / 2 - this.target_monitor.height / 2;
const allDisplaysCenterXBegin = (minX + maxX) / 2 - targetMonitor.width / 2;
const allDisplaysCenterYBegin = (minY + maxY) / 2 - targetMonitor.height / 2;
const viewportXBegin = this.headset_display_as_viewport_center ? this.target_monitor.x : allDisplaysCenterXBegin;
const viewportYBegin = this.headset_display_as_viewport_center ? this.target_monitor.y : allDisplaysCenterYBegin;
const viewportXBegin = this.headset_display_as_viewport_center ? targetMonitor.x : allDisplaysCenterXBegin;
const viewportYBegin = this.headset_display_as_viewport_center ? targetMonitor.y : allDisplaysCenterYBegin;
this.fov_details = this._fov_details();
this.lens_vector = [0.0, 0.0, -this.fov_details.lensDistancePixels];
this.lens_vector = [this.fov_details.lensDistancePixels, 0.0, 0.0];
this.monitor_placements = monitorsToPlacements(
this.fov_details,
// shift all monitors so they center around the viewport center, then adjusted by the offsets
this._all_monitors.map(monitor => ({
x: monitor.x - viewportXBegin - this.viewport_offset_x * this.target_monitor.width,
y: monitor.y - viewportYBegin + this.viewport_offset_y * this.target_monitor.height,
x: monitor.x - viewportXBegin - this.viewport_offset_x * targetMonitor.width,
y: monitor.y - viewportYBegin + this.viewport_offset_y * targetMonitor.height,
width: monitor.width,
height: monitor.height
})),
@ -909,10 +970,22 @@ export const VirtualDisplaysActor = GObject.registerClass({
}
}
_handle_display_distance_properties_change() {
_handle_display_size_distance_change() {
this._distance_adjusted_size = (this._display_distance_default() - Globals.data_stream.device_data.lensDistanceRatio) * this.display_size;
const distance_from_end = Math.abs(this.display_distance - this.toggle_display_distance_end);
const distance_from_start = Math.abs(this.display_distance - this.toggle_display_distance_start);
this._is_display_distance_at_end = distance_from_end < distance_from_start;
const sizeComplement = (1.0 - this._distance_adjusted_size) / 2.0;
const sizeViewportOffsetX = sizeComplement * this.target_monitor.width;
const sizeViewportOffsetY = sizeComplement * this.target_monitor.height;
this._all_monitors = this._all_monitors_unmodified.map(monitor => ({
x: monitor.x * this._distance_adjusted_size + sizeViewportOffsetX,
y: monitor.y * this._distance_adjusted_size + sizeViewportOffsetY,
width: monitor.width * this._distance_adjusted_size,
height: monitor.height * this._distance_adjusted_size
}));
this._update_monitor_placements();
}

@ -1 +1 @@
Subproject commit a643978295eca99ca5b86e5b17aef29a1b713092
Subproject commit 7cf5924771b01bccf6d8d8ab0a9a38d3aa37babb

@ -1 +1 @@
Subproject commit 40326b9ec0266352f24500f47693c06f39832509
Subproject commit e3da76189390d81fda70ced38976a2061a8711ea

View File

@ -100,6 +100,15 @@
The size of the display
</description>
</key>
<key name="units" type="s">
<default>
"cm"
</default>
<summary>Measurement units</summary>
<description>
Units to display for physical measurements: "cm" or "in"
</description>
</key>
<key name="viewport-offset-x" type="d">
<default>
0.0

@ -1 +1 @@
Subproject commit da173bd9e0392aaeb2cb68a332e5d4a20dd4dae1
Subproject commit 1655c2bb03a4e75bf2c9a9815a96b71342396fbd

View File

@ -33,6 +33,8 @@ class ConnectedDevice(Gtk.Box):
effect_enable_switch = Gtk.Template.Child()
disable_physical_displays_switch = Gtk.Template.Child()
display_zoom_on_focus_switch = Gtk.Template.Child()
display_size_scale = Gtk.Template.Child()
display_size_adjustment = Gtk.Template.Child()
follow_threshold_scale = Gtk.Template.Child()
follow_threshold_adjustment = Gtk.Template.Child()
follow_mode_switch = Gtk.Template.Child()
@ -83,6 +85,7 @@ class ConnectedDevice(Gtk.Box):
viewport_offset_x_adjustment = Gtk.Template.Child()
viewport_offset_y_scale = Gtk.Template.Child()
viewport_offset_y_adjustment = Gtk.Template.Child()
units_menu = Gtk.Template.Child()
def __init__(self):
super(Gtk.Box, self).__init__()
@ -90,7 +93,7 @@ class ConnectedDevice(Gtk.Box):
self.active = True
self.all_enabled_state_inputs = [
self.display_zoom_on_focus_switch,
# self.display_size_scale,
self.display_size_scale,
self.follow_mode_switch,
self.follow_threshold_scale,
self.curved_display_switch,
@ -115,7 +118,7 @@ class ConnectedDevice(Gtk.Box):
self.settings.bind('disable-physical-displays', self.disable_physical_displays_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.connect('changed::display-distance', self._handle_display_distance)
# self.settings.bind('display-size', self.display_size_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('display-size', self.display_size_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('follow-threshold', self.follow_threshold_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
# self.settings.bind('widescreen-mode', self.widescreen_mode_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('curved-display', self.curved_display_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
@ -136,6 +139,10 @@ class ConnectedDevice(Gtk.Box):
self.monitor_wrapping_scheme_menu.connect('changed', self._handle_monitor_wrapping_scheme_menu_changed)
self._handle_monitor_wrapping_scheme_setting_changed(self.settings, self.settings.get_string('monitor-wrapping-scheme'))
current_units = self.settings.get_string('units')
self.units_menu.set_active_id(current_units)
self.units_menu.connect('changed', self._handle_units_menu_changed)
bind_shortcut_settings(self.get_parent(), [
[self.reassign_toggle_xr_effect_shortcut_button, self.toggle_xr_effect_shortcut_label],
[self.reassign_recenter_display_shortcut_button, self.recenter_display_shortcut_label],
@ -176,6 +183,10 @@ class ConnectedDevice(Gtk.Box):
self.follow_mode_switch.connect('notify::active', self._refresh_follow_mode)
self.effect_enable_switch.connect('notify::active', self._handle_switch_enabled_state)
self.state_manager.connect('notify::connected-device-full-size-cm', self._handle_metric_change)
self.state_manager.connect('notify::connected-device-full-distance-cm', self._handle_metric_change)
self.settings.connect('changed::units', self._handle_units_changed)
self.config_manager = ConfigManager.get_instance()
self.config_manager.connect('notify::breezy-desktop-enabled', self._handle_enabled_config)
self._bind_switch_to_config(self.enable_multi_tap_switch, 'multi-tap-enabled')
@ -238,6 +249,18 @@ class ConnectedDevice(Gtk.Box):
elif not widget.get_active() and is_zoom_on_focus_already_enabled:
self.settings.set_double('display-distance', toggle_display_distance_end)
def _handle_units_menu_changed(self, widget):
active_id = widget.get_active_id() or 'cm'
self.settings.set_string('units', active_id)
def _handle_units_changed(self, *args):
self._set_all_displays_distance(self.settings.get_double('toggle-display-distance-end'))
self._set_focused_display_distance(self.settings.get_double('toggle-display-distance-start'))
def _handle_metric_change(self, *args):
self._set_all_displays_distance(self.settings.get_double('toggle-display-distance-end'))
self._set_focused_display_distance(self.settings.get_double('toggle-display-distance-start'))
def _handle_monitor_wrapping_scheme_setting_changed(self, settings, val):
self.monitor_wrapping_scheme_menu.set_active_id(val)
@ -314,17 +337,33 @@ class ConnectedDevice(Gtk.Box):
self.display_zoom_on_focus_switch.set_active(should_zoom_on_focus_be_enabled)
def _set_focused_display_distance(self, distance):
self.focused_display_distance_label.set_markup(f"{_('Focused display')}: <b>{distance}</b>")
self.focused_display_distance_label.set_markup(f"{_('Focused display')}: <b>{self._format_distance(distance)}</b>")
self.settings.set_double('toggle-display-distance-start', distance)
self.display_zoom_on_focus_switch.set_sensitive(distance != self.settings.get_double('toggle-display-distance-end'))
def _set_all_displays_distance(self, distance):
self.all_displays_distance_label.set_markup(f"{_('All displays')}: <b>{distance}</b>")
self.all_displays_distance_label.set_markup(f"{_('All displays')}: <b>{self._format_distance(distance)}</b>")
self.settings.set_double('toggle-display-distance-end', distance)
self.display_zoom_on_focus_switch.set_active(False)
self.display_zoom_on_focus_switch.set_sensitive(distance != self.settings.get_double('toggle-display-distance-start'))
def _get_units(self):
units = self.settings.get_string('units')
return units if units in ['cm', 'in'] else 'cm'
def _format_distance(self, normalized):
sm = getattr(self, 'state_manager', None) or StateManager.get_instance()
full_cm = float(sm.get_property('connected-device-full-distance-cm') or 0.0)
if full_cm <= 0:
# Fallback to normalized display if metric unknown
return f"{round(normalized, 2)}"
cm = normalized * full_cm
if self._get_units() == 'in':
inches = cm / 2.54
return f"{inches:.2f} in"
return f"{cm:.1f} cm"
def _on_display_distance_preset_change_button_clicked(self, widget, settings_key, on_save_callback, title, subtitle, lower_limit, upper_limit):
dialog = DisplayDistanceDialog(settings_key, on_save_callback, title, subtitle, lower_limit, upper_limit)
dialog.set_transient_for(widget.get_ancestor(Gtk.Window))
@ -332,23 +371,28 @@ class ConnectedDevice(Gtk.Box):
def _on_set_all_displays_distance(self, prev_distance, distance):
focused_display_distance = self.settings.get_double('toggle-display-distance-start')
all_displays_distance = self.settings.get_double('toggle-display-distance-end')
if (distance < focused_display_distance):
self._set_focused_display_distance(distance)
all_displays_distance = self.settings.get_double('toggle-display-distance-end')
self._set_all_displays_distance(distance)
if prev_distance == focused_display_distance:
self.settings.set_double('display-distance', prev_distance)
# if we were at the unfocused distance, put us at the new unfocused distance
if prev_distance == all_displays_distance:
self.settings.set_double('display-distance', distance)
def _on_set_focused_display_distance(self, prev_distance, distance):
focused_display_distance = self.settings.get_double('toggle-display-distance-start')
all_displays_distance = self.settings.get_double('toggle-display-distance-end')
if (distance > all_displays_distance):
self._set_all_displays_distance(distance)
focused_display_distance = self.settings.get_double('toggle-display-distance-start')
self._set_focused_display_distance(distance)
# if we were at the focused distance, put us at the new focused distance
if prev_distance == focused_display_distance:
self.settings.set_double('display-distance', distance)
def _save_custom_resolutions(self):
with open(self._custom_resolutions_file_path, 'w') as f:
json.dump(self._custom_resolution_options, f)

View File

@ -5,7 +5,6 @@ from .displaydistancedialogcontent import DisplayDistanceDialogContent
class DisplayDistanceDialog(Gtk.Dialog):
__gtype_name__ = 'DisplayDistanceDialog'
show_full_scale_button = Gtk.Template.Child()
save_button = Gtk.Template.Child()
def __init__(self, settings_key, on_save_callback, title, subtitle, lower_limit, upper_limit):
@ -15,14 +14,9 @@ class DisplayDistanceDialog(Gtk.Dialog):
self.on_save_callback = on_save_callback
self.set_title(title)
self.content = DisplayDistanceDialogContent(settings_key, self.show_full_scale_button, self.save_button, self._on_save_callback, subtitle, lower_limit, upper_limit)
self.content = DisplayDistanceDialogContent(settings_key, self.save_button, self._on_save_callback, subtitle, lower_limit, upper_limit)
self.get_content_area().append(self.content)
self.show_full_scale_button.connect('clicked', self._on_show_full_scale_button_clicked)
def _on_show_full_scale_button_clicked(self, button):
self.show_full_scale_button.set_visible(False)
def _on_save_callback(self, prev_distance, distance):
self.on_save_callback(prev_distance, distance)
self.close()

View File

@ -1,5 +1,6 @@
from gi.repository import Gtk, Gio
from .settingsmanager import SettingsManager
from .statemanager import StateManager
import gettext
@ -14,7 +15,7 @@ class DisplayDistanceDialogContent(Gtk.Box):
display_distance_scale = Gtk.Template.Child()
display_distance_adjustment = Gtk.Template.Child()
def __init__(self, settings_key, show_full_scale_button, save_button, on_save_callback, subtitle, lower_limit, upper_limit):
def __init__(self, settings_key, save_button, on_save_callback, subtitle, lower_limit, upper_limit):
super(Gtk.Box, self).__init__()
self.init_template()
@ -22,37 +23,28 @@ class DisplayDistanceDialogContent(Gtk.Box):
self.on_save_callback = on_save_callback
self.settings = SettingsManager.get_instance().settings
self.state_manager = StateManager.get_instance()
self.prev_distance = self.settings.get_double('display-distance')
self.display_distance_adjustment.set_value(self.settings.get_double(settings_key))
self.lower_limit_orig = self.display_distance_adjustment.get_lower()
self.upper_limit_orig = self.display_distance_adjustment.get_upper()
self._add_marks(lower_limit, upper_limit)
self.settings.bind('display-distance', self.display_distance_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
show_full_scale_button.connect('clicked', self._on_show_full_scale_button_clicked)
self.display_distance_scale.set_format_value_func(lambda scale, val: self._format_distance(val))
self.state_manager.connect('notify::connected-device-full-distance-cm', lambda *args: self.display_distance_scale.queue_draw())
self.settings.connect('changed::units', lambda *args: self.display_distance_scale.queue_draw())
save_button.connect('clicked', self._on_save_button_clicked)
def _add_marks(self, lower_limit, upper_limit):
self.display_distance_scale.clear_marks()
if self.lower_limit_orig == lower_limit:
self.display_distance_scale.add_mark(self.lower_limit_orig, Gtk.PositionType.BOTTOM, _("closer"))
else:
self.display_distance_adjustment.set_lower(lower_limit)
self.display_distance_scale.add_mark(1.0, Gtk.PositionType.BOTTOM, _("fullscreen"))
if self.upper_limit_orig == upper_limit:
self.display_distance_scale.add_mark(self.upper_limit_orig, Gtk.PositionType.BOTTOM, _("farther"))
else:
self.display_distance_adjustment.set_upper(upper_limit)
def _on_show_full_scale_button_clicked(self, button):
self._add_marks(self.lower_limit_orig, self.upper_limit_orig)
self.display_distance_adjustment.set_lower(self.lower_limit_orig)
self.display_distance_adjustment.set_upper(self.upper_limit_orig)
def _on_save_button_clicked(self, button):
self.on_save_callback(self.prev_distance, self.display_distance_adjustment.get_value())
self.on_save_callback(self.prev_distance, self.display_distance_adjustment.get_value())
def _get_units(self):
units = self.settings.get_string('units')
return units if units in ['cm', 'in'] else 'cm'
def _format_distance(self, normalized):
full_cm = float(self.state_manager.get_property('connected-device-full-distance-cm') or 0.0)
if full_cm <= 0:
return f"{round(normalized, 2)}"
cm = normalized * full_cm
if self._get_units() == 'in':
inches = cm / 2.54
return f"{inches:.2f} in"
return f"{cm:.1f} cm"

View File

@ -229,6 +229,36 @@
</child>
</object>
</child>
<child>
<object class="AdwActionRow" id="display_size_row">
<property name="title" translatable="yes"><!-- adjustment slider -->Display size</property>
<property name="subtitle" translatable="yes">Set how large you want the display to appear.</property>
<child>
<object class="GtkScale" id="display_size_scale">
<property name="valign">3</property>
<property name="draw-value">true</property>
<property name="value-pos">0</property>
<property name="digits">2</property>
<property name="width-request">350</property>
<property name="has-origin">false</property>
<property name="adjustment">
<object class="GtkAdjustment" id="display_size_adjustment">
<property name="lower">0.1</property>
<property name="upper">2.5</property>
<property name="step-increment">0.01</property>
<property name="value">1.0</property>
</object>
</property>
<marks>
<mark value="0.5" position="bottom">0.5×</mark>
<mark value="1.0" position="bottom" translatable="yes">full</mark>
<mark value="1.5" position="bottom">1.5×</mark>
<mark value="2.0" position="bottom">2.0×</mark>
</marks>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes"><!-- adjustment slider -->Follow threshold</property>
@ -554,6 +584,35 @@
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes"><!-- section heading for the advanced settings -->Advanced Settings</property>
<property name="width-request">450</property>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Units</property>
<property name="subtitle" translatable="yes">Choose measurement units for size and distance displays.</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="width-request">150</property>
<property name="margin-start">30</property>
<child>
<object class="GtkBox">
<property name="valign">3</property>
<style>
<class name="flat"/>
</style>
<child>
<object class="GtkComboBoxText" id="units_menu">
<items>
<item translatable="yes" id="cm">Centimeters</item>
<item translatable="yes" id="in">Inches</item>
</items>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes"><!-- feature that tries to the find best-fit monitor config -->Find optimal display config</property>

View File

@ -25,12 +25,17 @@
<property name="has-origin">false</property>
<property name="adjustment">
<object class="GtkAdjustment" id="display_distance_adjustment">
<property name="lower">0.2</property>
<property name="upper">2.5</property>
<property name="lower">0.1</property>
<property name="upper">1.5</property>
<property name="step-increment">0.01</property>
<property name="value">1.05</property>
</object>
</property>
<marks>
<mark value="0.1" position="bottom" translatable="yes">closer</mark>
<mark value="1.0" position="bottom" translatable="yes">default</mark>
<mark value="1.5" position="bottom" translatable="yes">farther</mark>
</marks>
</object>
</child>
</template>

View File

@ -4,15 +4,6 @@
<template class="DisplayDistanceDialog" parent="GtkDialog">
<property name="modal">1</property>
<property name="use-header-bar">1</property>
<child type="action">
<object class="GtkButton" id="show_full_scale_button">
<property name="label" translatable="yes">Show full range</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="save_button">
<property name="label" translatable="yes">Done</property>

View File

@ -21,6 +21,8 @@ class StateManager(GObject.GObject):
'license-present': (bool, 'License Present', 'Whether a license is present', False, GObject.ParamFlags.READWRITE),
'enabled-features-list': (object, 'Enabled Features List', 'A list of the enabled features', GObject.ParamFlags.READWRITE),
'device-supports-sbs': (bool, 'Device Supports SBS', 'Whether the connected device supports SBS', False, GObject.ParamFlags.READWRITE),
'connected-device-full-distance-cm': (float, 'Full Distance (cm)', 'Device full distance in cm', 0.0, 10000.0, 0.0, GObject.ParamFlags.READWRITE),
'connected-device-full-size-cm': (float, 'Full Size (cm)', 'Device full display size in cm', 0.0, 10000.0, 0.0, GObject.ParamFlags.READWRITE),
}
_instance = None
@ -59,6 +61,8 @@ class StateManager(GObject.GObject):
self.license_present = False
self.enabled_features = []
self.device_supports_sbs = False
self.connected_device_full_distance_cm = 0.0
self.connected_device_full_size_cm = 0.0
self._running = True
self._refresh_state()
@ -98,6 +102,14 @@ class StateManager(GObject.GObject):
self.set_property('device-supports-sbs', self.state.get('sbs_mode_supported', False))
self.set_property('widescreen-mode', self.state.get('sbs_mode_enabled', False))
full_distance = self.state.get('connected_device_full_distance_cm') or 0.0
if full_distance != self.connected_device_full_distance_cm:
self.set_property('connected-device-full-distance-cm', full_distance)
full_size = self.state.get('connected_device_full_size_cm') or 0.0
if full_size != self.connected_device_full_size_cm:
self.set_property('connected-device-full-size-cm', full_size)
if self._running: threading.Timer(1.0, self._refresh_state).start()
def do_set_property(self, prop, value):
@ -115,6 +127,10 @@ class StateManager(GObject.GObject):
self.enabled_features = value
if prop.name == 'device-supports-sbs':
self.device_supports_sbs = value
if prop.name == 'connected-device-full-distance-cm':
self.connected_device_full_distance_cm = value
if prop.name == 'connected-device-full-size-cm':
self.connected_device_full_size_cm = value
def do_get_property(self, prop):
if prop.name == 'driver-running':
@ -130,4 +146,8 @@ class StateManager(GObject.GObject):
if prop.name == 'enabled-features-list':
return self.enabled_features
if prop.name == 'device-supports-sbs':
return self.device_supports_sbs
return self.device_supports_sbs
if prop.name == 'connected-device-full-distance-cm':
return self.connected_device_full_distance_cm
if prop.name == 'connected-device-full-size-cm':
return self.connected_device_full_size_cm