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 # build XR driver
XR_DRIVER_BINARY=$XR_DRIVER_DIR/out/xrDriver-$ARCH.tar.gz 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 [ ! -e "$XR_DRIVER_BINARY" ] || [ "$1" == "--rebuild-driver" ]; then
# if a file exists at custom_banner_config.yml, copy it to the xrealAirLinuxDriver directory # if a file exists at custom_banner_config.yml, copy it to the xrealAirLinuxDriver directory
@ -71,6 +75,9 @@ 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 # strange issue where the base library produces a .so file if the build is not cleaned
rm -rf build/ rm -rf build/
if [ -n "${STEAMOS:-}" ]; then
export STEAMOS
fi
docker-build/init.sh docker-build/init.sh
docker-build/run-build.sh $ARCH docker-build/run-build.sh $ARCH
popd popd

View File

@ -35,6 +35,7 @@ export default class BreezyDesktopExtension extends Extension {
this._data_stream_bindings = []; this._data_stream_bindings = [];
this._show_banner_connection = null; this._show_banner_connection = null;
this._distance_connection = null; this._distance_connection = null;
this._display_size_connection = null;
this._focused_monitor_distance_connection = null; this._focused_monitor_distance_connection = null;
this._follow_threshold_connection = null; this._follow_threshold_connection = null;
this._breezy_desktop_running_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_position(targetMonitor.x, targetMonitor.y);
this._virtual_displays_overlay.set_size(targetMonitor.width, targetMonitor.height); 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(); Globals.data_stream.refresh_data();
this._virtual_displays_actor = new VirtualDisplaysActor({ this._virtual_displays_actor = new VirtualDisplaysActor({
width: targetMonitor.width, 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_x: this.settings.get_double('viewport-offset-x'),
viewport_offset_y: this.settings.get_double('viewport-offset-y'), viewport_offset_y: this.settings.get_double('viewport-offset-y'),
display_distance: this.settings.get_double('display-distance'), 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_start: this.settings.get_double('toggle-display-distance-start'),
toggle_display_distance_end: this.settings.get_double('toggle-display-distance-end'), toggle_display_distance_end: this.settings.get_double('toggle-display-distance-end'),
framerate_cap: this.settings.get_double('framerate-cap'), framerate_cap: this.settings.get_double('framerate-cap'),
imu_snapshots: Globals.data_stream.imu_snapshots, imu_snapshots: Globals.data_stream.imu_snapshots,
show_banner: Globals.data_stream.show_banner, 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); 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._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._focused_monitor_distance_connection =
this._virtual_displays_actor.connect('notify::focused-monitor-details', this._update_display_distance.bind(this)); 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)); 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) { _update_display_distance(object, event) {
const value = this.settings.get_double('display-distance'); const distance = this.settings.get_double('display-distance');
Globals.logger.log_debug(`BreezyDesktopExtension _update_display_distance ${value}`); const size = this.settings.get_double('display-size');
if (value !== undefined) { Globals.logger.log_debug(`BreezyDesktopExtension _update_display_distance ${distance} ${size}`);
let focusedMonitorSizeAdjustment = 1.0; 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) { if (this._virtual_displays_actor?.focused_monitor_details && this._target_monitor) {
const fovMonitor = this._target_monitor.monitor; const fovMonitor = this._target_monitor.monitor;
const focusedMonitor = this._virtual_displays_actor.focused_monitor_details; const focusedMonitor = this._virtual_displays_actor.focused_monitor_details;
focusedMonitorSizeAdjustment = focusedMonitorSizeAdjustment *=
Math.max(focusedMonitor.width / fovMonitor.width, focusedMonitor.height / fovMonitor.height); 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.settings.disconnect(this._distance_connection);
this._distance_connection = null; 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) { if (this._focused_monitor_distance_connection) {
this._virtual_displays_actor.disconnect(this._focused_monitor_distance_connection); this._virtual_displays_actor.disconnect(this._focused_monitor_distance_connection);
this._focused_monitor_distance_connection = null; 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 // FOV in radians is spherical, so doesn't follow Pythagoras' theorem
export function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) { 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 // first convert from a spherical FOV to a diagonal FOV on a flat plane at a unit distance of 1.0
const flatDiagonalFOV = 2 * Math.tan(diagonalFOVRadians / 2); const diagonalLengthUnitDistance = 2 * Math.tan(diagonalFOVRadians / 2);
// then convert to flat plane horizontal and vertical FOVs // then convert to flat plane horizontal and vertical FOVs
const flatVerticalFOV = flatDiagonalFOV / Math.sqrt(1 + aspectRatio * aspectRatio); const heightUnitDistance = diagonalLengthUnitDistance / Math.sqrt(1 + aspectRatio * aspectRatio);
const flatHorizontalFOV = flatVerticalFOV * aspectRatio; const widthUnitDistance = heightUnitDistance * aspectRatio;
// then convert back to spherical FOV
return { return {
diagonal: diagonalFOVRadians, // then convert back to spherical FOV
horizontal: 2 * Math.atan(flatHorizontalFOV / 2), diagonalRadians: diagonalFOVRadians,
vertical: 2 * Math.atan(flatVerticalFOV / 2) 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)), 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, lengthToRadians: (fovRadians, fovLength, screenEdgeDistance, toLength) => Math.asin(toLength / 2 / screenEdgeDistance) * 2,
angleToLength: (fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) => { 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 radiansToSegments: (screenRadians) => 1
}, },
@ -42,6 +50,7 @@ export const fovConversionFns = {
fovEdgeToScreenCenterDistance: (edgeDistance, screenLength) => edgeDistance, fovEdgeToScreenCenterDistance: (edgeDistance, screenLength) => edgeDistance,
lengthToRadians: (fovRadians, fovLength, screenEdgeDistance, toLength) => fovRadians / fovLength * toLength, lengthToRadians: (fovRadians, fovLength, screenEdgeDistance, toLength) => fovRadians / fovLength * toLength,
angleToLength: (fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) => fovLength / fovRadians * Math.atan2(toAngleOpposite, toAngleAdjacent), angleToLength: (fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) => fovLength / fovRadians * Math.atan2(toAngleOpposite, toAngleAdjacent),
fovRadiansAtDistance: (fovRadians, unitLength, newScreenDistance) => fovRadians / newScreenDistance,
radiansToSegments: (screenRadians) => Math.ceil(screenRadians * segmentsPerRadian) 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) => { 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]; 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 // Create a mesh of vertices in a pattern suitable for TRIANGLE_STRIP
function createVertexMesh(fovDetails, monitorDetails, positionVectorNWU) { function createVertexMesh(fovDetails, monitorDetails, positionVectorNWU) {
let fovConversions = fovDetails.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat; let horizontalWrap = fovDetails.monitorWrappingScheme === 'horizontal';
const sideEdgeDistancePixels = fovConversions.centerToFovEdgeDistance( const horizontalConversions = fovDetails.curvedDisplay && horizontalWrap ? fovConversionFns.curved : fovConversionFns.flat;
const sideEdgeDistancePixels = horizontalConversions.centerToFovEdgeDistance(
fovDetails.completeScreenDistancePixels, fovDetails.completeScreenDistancePixels,
fovDetails.widthPixels fovDetails.sizeAdjustedWidthPixels
); );
const horizontalRadians = fovConversions.lengthToRadians( const horizontalRadians = horizontalConversions.lengthToRadians(
fovDetails.defaultDistanceHorizontalRadians, fovDetails.defaultDistanceHorizontalRadians,
fovDetails.widthPixels, fovDetails.widthPixels,
sideEdgeDistancePixels, sideEdgeDistancePixels,
monitorDetails.width 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.completeScreenDistancePixels,
fovDetails.heightPixels fovDetails.sizeAdjustedHeightPixels
); );
const verticalRadians = fovConversions.lengthToRadians( const verticalRadians = verticalConversions.lengthToRadians(
fovDetails.defaultDistanceVerticalRadians, fovDetails.defaultDistanceVerticalRadians,
fovDetails.heightPixels, fovDetails.heightPixels,
topEdgeDistancePixels, topEdgeDistancePixels,
monitorDetails.height monitorDetails.height
); );
let horizontalWrap = fovDetails.monitorWrappingScheme === 'horizontal'; const xSegments = horizontalConversions.radiansToSegments(horizontalRadians);
let verticalWrap = fovDetails.monitorWrappingScheme === 'vertical'; const ySegments = verticalConversions.radiansToSegments(verticalRadians);
const xSegments = horizontalWrap ? fovConversions.radiansToSegments(horizontalRadians) : 1;
const ySegments = verticalWrap ? fovConversions.radiansToSegments(verticalRadians) : 1;
const texXLeft = 0; const texXLeft = 0;
const texYTop = 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', 'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
GObject.ParamFlags.READWRITE 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': GObject.ParamSpec.boolean(
'smooth-follow-enabled', 'smooth-follow-enabled',
'Smooth follow enabled', 'Smooth follow enabled',
@ -176,12 +184,21 @@ export const VirtualDisplayEffect = GObject.registerClass({
GObject.ParamFlags.READWRITE, GObject.ParamFlags.READWRITE,
true 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': GObject.ParamSpec.double(
'display-distance', 'display-distance',
'Display Distance', 'Display Distance',
'Distance of the display from the camera', 'Distance of the display from the camera',
GObject.ParamFlags.READWRITE, GObject.ParamFlags.READWRITE,
0.0, 0.1,
2.5, 2.5,
1.0 1.0
), ),
@ -190,7 +207,7 @@ export const VirtualDisplayEffect = GObject.registerClass({
'Display distance default', 'Display distance default',
'Distance to use when not explicitly set, or when reset', 'Distance to use when not explicitly set, or when reset',
GObject.ParamFlags.READWRITE, GObject.ParamFlags.READWRITE,
0.2, 0.1,
2.5, 2.5,
1.0 1.0
), ),
@ -253,6 +270,8 @@ export const VirtualDisplayEffect = GObject.registerClass({
this._use_smooth_follow_origin = false; this._use_smooth_follow_origin = false;
this.connect('notify::display-distance', this._update_display_distance.bind(this)); 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::focused-monitor-index', this._update_display_distance.bind(this));
this.connect('notify::monitor-placements', this._update_display_position.bind(this)); this.connect('notify::monitor-placements', this._update_display_position.bind(this));
this.connect('notify::show-banner', this._handle_banner_update.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 = noRotationVector.map(coord => coord * inverse_follow_ease);
finalPositionVector[0] = noRotationVector[0]; 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; const rotation_radians = this.monitor_placements[this.monitor_index].rotationAngleRadians;
if (this._initialized) { 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]); this.set_uniform_float(this.get_uniform_location("u_show_banner"), 1, [this.show_banner ? 1.0 : 0.0]);
} }
perspective(fovHorizontalRadians, aspect, near, far) { perspective(widthUnitDistance, aspect, near, far) {
const f = 1.0 / Math.tan(fovHorizontalRadians / 2.0); const f = 2.0 / widthUnitDistance;
const range = far - near; const range = far - near;
return [ 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); 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); 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); return vec3(-v.y, v.z, -v.x);
} }
@ -492,15 +515,15 @@ export const VirtualDisplayEffect = GObject.registerClass({
if (!u_show_banner) { if (!u_show_banner) {
float aspect_ratio = u_display_resolution.x / u_display_resolution.y; float aspect_ratio = u_display_resolution.x / u_display_resolution.y;
vec4 quat_t0 = nwuToESU(quatConjugate(u_pose_orientation[0])); vec4 quat_t0 = nwuToEUS(quatConjugate(u_pose_orientation[0]));
vec3 position_vector = applyQuaternionToVector(nwuToESU(u_pose_position), quat_t0); vec3 position_vector = applyQuaternionToVector(nwuToEUS(u_pose_position), quat_t0);
vec3 final_lens_position = u_lens_vector + position_vector; vec3 final_lens_position = nwuToEUS(u_lens_vector) + position_vector;
vec3 complete_vector = applyXRotationToVector(world_pos.xyz, u_rotation_x_radians); vec3 complete_vector = applyXRotationToVector(world_pos.xyz, u_rotation_x_radians);
complete_vector = applyYRotationToVector(complete_vector, u_rotation_y_radians); complete_vector = applyYRotationToVector(complete_vector, u_rotation_y_radians);
vec3 rotated_vector_t0 = applyQuaternionToVector(complete_vector, quat_t0); 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]; float delta_time_t0 = u_pose_orientation[3][0] - u_pose_orientation[3][1];
// how quickly the vertex is moving relative to the camera // how quickly the vertex is moving relative to the camera
@ -545,20 +568,20 @@ export const VirtualDisplayEffect = GObject.registerClass({
this._initialized = true; this._initialized = true;
const aspect = this.target_monitor.width / this.target_monitor.height; 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( const projection_matrix = this.perspective(
fovRadians.horizontal, fovLengths.widthUnitDistance,
aspect, aspect,
1.0, 1.0,
10000.0 10000.0
); );
this.set_uniform_matrix(this.get_uniform_location("u_projection_matrix"), false, 4, projection_matrix); 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_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_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_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_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._update_display_position();
this._handle_banner_update(); 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]); this.set_uniform_float(this.get_uniform_location('u_look_ahead_ms'), 1, [0.0]);
lookAheadSet = true; 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_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); this.set_uniform_float(this.get_uniform_location("u_pose_position"), 3, posePositionPixels);
} else { } else {

View File

@ -7,7 +7,7 @@ import Shell from 'gi://Shell';
import St from 'gi://St'; import St from 'gi://St';
import { VirtualDisplayEffect, SMOOTH_FOLLOW_SLERP_TIMELINE_MS } from './virtualdisplayeffect.js'; 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'; 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 // if we leave the monitor with some margin, unfocus even if no other monitor is in focus
const UNFOCUS_THRESHOLD = 1.1 / 2.0; 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) { function getMonitorDistance(fovDetails, lookUpPixels, lookWestPixels, monitorVector, monitorDetails, upAngleToLength, westAngleToLength) {
const monitorAspectRatio = monitorDetails.width / monitorDetails.height; // 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;
// weight the up distance by the aspect ratio
const vectorUpPixels = upAngleToLength( const vectorUpPixels = upAngleToLength(
fovDetails.defaultDistanceVerticalRadians, fovDetails.defaultDistanceVerticalRadians,
fovDetails.heightPixels, fovDetails.heightPixels,
fovDetails.completeScreenDistancePixels, monitorDistance,
monitorVector[2], monitorVector[2],
monitorVector[0] monitorVector[0]
); ) * distanceAdjustment;
const upDeltaPixels = (lookUpPixels - vectorUpPixels) * monitorAspectRatio; const upPercentage = Math.abs(lookUpPixels * distanceAdjustment - vectorUpPixels) / monitorDetails.height;
const vectorWestPixels = westAngleToLength( const vectorWestPixels = westAngleToLength(
fovDetails.defaultDistanceHorizontalRadians, fovDetails.defaultDistanceHorizontalRadians,
fovDetails.widthPixels, fovDetails.widthPixels,
fovDetails.completeScreenDistancePixels, monitorDistance,
monitorVector[1], monitorVector[1],
monitorVector[0] monitorVector[0]
); ) * distanceAdjustment;
const westDeltaPixels = lookWestPixels - vectorWestPixels; const westPercentage = Math.abs(lookWestPixels * distanceAdjustment - vectorWestPixels) / monitorDetails.width;
const totalDeltaPixels = Math.sqrt(upDeltaPixels * upDeltaPixels + westDeltaPixels * westDeltaPixels);
// threshold is a percentage of width, and height was already properly weighted // how close we are to any edge is the largest of the two percentages
return totalDeltaPixels / monitorDetails.width; return Math.max(upPercentage, westPercentage);
} }
/** /**
* Find the vector in the array that's closest to the quaternion rotation * Find the vector in the array that's closest to the quaternion rotation
* *
* @param {number[]} quaternion - Reference quaternion [x, y, z, w] * @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[][]} monitorVectors - Array of monitor vectors [x, y, z] to search from
* @param {number} currentFocusedIndex - Index of the currently focused monitor * @param {number} currentFocusedIndex - Index of the currently focused monitor
* @param {number} focusedMonitorDistance - Distance to the focused monitor, < 1.0 if zoomed in * @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 * @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 * @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 lookVector = [1.0, 0.0, 0.0]; // NWU vector pointing to the center of the screen
const rotatedLookVector = applyQuaternionToVector(lookVector, quaternion); const rotatedLookVector = applyQuaternionToVector(lookVector, quaternion);
@ -82,8 +86,13 @@ function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, foc
rotatedLookVector[0] rotatedLookVector[0]
); );
let closestIndex = -1; function vectorRelativeToLensPosition(vector) {
let closestDistance = Infinity; 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 // the currently focused monitor is the most likely to be the closest, check it first and exit early if it is
if (currentFocusedIndex !== -1) { if (currentFocusedIndex !== -1) {
@ -91,15 +100,18 @@ function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, foc
fovDetails, fovDetails,
lookUpPixels, lookUpPixels,
lookWestPixels, lookWestPixels,
monitorVectors[currentFocusedIndex], vectorRelativeToLensPosition(monitorVectors[currentFocusedIndex]),
monitorsDetails[currentFocusedIndex], monitorsDetails[currentFocusedIndex],
upConversionFns.angleToLength, upConversionFns.angleToLength,
westConversionFns.angleToLength westConversionFns.angleToLength
) * focusedMonitorDistance; ) * 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 // find the vector closest to the rotated look vector
monitorVectors.forEach((monitorVector, index) => { monitorVectors.forEach((monitorVector, index) => {
if (index === currentFocusedIndex) return; if (index === currentFocusedIndex) return;
@ -108,7 +120,7 @@ function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, foc
fovDetails, fovDetails,
lookUpPixels, lookUpPixels,
lookWestPixels, lookWestPixels,
monitorVector, vectorRelativeToLensPosition(monitorVector),
monitorsDetails[index], monitorsDetails[index],
upConversionFns.angleToLength, upConversionFns.angleToLength,
westConversionFns.angleToLength westConversionFns.angleToLength
@ -208,8 +220,10 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
if (fovDetails.monitorWrappingScheme === 'horizontal') { if (fovDetails.monitorWrappingScheme === 'horizontal') {
// monitors wrap around us horizontally // monitors wrap around us horizontally
const sideEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.widthPixels); const sideEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.sizeAdjustedWidthPixels);
const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels; const monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedWidthPixels;
// targetWidth is assumed to aleady be size adjusted
const lengthToRadianFn = (targetWidth) => conversionFns.lengthToRadians( const lengthToRadianFn = (targetWidth) => conversionFns.lengthToRadians(
fovDetails.defaultDistanceHorizontalRadians, fovDetails.defaultDistanceHorizontalRadians,
fovDetails.widthPixels, fovDetails.widthPixels,
@ -217,14 +231,14 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
targetWidth targetWidth
); );
cachedMonitorRadians[0] = -fovDetails.defaultDistanceHorizontalRadians / 2; cachedMonitorRadians[0] = -lengthToRadianFn(fovDetails.sizeAdjustedWidthPixels) / 2;
horizontalMonitorSort(monitorDetailsList).forEach(({monitorDetails, originalIndex}) => { horizontalMonitorSort(monitorDetailsList).forEach(({monitorDetails, originalIndex}) => {
const monitorWrapDetails = monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorDetails.x, monitorDetails.width, lengthToRadianFn); const monitorWrapDetails = monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorDetails.x, monitorDetails.width, lengthToRadianFn);
const monitorCenterRadius = conversionFns.fovEdgeToScreenCenterDistance(sideEdgeRadius, monitorDetails.width); 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 // 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) // this is where our monitor's center is in relation to an fov-sized viewport centered about (0, 0)
const upCenterPixels = upTopPixels - upCenterOffsetPixels; const upCenterPixels = upTopPixels - upCenterOffsetPixels;
@ -240,7 +254,7 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
// up is flat when wrapping horizontally // up is flat when wrapping horizontally
upCenterPixels upCenterPixels
], ],
centerLook: normalizeVector([ centerLook: [
// north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians // north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
monitorCenterRadius * Math.cos(monitorWrapDetails.center), monitorCenterRadius * Math.cos(monitorWrapDetails.center),
@ -249,7 +263,7 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
// up is flat when wrapping horizontally // up is flat when wrapping horizontally
upCenterPixels upCenterPixels
]), ],
rotationAngleRadians: { rotationAngleRadians: {
x: 0, x: 0,
y: -monitorWrapDetails.center y: -monitorWrapDetails.center
@ -259,8 +273,10 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
} else if (fovDetails.monitorWrappingScheme === 'vertical') { } else if (fovDetails.monitorWrappingScheme === 'vertical') {
// monitors wrap around us vertically // monitors wrap around us vertically
const topEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.heightPixels); const topEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.sizeAdjustedHeightPixels);
const monitorSpacingPixels = monitorSpacing * fovDetails.heightPixels; const monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedHeightPixels;
// targetHeight is assumed to aleady be size adjusted
const lengthToRadianFn = (targetHeight) => conversionFns.lengthToRadians( const lengthToRadianFn = (targetHeight) => conversionFns.lengthToRadians(
fovDetails.defaultDistanceVerticalRadians, fovDetails.defaultDistanceVerticalRadians,
fovDetails.heightPixels, fovDetails.heightPixels,
@ -268,14 +284,14 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
targetHeight targetHeight
); );
cachedMonitorRadians[0] = -fovDetails.defaultDistanceVerticalRadians / 2; cachedMonitorRadians[0] = -lengthToRadianFn(fovDetails.sizeAdjustedHeightPixels) / 2;
verticalMonitorSort(monitorDetailsList).forEach(({monitorDetails, originalIndex}) => { verticalMonitorSort(monitorDetailsList).forEach(({monitorDetails, originalIndex}) => {
const monitorWrapDetails = monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorDetails.y, monitorDetails.height, lengthToRadianFn); const monitorWrapDetails = monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorDetails.y, monitorDetails.height, lengthToRadianFn);
const monitorCenterRadius = conversionFns.fovEdgeToScreenCenterDistance(topEdgeRadius, monitorDetails.height); 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 // 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) // this is where our monitor's center is in relation to an fov-sized viewport centered about (0, 0)
const westCenterPixels = westLeftPixels - westCenterOffsetPixels; const westCenterPixels = westLeftPixels - westCenterOffsetPixels;
@ -291,7 +307,7 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
// up is centered about the FOV center // up is centered about the FOV center
0 0
], ],
centerLook: normalizeVector([ centerLook: [
// north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians // north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
monitorCenterRadius * Math.cos(monitorWrapDetails.center), 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 // up is opposite where radius is the hypotenuse, using monitorWrapDetails.center as the radians
-monitorCenterRadius * Math.sin(monitorWrapDetails.center) -monitorCenterRadius * Math.sin(monitorWrapDetails.center)
]), ],
rotationAngleRadians: { rotationAngleRadians: {
x: -monitorWrapDetails.center, x: -monitorWrapDetails.center,
y: 0 y: 0
@ -308,17 +324,16 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
}); });
}); });
} else { } else {
const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels; const monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedWidthPixels;
// monitors make a flat wall in front of us, no wrapping // monitors make a flat wall in front of us, no wrapping
monitorDetailsList.forEach((monitorDetails, index) => { monitorDetailsList.forEach((monitorDetails, index) => {
const upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels; const upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.sizeAdjustedHeightPixels) * monitorSpacingPixels;
const westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels; const westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.sizeAdjustedWidthPixels) * monitorSpacingPixels;
// offsets for aligning this monitor's center with the fov-sized viewport's center // offsets 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;
const upCenterOffsetPixels = (monitorDetails.height - fovDetails.heightPixels) / 2; const upCenterOffsetPixels = (monitorDetails.height - fovDetails.sizeAdjustedHeightPixels) / 2;
const westCenterPixels = westLeftPixels - westCenterOffsetPixels; const westCenterPixels = westLeftPixels - westCenterOffsetPixels;
const upCenterPixels = upTopPixels - upCenterOffsetPixels; const upCenterPixels = upTopPixels - upCenterOffsetPixels;
@ -329,11 +344,11 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
westCenterPixels, westCenterPixels,
upCenterPixels upCenterPixels
], ],
centerLook: normalizeVector([ centerLook: [
fovDetails.completeScreenDistancePixels, fovDetails.completeScreenDistancePixels,
westCenterPixels, westCenterPixels,
upCenterPixels upCenterPixels
]), ],
rotationAngleRadians: { rotationAngleRadians: {
x: 0, x: 0,
y: 0 y: 0
@ -446,6 +461,13 @@ export const VirtualDisplaysActor = GObject.registerClass({
'Latest IMU quaternion snapshots and epoch timestamp for when it was collected', 'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
GObject.ParamFlags.READWRITE 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': GObject.ParamSpec.boolean(
'curved-display', 'curved-display',
'Curved Display', 'Curved Display',
@ -499,7 +521,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
'Display size', 'Display size',
'Size of the display', 'Size of the display',
GObject.ParamFlags.READWRITE, GObject.ParamFlags.READWRITE,
0.2, 0.1,
2.5, 2.5,
1.0 1.0
), ),
@ -515,7 +537,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
'Display Distance', 'Display Distance',
'Distance of the display from the camera', 'Distance of the display from the camera',
GObject.ParamFlags.READWRITE, GObject.ParamFlags.READWRITE,
0.2, 0.1,
2.5, 2.5,
1.05 1.05
), ),
@ -537,7 +559,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
'Display distance start', 'Display distance start',
'Start distance when using the "change distance" shortcut.', 'Start distance when using the "change distance" shortcut.',
GObject.ParamFlags.READWRITE, GObject.ParamFlags.READWRITE,
0.2, 0.1,
2.5, 2.5,
1.05 1.05
), ),
@ -546,7 +568,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
'Display distance end', 'Display distance end',
'End distance when using the "change distance" shortcut.', 'End distance when using the "change distance" shortcut.',
GObject.ParamFlags.READWRITE, GObject.ParamFlags.READWRITE,
0.2, 0.1,
2.5, 2.5,
1.05 1.05
), ),
@ -578,7 +600,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
constructor(params = {}) { constructor(params = {}) {
super(params); super(params);
this._all_monitors = [ this._all_monitors_unmodified = [
this.target_monitor, this.target_monitor,
...this.virtual_monitors ...this.virtual_monitors
]; ];
@ -649,10 +671,10 @@ export const VirtualDisplaysActor = GObject.registerClass({
this._property_connections.push(this.connect(`notify::${property}`, fn.bind(this))); this._property_connections.push(this.connect(`notify::${property}`, fn.bind(this)));
}).bind(this); }).bind(this);
this._distance_ease_timeline = null; notifyToFunction('toggle-display-distance-start', this._handle_display_size_distance_change);
notifyToFunction('toggle-display-distance-start', this._handle_display_distance_properties_change); notifyToFunction('toggle-display-distance-end', this._handle_display_size_distance_change);
notifyToFunction('toggle-display-distance-end', this._handle_display_distance_properties_change); notifyToFunction('display-distance', this._handle_display_size_distance_change);
notifyToFunction('display-distance', this._handle_display_distance_properties_change); notifyToFunction('display-size', this._handle_display_size_distance_change);
notifyToFunction('monitor-wrapping-scheme', this._update_monitor_placements); notifyToFunction('monitor-wrapping-scheme', this._update_monitor_placements);
notifyToFunction('monitor-spacing', this._update_monitor_placements); notifyToFunction('monitor-spacing', this._update_monitor_placements);
notifyToFunction('headset-display-as-viewport-center', 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('custom-banner-enabled', this._handle_banner_update);
notifyToFunction('framerate-cap', this._handle_frame_rate_cap_change); notifyToFunction('framerate-cap', this._handle_frame_rate_cap_change);
notifyToFunction('smooth-follow-enabled', this._handle_smooth_follow_enabled_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(); this._handle_frame_rate_cap_change();
const actorToDisplayRatios = [ const actorToDisplayRatios = [
@ -681,7 +703,7 @@ export const VirtualDisplaysActor = GObject.registerClass({
Globals.logger.log_debug(`\t\t\tActor to display ratios: ${actorToDisplayRatios}, offsets: ${actorToDisplayOffsets}`); 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}`); Globals.logger.log_debug(`\t\t\tMonitor ${index}: ${monitor.x}, ${monitor.y}, ${monitor.width}, ${monitor.height}`);
const containerActor = new Clutter.Actor({ const containerActor = new Clutter.Actor({
@ -706,11 +728,13 @@ export const VirtualDisplaysActor = GObject.registerClass({
const effect = new VirtualDisplayEffect({ const effect = new VirtualDisplayEffect({
focused_monitor_index: this.focused_monitor_index, focused_monitor_index: this.focused_monitor_index,
imu_snapshots: this.imu_snapshots, imu_snapshots: this.imu_snapshots,
pose_has_position: this.pose_has_position,
monitor_index: index, monitor_index: index,
monitor_details: monitor, monitor_details: monitor,
monitor_placements: this.monitor_placements, monitor_placements: this.monitor_placements,
fov_details: this.fov_details, fov_details: this.fov_details,
target_monitor: this.target_monitor, target_monitor: this.target_monitor,
display_size: this.display_size,
display_distance: this.display_distance, display_distance: this.display_distance,
display_distance_default: this._display_distance_default(), display_distance_default: this._display_distance_default(),
actor_to_display_ratios: actorToDisplayRatios, actor_to_display_ratios: actorToDisplayRatios,
@ -735,8 +759,10 @@ export const VirtualDisplaysActor = GObject.registerClass({
[ [
'monitor-placements', 'monitor-placements',
'display-size',
'fov-details', 'fov-details',
'imu-snapshots', 'imu-snapshots',
'pose-has-position',
'smooth-follow-enabled', 'smooth-follow-enabled',
'smooth-follow-toggle-epoch-ms', 'smooth-follow-toggle-epoch-ms',
'focused-monitor-index', 'focused-monitor-index',
@ -789,8 +815,13 @@ export const VirtualDisplaysActor = GObject.registerClass({
this.imu_snapshots.smooth_follow_origin.splice(0, 4) : this.imu_snapshots.smooth_follow_origin.splice(0, 4) :
this.imu_snapshots.pose_orientation.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( const focusedMonitorIndex = findFocusedMonitor(
currentOrientationQuat, currentOrientationQuat,
currentPosition,
this.monitor_placements.map(monitorVectors => monitorVectors.centerLook), this.monitor_placements.map(monitorVectors => monitorVectors.centerLook),
this.focused_monitor_index, this.focused_monitor_index,
this.display_distance / this._display_distance_default(), this.display_distance / this._display_distance_default(),
@ -827,34 +858,61 @@ export const VirtualDisplaysActor = GObject.registerClass({
this._redraw_timeline.start(); this._redraw_timeline.start();
} }
_size_adjusted_target_monitor() {
return this._all_monitors[0];
}
_display_distance_default() { _display_distance_default() {
return Math.max(this.display_distance, this.toggle_display_distance_start, this.toggle_display_distance_end); return Math.max(this.display_distance, this.toggle_display_distance_start, this.toggle_display_distance_end);
} }
_fov_details() { _fov_details() {
const aspect = this.target_monitor.width / this.target_monitor.height; 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;
// adjusted angles based on how far away the screens are e.g. a closer screen takes up a larger slice of our FOV // adjust FOV to a new focal point distance while keeping screens the same size
const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / this._display_distance_default()); // i.e. focus from pivot point to new screen distance, adjusted from lens at unit distance
const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / this._display_distance_default()); const defaultDistanceVerticalRadians = verticalConversions.fovRadiansAtDistance(
fovLengths.verticalRadians,
fovLengths.heightUnitDistance,
defaultDistance
);
const defaultDistanceHorizontalRadians = horizontalConversions.fovRadiansAtDistance(
fovLengths.horizontalRadians,
fovLengths.widthUnitDistance,
defaultDistance
);
// distance needed for the FOV-sized monitor to fill up the screen // distance needed for the FOV-sized monitor to fill up the screen, as measured from the lenses
const fullScreenDistance = this.target_monitor.height / 2 / Math.tan(fovRadians.vertical / 2); const lensToUnitDistancePixels = this.target_monitor.width / fovLengths.widthUnitDistance;
const lensDistancePixels = fullScreenDistance / (1.0 - Globals.data_stream.device_data.lensDistanceRatio) - fullScreenDistance;
// distance of a display at the default (most zoomed out) distance, plus the lens distance constant // distance from pivot point to lens
const lensToScreenDistance = this.target_monitor.height / 2 / Math.tan(defaultDistanceVerticalRadians / 2); const lensDistancePixels = lensToUnitDistancePixels * lensDistanceFactor;
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 { return {
widthPixels: this.target_monitor.width, widthPixels: this.target_monitor.width,
distanceAdjustedSize: this._distance_adjusted_size,
sizeAdjustedWidthPixels: this.target_monitor.width * this._distance_adjusted_size,
heightPixels: this.target_monitor.height, heightPixels: this.target_monitor.height,
sizeAdjustedHeightPixels: this.target_monitor.height * this._distance_adjusted_size,
defaultDistanceVerticalRadians, defaultDistanceVerticalRadians,
defaultDistanceHorizontalRadians, defaultDistanceHorizontalRadians,
lensDistancePixels, lensDistancePixels,
fullScreenDistancePixels,
completeScreenDistancePixels, completeScreenDistancePixels,
monitorWrappingScheme: this._actual_wrap_scheme(), monitorWrappingScheme,
curvedDisplay: this.curved_display 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 minY = Math.min(...this._all_monitors.map(monitor => monitor.y));
const maxY = Math.max(...this._all_monitors.map(monitor => monitor.y + monitor.height)); 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'; return 'horizontal';
} else { } else {
return 'vertical'; return 'vertical';
@ -878,27 +937,29 @@ export const VirtualDisplaysActor = GObject.registerClass({
_update_monitor_placements() { _update_monitor_placements() {
try { try {
const targetMonitor = this._size_adjusted_target_monitor();
const minX = Math.min(...this._all_monitors.map(monitor => monitor.x)); 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 maxX = Math.max(...this._all_monitors.map(monitor => monitor.x + monitor.width));
const minY = Math.min(...this._all_monitors.map(monitor => monitor.y)); const minY = Math.min(...this._all_monitors.map(monitor => monitor.y));
const maxY = Math.max(...this._all_monitors.map(monitor => monitor.y + monitor.height)); 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 // the beginning edges of the viewport if it's centered on all displays
const allDisplaysCenterXBegin = (minX + maxX) / 2 - this.target_monitor.width / 2; const allDisplaysCenterXBegin = (minX + maxX) / 2 - targetMonitor.width / 2;
const allDisplaysCenterYBegin = (minY + maxY) / 2 - this.target_monitor.height / 2; const allDisplaysCenterYBegin = (minY + maxY) / 2 - targetMonitor.height / 2;
const viewportXBegin = this.headset_display_as_viewport_center ? this.target_monitor.x : allDisplaysCenterXBegin; const viewportXBegin = this.headset_display_as_viewport_center ? targetMonitor.x : allDisplaysCenterXBegin;
const viewportYBegin = this.headset_display_as_viewport_center ? this.target_monitor.y : allDisplaysCenterYBegin; const viewportYBegin = this.headset_display_as_viewport_center ? targetMonitor.y : allDisplaysCenterYBegin;
this.fov_details = this._fov_details(); 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.monitor_placements = monitorsToPlacements(
this.fov_details, this.fov_details,
// shift all monitors so they center around the viewport center, then adjusted by the offsets // shift all monitors so they center around the viewport center, then adjusted by the offsets
this._all_monitors.map(monitor => ({ this._all_monitors.map(monitor => ({
x: monitor.x - viewportXBegin - this.viewport_offset_x * this.target_monitor.width, x: monitor.x - viewportXBegin - this.viewport_offset_x * targetMonitor.width,
y: monitor.y - viewportYBegin + this.viewport_offset_y * this.target_monitor.height, y: monitor.y - viewportYBegin + this.viewport_offset_y * targetMonitor.height,
width: monitor.width, width: monitor.width,
height: monitor.height 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_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); 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; 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(); 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 The size of the display
</description> </description>
</key> </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"> <key name="viewport-offset-x" type="d">
<default> <default>
0.0 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() effect_enable_switch = Gtk.Template.Child()
disable_physical_displays_switch = Gtk.Template.Child() disable_physical_displays_switch = Gtk.Template.Child()
display_zoom_on_focus_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_scale = Gtk.Template.Child()
follow_threshold_adjustment = Gtk.Template.Child() follow_threshold_adjustment = Gtk.Template.Child()
follow_mode_switch = 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_x_adjustment = Gtk.Template.Child()
viewport_offset_y_scale = Gtk.Template.Child() viewport_offset_y_scale = Gtk.Template.Child()
viewport_offset_y_adjustment = Gtk.Template.Child() viewport_offset_y_adjustment = Gtk.Template.Child()
units_menu = Gtk.Template.Child()
def __init__(self): def __init__(self):
super(Gtk.Box, self).__init__() super(Gtk.Box, self).__init__()
@ -90,7 +93,7 @@ class ConnectedDevice(Gtk.Box):
self.active = True self.active = True
self.all_enabled_state_inputs = [ self.all_enabled_state_inputs = [
self.display_zoom_on_focus_switch, self.display_zoom_on_focus_switch,
# self.display_size_scale, self.display_size_scale,
self.follow_mode_switch, self.follow_mode_switch,
self.follow_threshold_scale, self.follow_threshold_scale,
self.curved_display_switch, 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.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.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('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('widescreen-mode', self.widescreen_mode_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('curved-display', self.curved_display_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.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')) 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(), [ bind_shortcut_settings(self.get_parent(), [
[self.reassign_toggle_xr_effect_shortcut_button, self.toggle_xr_effect_shortcut_label], [self.reassign_toggle_xr_effect_shortcut_button, self.toggle_xr_effect_shortcut_label],
[self.reassign_recenter_display_shortcut_button, self.recenter_display_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.follow_mode_switch.connect('notify::active', self._refresh_follow_mode)
self.effect_enable_switch.connect('notify::active', self._handle_switch_enabled_state) 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 = ConfigManager.get_instance()
self.config_manager.connect('notify::breezy-desktop-enabled', self._handle_enabled_config) 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') 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: elif not widget.get_active() and is_zoom_on_focus_already_enabled:
self.settings.set_double('display-distance', toggle_display_distance_end) 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): def _handle_monitor_wrapping_scheme_setting_changed(self, settings, val):
self.monitor_wrapping_scheme_menu.set_active_id(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) self.display_zoom_on_focus_switch.set_active(should_zoom_on_focus_be_enabled)
def _set_focused_display_distance(self, distance): 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.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')) self.display_zoom_on_focus_switch.set_sensitive(distance != self.settings.get_double('toggle-display-distance-end'))
def _set_all_displays_distance(self, distance): 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.settings.set_double('toggle-display-distance-end', distance)
self.display_zoom_on_focus_switch.set_active(False) 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')) 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): 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 = DisplayDistanceDialog(settings_key, on_save_callback, title, subtitle, lower_limit, upper_limit)
dialog.set_transient_for(widget.get_ancestor(Gtk.Window)) 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): def _on_set_all_displays_distance(self, prev_distance, distance):
focused_display_distance = self.settings.get_double('toggle-display-distance-start') 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): if (distance < focused_display_distance):
self._set_focused_display_distance(distance) self._set_focused_display_distance(distance)
all_displays_distance = self.settings.get_double('toggle-display-distance-end')
self._set_all_displays_distance(distance) self._set_all_displays_distance(distance)
if prev_distance == focused_display_distance: # if we were at the unfocused distance, put us at the new unfocused distance
self.settings.set_double('display-distance', prev_distance) if prev_distance == all_displays_distance:
self.settings.set_double('display-distance', distance)
def _on_set_focused_display_distance(self, prev_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') all_displays_distance = self.settings.get_double('toggle-display-distance-end')
if (distance > all_displays_distance): if (distance > all_displays_distance):
self._set_all_displays_distance(distance) self._set_all_displays_distance(distance)
focused_display_distance = self.settings.get_double('toggle-display-distance-start')
self._set_focused_display_distance(distance) 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): def _save_custom_resolutions(self):
with open(self._custom_resolutions_file_path, 'w') as f: with open(self._custom_resolutions_file_path, 'w') as f:
json.dump(self._custom_resolution_options, f) json.dump(self._custom_resolution_options, f)

View File

@ -5,7 +5,6 @@ from .displaydistancedialogcontent import DisplayDistanceDialogContent
class DisplayDistanceDialog(Gtk.Dialog): class DisplayDistanceDialog(Gtk.Dialog):
__gtype_name__ = 'DisplayDistanceDialog' __gtype_name__ = 'DisplayDistanceDialog'
show_full_scale_button = Gtk.Template.Child()
save_button = Gtk.Template.Child() save_button = Gtk.Template.Child()
def __init__(self, settings_key, on_save_callback, title, subtitle, lower_limit, upper_limit): 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.on_save_callback = on_save_callback
self.set_title(title) 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.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): def _on_save_callback(self, prev_distance, distance):
self.on_save_callback(prev_distance, distance) self.on_save_callback(prev_distance, distance)
self.close() self.close()

View File

@ -1,5 +1,6 @@
from gi.repository import Gtk, Gio from gi.repository import Gtk, Gio
from .settingsmanager import SettingsManager from .settingsmanager import SettingsManager
from .statemanager import StateManager
import gettext import gettext
@ -14,7 +15,7 @@ class DisplayDistanceDialogContent(Gtk.Box):
display_distance_scale = Gtk.Template.Child() display_distance_scale = Gtk.Template.Child()
display_distance_adjustment = 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__() super(Gtk.Box, self).__init__()
self.init_template() self.init_template()
@ -22,37 +23,28 @@ class DisplayDistanceDialogContent(Gtk.Box):
self.on_save_callback = on_save_callback self.on_save_callback = on_save_callback
self.settings = SettingsManager.get_instance().settings self.settings = SettingsManager.get_instance().settings
self.state_manager = StateManager.get_instance()
self.prev_distance = self.settings.get_double('display-distance') 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.display_distance_scale.set_format_value_func(lambda scale, val: self._format_distance(val))
self.upper_limit_orig = self.display_distance_adjustment.get_upper() 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())
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)
save_button.connect('clicked', self._on_save_button_clicked) 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): 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> </child>
</object> </object>
</child> </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> <child>
<object class="AdwActionRow"> <object class="AdwActionRow">
<property name="title" translatable="yes"><!-- adjustment slider -->Follow threshold</property> <property name="title" translatable="yes"><!-- adjustment slider -->Follow threshold</property>
@ -554,6 +584,35 @@
<object class="AdwPreferencesGroup"> <object class="AdwPreferencesGroup">
<property name="title" translatable="yes"><!-- section heading for the advanced settings -->Advanced Settings</property> <property name="title" translatable="yes"><!-- section heading for the advanced settings -->Advanced Settings</property>
<property name="width-request">450</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> <child>
<object class="AdwActionRow"> <object class="AdwActionRow">
<property name="title" translatable="yes"><!-- feature that tries to the find best-fit monitor config -->Find optimal display config</property> <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="has-origin">false</property>
<property name="adjustment"> <property name="adjustment">
<object class="GtkAdjustment" id="display_distance_adjustment"> <object class="GtkAdjustment" id="display_distance_adjustment">
<property name="lower">0.2</property> <property name="lower">0.1</property>
<property name="upper">2.5</property> <property name="upper">1.5</property>
<property name="step-increment">0.01</property> <property name="step-increment">0.01</property>
<property name="value">1.05</property> <property name="value">1.05</property>
</object> </object>
</property> </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> </object>
</child> </child>
</template> </template>

View File

@ -4,15 +4,6 @@
<template class="DisplayDistanceDialog" parent="GtkDialog"> <template class="DisplayDistanceDialog" parent="GtkDialog">
<property name="modal">1</property> <property name="modal">1</property>
<property name="use-header-bar">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"> <child type="action">
<object class="GtkButton" id="save_button"> <object class="GtkButton" id="save_button">
<property name="label" translatable="yes">Done</property> <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), '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), '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), '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 _instance = None
@ -59,6 +61,8 @@ class StateManager(GObject.GObject):
self.license_present = False self.license_present = False
self.enabled_features = [] self.enabled_features = []
self.device_supports_sbs = False 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._running = True
self._refresh_state() 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('device-supports-sbs', self.state.get('sbs_mode_supported', False))
self.set_property('widescreen-mode', self.state.get('sbs_mode_enabled', 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() if self._running: threading.Timer(1.0, self._refresh_state).start()
def do_set_property(self, prop, value): def do_set_property(self, prop, value):
@ -115,6 +127,10 @@ class StateManager(GObject.GObject):
self.enabled_features = value self.enabled_features = value
if prop.name == 'device-supports-sbs': if prop.name == 'device-supports-sbs':
self.device_supports_sbs = value 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): def do_get_property(self, prop):
if prop.name == 'driver-running': if prop.name == 'driver-running':
@ -131,3 +147,7 @@ class StateManager(GObject.GObject):
return self.enabled_features return self.enabled_features
if prop.name == 'device-supports-sbs': 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