WIP pull shared JS logic out of gnome/kwin
This commit is contained in:
parent
0df496770c
commit
24f240f338
|
|
@ -1,78 +1 @@
|
||||||
export function degreeToRadian(degree) {
|
export { degreeToRadian, diagonalToCrossFOVs, fovConversionFns, applyQuaternionToVector, vectorMagnitude, normalizeVector } from '../../shared/js/math.js';
|
||||||
return degree * Math.PI / 180;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 unit distance of 1.0
|
|
||||||
const diagonalLengthUnitDistance = 2 * Math.tan(diagonalFOVRadians / 2);
|
|
||||||
|
|
||||||
// then convert to flat plane horizontal and vertical FOVs
|
|
||||||
const heightUnitDistance = diagonalLengthUnitDistance / Math.sqrt(1 + aspectRatio * aspectRatio);
|
|
||||||
const widthUnitDistance = heightUnitDistance * aspectRatio;
|
|
||||||
|
|
||||||
return {
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const segmentsPerRadian = 20.0 / degreeToRadian(90.0);
|
|
||||||
|
|
||||||
// displays are placed around a circle, these functions help determine radians and distances from the original
|
|
||||||
// FOV measurements scaled to the display dimensions
|
|
||||||
export const fovConversionFns = {
|
|
||||||
// convert curved FOV for flat displays
|
|
||||||
flat: {
|
|
||||||
// distance to an edge is the hypothenuse of the triangle where the opposite side is half the width of the reference fov screen
|
|
||||||
centerToFovEdgeDistance: (centerDistance, fovLength) => Math.sqrt(Math.pow(fovLength / 2, 2) + Math.pow(centerDistance, 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,
|
|
||||||
angleToLength: (fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) => {
|
|
||||||
return toAngleOpposite / toAngleAdjacent * screenDistance;
|
|
||||||
},
|
|
||||||
fovRadiansAtDistance: (fovRadians, unitLength, newScreenDistance) => {
|
|
||||||
return 2 * Math.atan(unitLength / 2 / newScreenDistance);
|
|
||||||
},
|
|
||||||
radiansToSegments: (screenRadians) => 1
|
|
||||||
},
|
|
||||||
|
|
||||||
// convert curved FOV for curved displays, scaling either involves no change or is linear
|
|
||||||
curved: {
|
|
||||||
centerToFovEdgeDistance: (centerDistance, fovLength) => centerDistance,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const applyQuaternionToVector = (vector, quaternion) => {
|
|
||||||
const t = [
|
|
||||||
2.0 * (quaternion[1] * vector[2] - quaternion[2] * vector[1]),
|
|
||||||
2.0 * (quaternion[2] * vector[0] - quaternion[0] * vector[2]),
|
|
||||||
2.0 * (quaternion[0] * vector[1] - quaternion[1] * vector[0])
|
|
||||||
];
|
|
||||||
return [
|
|
||||||
vector[0] + quaternion[3] * t[0] + quaternion[1] * t[2] - quaternion[2] * t[1],
|
|
||||||
vector[1] + quaternion[3] * t[1] + quaternion[2] * t[0] - quaternion[0] * t[2],
|
|
||||||
vector[2] + quaternion[3] * t[2] + quaternion[0] * t[1] - quaternion[1] * t[0]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = vectorMagnitude(vector);
|
|
||||||
return [vector[0] / length, vector[1] / length, vector[2] / length];
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -7,405 +7,13 @@ 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, vectorMagnitude } from './math.js';
|
import { degreeToRadian, diagonalToCrossFOVs, fovConversionFns } from './math.js';
|
||||||
|
import { findFocusedMonitor, monitorsToPlacements } from '../../shared/js/displayPlacement.js';
|
||||||
|
|
||||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||||
|
|
||||||
import Globals from './globals.js';
|
import Globals from './globals.js';
|
||||||
|
|
||||||
// if nothing is in focus, take it as soon as it crosses into the monitor's bounds
|
|
||||||
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 dimensions
|
|
||||||
function getMonitorDistance(fovDetails, lookUpPixels, lookWestPixels, monitorVector, monitorDetails, upAngleToLength, westAngleToLength) {
|
|
||||||
// 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,
|
|
||||||
monitorDistance,
|
|
||||||
monitorVector[2],
|
|
||||||
monitorVector[0]
|
|
||||||
) * distanceAdjustment;
|
|
||||||
const upPercentage = Math.abs(lookUpPixels * distanceAdjustment - vectorUpPixels) / monitorDetails.height;
|
|
||||||
|
|
||||||
const vectorWestPixels = westAngleToLength(
|
|
||||||
fovDetails.defaultDistanceHorizontalRadians,
|
|
||||||
fovDetails.widthPixels,
|
|
||||||
monitorDistance,
|
|
||||||
monitorVector[1],
|
|
||||||
monitorVector[0]
|
|
||||||
) * distanceAdjustment;
|
|
||||||
const westPercentage = Math.abs(lookWestPixels * distanceAdjustment - vectorWestPixels) / 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
|
|
||||||
* @param {boolean} smoothFollowEnabled - If true, always keep the current monitor in focus or choose the closest
|
|
||||||
* @param {Object} fovDetails - Contains reference widthPixels, heightPixels, horizontal and vertical radians, and pixel distance to the center of the screen
|
|
||||||
* @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, 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);
|
|
||||||
|
|
||||||
// TODO - right now we're using the curved functions to figure out distances even for flat monitors
|
|
||||||
// because it will account for the monitors facing towards us, but this will lose some accuracy
|
|
||||||
const upConversionFns = fovDetails.monitorWrappingScheme === 'vertical' ? fovConversionFns.curved : fovConversionFns.flat;
|
|
||||||
const lookUpPixels = upConversionFns.angleToLength(
|
|
||||||
fovDetails.defaultDistanceVerticalRadians,
|
|
||||||
fovDetails.heightPixels,
|
|
||||||
fovDetails.completeScreenDistancePixels,
|
|
||||||
rotatedLookVector[2],
|
|
||||||
rotatedLookVector[0]
|
|
||||||
);
|
|
||||||
const westConversionFns = fovDetails.monitorWrappingScheme === 'horizontal' ? fovConversionFns.curved : fovConversionFns.flat;
|
|
||||||
const lookWestPixels = westConversionFns.angleToLength(
|
|
||||||
fovDetails.defaultDistanceHorizontalRadians,
|
|
||||||
fovDetails.widthPixels,
|
|
||||||
fovDetails.completeScreenDistancePixels,
|
|
||||||
rotatedLookVector[1],
|
|
||||||
rotatedLookVector[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const focusedDistance = getMonitorDistance(
|
|
||||||
fovDetails,
|
|
||||||
lookUpPixels,
|
|
||||||
lookWestPixels,
|
|
||||||
vectorRelativeToLensPosition(monitorVectors[currentFocusedIndex]),
|
|
||||||
monitorsDetails[currentFocusedIndex],
|
|
||||||
upConversionFns.angleToLength,
|
|
||||||
westConversionFns.angleToLength
|
|
||||||
) * focusedMonitorDistance;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const distance = getMonitorDistance(
|
|
||||||
fovDetails,
|
|
||||||
lookUpPixels,
|
|
||||||
lookWestPixels,
|
|
||||||
vectorRelativeToLensPosition(monitorVector),
|
|
||||||
monitorsDetails[index],
|
|
||||||
upConversionFns.angleToLength,
|
|
||||||
westConversionFns.angleToLength
|
|
||||||
);
|
|
||||||
|
|
||||||
if (distance < closestDistance) {
|
|
||||||
closestIndex = index;
|
|
||||||
closestDistance = distance;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (smoothFollowEnabled || closestDistance < FOCUS_THRESHOLD) return closestIndex;
|
|
||||||
|
|
||||||
// neither the current nor the closest will take focus, unfocus all displays
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/***
|
|
||||||
* @returns {Object} - containing `begin`, `center`, and `end` radians for rotating the given monitor
|
|
||||||
*/
|
|
||||||
function monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorBeginPixel, monitorLengthPixels, lengthToRadianFn) {
|
|
||||||
// Monitor coordinates can become fractional due to size adjustment.
|
|
||||||
// If a monitor edge lands extremely close to a cached pixel key, snap to it;
|
|
||||||
// otherwise tiny negative gaps can cause us to subtract a full spacing interval.
|
|
||||||
let beginPixel = monitorBeginPixel;
|
|
||||||
const pixelEpsilon = Math.max(1e-6, Math.abs(monitorLengthPixels) * 1e-6);
|
|
||||||
|
|
||||||
let closestWrapPixel = beginPixel;
|
|
||||||
let closestWrap = cachedMonitorRadians[beginPixel];
|
|
||||||
if (closestWrap === undefined) {
|
|
||||||
closestWrapPixel = Object.keys(cachedMonitorRadians).reduce((previousPixel, currentPixel) => {
|
|
||||||
if (previousPixel === undefined) return currentPixel;
|
|
||||||
|
|
||||||
const currentDelta = currentPixel - monitorBeginPixel;
|
|
||||||
const previousDelta = previousPixel - monitorBeginPixel;
|
|
||||||
|
|
||||||
// always prefer an exact monitor width match
|
|
||||||
if (previousDelta % monitorLengthPixels !== 0) {
|
|
||||||
if (currentDelta % monitorLengthPixels === 0) return currentPixel;
|
|
||||||
|
|
||||||
// prefer placing a monitor to the right or below, even if there's a closer placement to the left or above
|
|
||||||
if (previousDelta < 0 && currentDelta > 0) return currentPixel;
|
|
||||||
|
|
||||||
// otherwise, just prefer the closest one
|
|
||||||
if (Math.abs(currentDelta) < Math.abs(previousDelta)) return currentPixel;
|
|
||||||
}
|
|
||||||
|
|
||||||
return previousPixel;
|
|
||||||
}, undefined);
|
|
||||||
closestWrap = cachedMonitorRadians[closestWrapPixel];
|
|
||||||
}
|
|
||||||
|
|
||||||
const closestWrapPixelNumber = Number(closestWrapPixel);
|
|
||||||
if (Number.isFinite(closestWrapPixelNumber) && Math.abs(closestWrapPixelNumber - beginPixel) < pixelEpsilon) {
|
|
||||||
beginPixel = closestWrapPixelNumber;
|
|
||||||
closestWrapPixel = closestWrapPixelNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
const spacingRadians = lengthToRadianFn(monitorSpacingPixels);
|
|
||||||
if (closestWrapPixel !== beginPixel) {
|
|
||||||
// there's a gap between the cached wrap value and this one
|
|
||||||
const gapPixels = beginPixel - closestWrapPixel;
|
|
||||||
const gapRadians = lengthToRadianFn(gapPixels);
|
|
||||||
|
|
||||||
// use Math.floor so if it's negative (this monitor is to the left of or above the closest) it will always
|
|
||||||
// compenstate for the spacing that's needed at the right/bottom
|
|
||||||
const appliedSpacingRadians = Math.floor(gapPixels / monitorLengthPixels) * spacingRadians;
|
|
||||||
|
|
||||||
// update the closestWrap value and cache it
|
|
||||||
closestWrap = closestWrap + gapRadians + appliedSpacingRadians;
|
|
||||||
closestWrapPixel = beginPixel;
|
|
||||||
cachedMonitorRadians[closestWrapPixel] = closestWrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
const monitorRadians = lengthToRadianFn(monitorLengthPixels);
|
|
||||||
const centerRadians = closestWrap + monitorRadians / 2;
|
|
||||||
const endRadians = closestWrap + monitorRadians;
|
|
||||||
|
|
||||||
// since we're computing the end values for this monitor, cache them too in case they line up with a future monitor
|
|
||||||
const nextMonitorPixel = beginPixel + monitorLengthPixels;
|
|
||||||
if (cachedMonitorRadians[nextMonitorPixel] === undefined)
|
|
||||||
cachedMonitorRadians[nextMonitorPixel] = endRadians + spacingRadians;
|
|
||||||
|
|
||||||
return {
|
|
||||||
begin: closestWrap,
|
|
||||||
center: centerRadians,
|
|
||||||
end: endRadians
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert the given monitor details into NWU vectors describing the center of the fully placed monitor,
|
|
||||||
* and the top-left of the partially placed monitor (minus only a single-axis rotation)
|
|
||||||
*
|
|
||||||
* @param {Object} fovDetails - contains reference widthPixels, heightPixels, horizontal and vertical radians,
|
|
||||||
* and distance to the center of the screen
|
|
||||||
* @param {Object[]} monitorDetailsList - contains x, y, width, height (coordinates from top-left)
|
|
||||||
* @param {number} monitorSpacing - visual spacing between monitors, as a percentage of the viewport width
|
|
||||||
* @returns {Object[]} - contains NWU vectors used for rendering and focused monitor detection
|
|
||||||
*/
|
|
||||||
function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
|
|
||||||
const monitorPlacements = [];
|
|
||||||
const cachedMonitorRadians = {};
|
|
||||||
|
|
||||||
Globals.logger.log_debug(`\t\t\tFOV Details: ${JSON.stringify(fovDetails)}`);
|
|
||||||
|
|
||||||
const conversionFns = fovDetails.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat;
|
|
||||||
|
|
||||||
if (fovDetails.monitorWrappingScheme === 'horizontal') {
|
|
||||||
// monitors wrap around us horizontally
|
|
||||||
|
|
||||||
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,
|
|
||||||
sideEdgeRadius,
|
|
||||||
targetWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
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.sizeAdjustedHeightPixels) * monitorSpacingPixels;
|
|
||||||
|
|
||||||
// offset for aligning this monitor's center with the fov-sized viewport's center
|
|
||||||
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;
|
|
||||||
|
|
||||||
monitorPlacements.push({
|
|
||||||
originalIndex,
|
|
||||||
centerNoRotate: [
|
|
||||||
monitorCenterRadius,
|
|
||||||
|
|
||||||
// west is centered about the FOV center
|
|
||||||
0,
|
|
||||||
|
|
||||||
// up is flat when wrapping horizontally
|
|
||||||
upCenterPixels
|
|
||||||
],
|
|
||||||
centerLook: [
|
|
||||||
// north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
|
|
||||||
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
|
|
||||||
|
|
||||||
// west is opposite where radius is the hypotenuse, using monitorWrapDetails.center as the radians
|
|
||||||
-monitorCenterRadius * Math.sin(monitorWrapDetails.center),
|
|
||||||
|
|
||||||
// up is flat when wrapping horizontally
|
|
||||||
upCenterPixels
|
|
||||||
],
|
|
||||||
rotationAngleRadians: {
|
|
||||||
x: 0,
|
|
||||||
y: -monitorWrapDetails.center
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (fovDetails.monitorWrappingScheme === 'vertical') {
|
|
||||||
// monitors wrap around us vertically
|
|
||||||
|
|
||||||
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,
|
|
||||||
topEdgeRadius,
|
|
||||||
targetHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
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.sizeAdjustedWidthPixels) * monitorSpacingPixels;
|
|
||||||
|
|
||||||
// offset for aligning this monitor's center with the fov-sized viewport's center
|
|
||||||
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;
|
|
||||||
|
|
||||||
monitorPlacements.push({
|
|
||||||
originalIndex,
|
|
||||||
centerNoRotate: [
|
|
||||||
monitorCenterRadius,
|
|
||||||
|
|
||||||
// west is flat when wrapping vertically
|
|
||||||
westCenterPixels,
|
|
||||||
|
|
||||||
// up is centered about the FOV center
|
|
||||||
0
|
|
||||||
],
|
|
||||||
centerLook: [
|
|
||||||
// north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
|
|
||||||
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
|
|
||||||
|
|
||||||
// west is flat when wrapping vertically
|
|
||||||
westCenterPixels,
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
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.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.sizeAdjustedWidthPixels) / 2;
|
|
||||||
const upCenterOffsetPixels = (monitorDetails.height - fovDetails.sizeAdjustedHeightPixels) / 2;
|
|
||||||
const westCenterPixels = westLeftPixels - westCenterOffsetPixels;
|
|
||||||
const upCenterPixels = upTopPixels - upCenterOffsetPixels;
|
|
||||||
|
|
||||||
monitorPlacements.push({
|
|
||||||
originalIndex: index,
|
|
||||||
centerNoRotate: [
|
|
||||||
fovDetails.completeScreenDistancePixels,
|
|
||||||
westCenterPixels,
|
|
||||||
upCenterPixels
|
|
||||||
],
|
|
||||||
centerLook: [
|
|
||||||
fovDetails.completeScreenDistancePixels,
|
|
||||||
westCenterPixels,
|
|
||||||
upCenterPixels
|
|
||||||
],
|
|
||||||
rotationAngleRadians: {
|
|
||||||
x: 0,
|
|
||||||
y: 0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// put them back in the original monitor order before returning
|
|
||||||
monitorPlacements.sort((a, b) => a.originalIndex - b.originalIndex);
|
|
||||||
|
|
||||||
Globals.logger.log_debug(`\t\t\tMonitor placements: ${JSON.stringify(monitorPlacements)}, cached values: ${JSON.stringify(cachedMonitorRadians)}`);
|
|
||||||
|
|
||||||
return monitorPlacements;
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort monitors based on wrapping scheme before determining their placements to avoid odd gaps
|
|
||||||
function horizontalMonitorSort(monitors) {
|
|
||||||
return monitors.map((monitor, index) => ({originalIndex: index, monitorDetails: monitor})).sort((a, b) => {
|
|
||||||
const aMon = a.monitorDetails;
|
|
||||||
const bMon = b.monitorDetails;
|
|
||||||
|
|
||||||
// First compare by y-coordinate to form rows (top to bottom)
|
|
||||||
if (aMon.y !== bMon.y) {
|
|
||||||
return aMon.y - bMon.y;
|
|
||||||
}
|
|
||||||
// Then compare by x-coordinate within the same row (left to right)
|
|
||||||
return aMon.x - bMon.x;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort monitors based on wrapping scheme before determining their placements to avoid odd gaps
|
|
||||||
function verticalMonitorSort(monitors) {
|
|
||||||
return monitors.map((monitor, index) => ({originalIndex: index, monitorDetails: monitor})).sort((a, b) => {
|
|
||||||
const aMon = a.monitorDetails;
|
|
||||||
const bMon = b.monitorDetails;
|
|
||||||
|
|
||||||
// First compare by x-coordinate to form columns (left to right)
|
|
||||||
if (aMon.x !== bMon.x) {
|
|
||||||
return aMon.x - bMon.x;
|
|
||||||
}
|
|
||||||
// Then compare by y-coordinate within the same column (top to bottom)
|
|
||||||
return aMon.y - bMon.y;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VirtualDisplaysActor = GObject.registerClass({
|
export const VirtualDisplaysActor = GObject.registerClass({
|
||||||
Properties: {
|
Properties: {
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,62 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import "../../../shared/js/math.js" as SharedMath
|
||||||
|
import "../../../shared/js/displayPlacement.js" as SharedPlacement
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
readonly property real focusThreshold: 0.95 / 2.0
|
readonly property real focusThreshold: SharedPlacement.FOCUS_THRESHOLD
|
||||||
readonly property real unfocusThreshold: 1.1 / 2.0
|
readonly property real unfocusThreshold: SharedPlacement.UNFOCUS_THRESHOLD
|
||||||
|
|
||||||
// Converts degrees to radians
|
// --- Qt coordinate-space helpers (not in shared because they use Qt types) ---
|
||||||
function degreeToRadian(degree) {
|
|
||||||
return degree * Math.PI / 180;
|
|
||||||
}
|
|
||||||
|
|
||||||
function radianToDegree(radian) {
|
function radianToDegree(radian) {
|
||||||
return radian * 180 / Math.PI;
|
return radian * 180 / Math.PI;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nwuToEusVector(vector) {
|
function nwuToEusVector(vector) {
|
||||||
// Converts NWU vector to EUS vector
|
|
||||||
return Qt.vector3d(-vector.y, vector.z, -vector.x);
|
return Qt.vector3d(-vector.y, vector.z, -vector.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
function eusToNwuVector(vector) {
|
function eusToNwuVector(vector) {
|
||||||
// Converts EUS vector to NWU vector
|
|
||||||
return Qt.vector3d(-vector.z, -vector.x, vector.y);
|
return Qt.vector3d(-vector.z, -vector.x, vector.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
function eusToNwuQuat(quaternion) {
|
function eusToNwuQuat(quaternion) {
|
||||||
// Converts EUS quaternion to NWU quaternion
|
|
||||||
return Qt.quaternion(quaternion.scalar, -quaternion.z, -quaternion.x, quaternion.y);
|
return Qt.quaternion(quaternion.scalar, -quaternion.z, -quaternion.x, quaternion.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts diagonal FOV in radians and aspect ratio to horizontal and vertical FOV measurements
|
function slerpVector(from, to, progress) {
|
||||||
function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) {
|
const inverseProgress = 1.0 - progress;
|
||||||
// first convert from a spherical FOV to a diagonal FOV on a flat plane at a unit distance of 1.0
|
return Qt.vector3d(
|
||||||
const diagonalLengthUnitDistance = 2 * Math.tan(diagonalFOVRadians / 2);
|
from.x * inverseProgress + to.x * progress,
|
||||||
|
from.y * inverseProgress + to.y * progress,
|
||||||
// then convert to flat plane horizontal and vertical FOVs
|
from.z * inverseProgress + to.z * progress
|
||||||
const heightUnitDistance = diagonalLengthUnitDistance / Math.sqrt(1 + aspectRatio * aspectRatio);
|
);
|
||||||
const widthUnitDistance = heightUnitDistance * aspectRatio;
|
|
||||||
|
|
||||||
return {
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function actualWrapScheme(screens, viewportWidth, viewportHeight) {
|
// --- Delegated to shared (re-exported for callers that go through this object) ---
|
||||||
const minX = Math.min(...screens.map(screen => screen.geometry.x));
|
|
||||||
const maxX = Math.max(...screens.map(screen => screen.geometry.x + screen.geometry.width));
|
|
||||||
const minY = Math.min(...screens.map(screen => screen.geometry.y));
|
|
||||||
const maxY = Math.max(...screens.map(screen => screen.geometry.y + screen.geometry.height));
|
|
||||||
|
|
||||||
if ((maxX - minX) / viewportWidth >= (maxY - minY) / viewportHeight) {
|
function degreeToRadian(degree) { return SharedMath.degreeToRadian(degree); }
|
||||||
return 'horizontal';
|
function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) { return SharedMath.diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio); }
|
||||||
} else {
|
|
||||||
return 'vertical';
|
readonly property var fovConversionFns: SharedMath.fovConversionFns
|
||||||
}
|
|
||||||
|
function monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorBeginPixel, monitorLengthPixels, lengthToRadianFn) {
|
||||||
|
return SharedPlacement.monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorBeginPixel, monitorLengthPixels, lengthToRadianFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FOV / wrap-scheme helpers ---
|
||||||
|
|
||||||
|
function actualWrapScheme(screens, viewportWidth, viewportHeight) {
|
||||||
|
const monitors = screens.map(screen => ({
|
||||||
|
x: screen.geometry.x, y: screen.geometry.y,
|
||||||
|
width: screen.geometry.width, height: screen.geometry.height
|
||||||
|
}));
|
||||||
|
return SharedPlacement.autoDetectWrapScheme(monitors, viewportWidth, viewportHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio, defaultDisplayDistance, wrappingChoice, distanceAdjustedSize) {
|
function buildFovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio, defaultDisplayDistance, wrappingChoice, distanceAdjustedSize) {
|
||||||
const aspect = viewportWidth / viewportHeight;
|
const aspect = viewportWidth / viewportHeight;
|
||||||
const fovLengths = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect);
|
const fovLengths = SharedMath.diagonalToCrossFOVs(SharedMath.degreeToRadian(viewportDiagonalFOV), aspect);
|
||||||
|
|
||||||
let monitorWrappingScheme = actualWrapScheme(screens, viewportWidth, viewportHeight);
|
let monitorWrappingScheme = actualWrapScheme(screens, viewportWidth, viewportHeight);
|
||||||
if (wrappingChoice === 1) monitorWrappingScheme = 'horizontal';
|
if (wrappingChoice === 1) monitorWrappingScheme = 'horizontal';
|
||||||
|
|
@ -74,8 +65,8 @@ QtObject {
|
||||||
|
|
||||||
const lensDistanceComplement = 1.0 - lensDistanceRatio;
|
const lensDistanceComplement = 1.0 - lensDistanceRatio;
|
||||||
const lensDistanceFactor = (1.0 / lensDistanceComplement) - 1.0;
|
const lensDistanceFactor = (1.0 / lensDistanceComplement) - 1.0;
|
||||||
const horizontalConversions = effect.curvedDisplay && monitorWrappingScheme === 'horizontal' ? fovConversionFns.curved : fovConversionFns.flat;
|
const horizontalConversions = effect.curvedDisplay && monitorWrappingScheme === 'horizontal' ? SharedMath.fovConversionFns.curved : SharedMath.fovConversionFns.flat;
|
||||||
const verticalConversions = effect.curvedDisplay && monitorWrappingScheme === 'vertical' ? fovConversionFns.curved : fovConversionFns.flat;
|
const verticalConversions = effect.curvedDisplay && monitorWrappingScheme === 'vertical' ? SharedMath.fovConversionFns.curved : SharedMath.fovConversionFns.flat;
|
||||||
|
|
||||||
const defaultDistanceVerticalRadians = verticalConversions.fovRadiansAtDistance(
|
const defaultDistanceVerticalRadians = verticalConversions.fovRadiansAtDistance(
|
||||||
fovLengths.verticalRadians,
|
fovLengths.verticalRadians,
|
||||||
|
|
@ -88,16 +79,9 @@ QtObject {
|
||||||
defaultDisplayDistance
|
defaultDisplayDistance
|
||||||
);
|
);
|
||||||
|
|
||||||
// distance needed for the FOV-sized monitor to fill up the screen, as measured from the lenses
|
|
||||||
const lensToUnitDistancePixels = viewportWidth / fovLengths.widthUnitDistance;
|
const lensToUnitDistancePixels = viewportWidth / fovLengths.widthUnitDistance;
|
||||||
|
|
||||||
// distance from pivot point to lens
|
|
||||||
const lensDistancePixels = lensToUnitDistancePixels * lensDistanceFactor;
|
const lensDistancePixels = lensToUnitDistancePixels * lensDistanceFactor;
|
||||||
|
|
||||||
// distance from pivot point to full screen (monitor at unit distance from lens)
|
|
||||||
const fullScreenDistancePixels = lensToUnitDistancePixels + lensDistancePixels;
|
const fullScreenDistancePixels = lensToUnitDistancePixels + lensDistancePixels;
|
||||||
|
|
||||||
// distance of a display at the default (most zoomed out) distance from the pivot point
|
|
||||||
const completeScreenDistancePixels = fullScreenDistancePixels * defaultDisplayDistance;
|
const completeScreenDistancePixels = fullScreenDistancePixels * defaultDisplayDistance;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -116,362 +100,43 @@ QtObject {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility constant
|
// Wraps SharedPlacement.monitorsToPlacements, converting plain-array vectors to Qt.vector3d.
|
||||||
readonly property real segmentsPerRadian: 20.0 / degreeToRadian(90.0)
|
|
||||||
|
|
||||||
// FOV conversion functions for flat and curved displays
|
|
||||||
property var fovConversionFns: ({
|
|
||||||
flat: {
|
|
||||||
centerToFovEdgeDistance: function(centerDistance, fovLength) {
|
|
||||||
return Math.sqrt(Math.pow(fovLength / 2, 2) + Math.pow(centerDistance, 2));
|
|
||||||
},
|
|
||||||
fovEdgeToScreenCenterDistance: function(edgeDistance, screenLength) {
|
|
||||||
return Math.sqrt(Math.pow(edgeDistance, 2) - Math.pow(screenLength / 2, 2));
|
|
||||||
},
|
|
||||||
lengthToRadians: function(fovRadians, fovLength, screenEdgeDistance, toLength) {
|
|
||||||
return Math.asin(toLength / 2 / screenEdgeDistance) * 2;
|
|
||||||
},
|
|
||||||
angleToLength: function(fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) {
|
|
||||||
return toAngleOpposite / toAngleAdjacent * screenDistance;
|
|
||||||
},
|
|
||||||
fovRadiansAtDistance: function(fovRadians, unitLength, newScreenDistance) {
|
|
||||||
return 2 * Math.atan(unitLength / 2 / newScreenDistance);
|
|
||||||
},
|
|
||||||
radiansToSegments: function(screenRadians) { return 1; }
|
|
||||||
},
|
|
||||||
curved: {
|
|
||||||
centerToFovEdgeDistance: function(centerDistance, fovLength) {
|
|
||||||
return centerDistance;
|
|
||||||
},
|
|
||||||
fovEdgeToScreenCenterDistance: function(edgeDistance, screenLength) {
|
|
||||||
return edgeDistance;
|
|
||||||
},
|
|
||||||
lengthToRadians: function(fovRadians, fovLength, screenEdgeDistance, toLength) {
|
|
||||||
return fovRadians / fovLength * toLength;
|
|
||||||
},
|
|
||||||
angleToLength: function(fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) {
|
|
||||||
return fovLength / fovRadians * Math.atan2(toAngleOpposite, toAngleAdjacent);
|
|
||||||
},
|
|
||||||
fovRadiansAtDistance: function(fovRadians, unitLength, newScreenDistance) {
|
|
||||||
return fovRadians / newScreenDistance;
|
|
||||||
},
|
|
||||||
radiansToSegments: function(screenRadians) {
|
|
||||||
return Math.ceil(screenRadians * segmentsPerRadian);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorBeginPixel, monitorLengthPixels, lengthToRadianFn) {
|
|
||||||
var closestWrapPixel = monitorBeginPixel;
|
|
||||||
var closestWrap = cachedMonitorRadians[monitorBeginPixel];
|
|
||||||
if (closestWrap === undefined) {
|
|
||||||
var keys = Object.keys(cachedMonitorRadians);
|
|
||||||
closestWrapPixel = keys.reduce(function(previousPixel, currentPixel) {
|
|
||||||
if (previousPixel === undefined) return currentPixel;
|
|
||||||
|
|
||||||
var currentDelta = currentPixel - monitorBeginPixel;
|
|
||||||
var previousDelta = previousPixel - monitorBeginPixel;
|
|
||||||
|
|
||||||
if (previousDelta % monitorLengthPixels !== 0) {
|
|
||||||
if (currentDelta % monitorLengthPixels === 0) return currentPixel;
|
|
||||||
if (previousDelta < 0 && currentDelta > 0) return currentPixel;
|
|
||||||
if (Math.abs(currentDelta) < Math.abs(previousDelta)) return currentPixel;
|
|
||||||
}
|
|
||||||
return previousPixel;
|
|
||||||
}, undefined);
|
|
||||||
closestWrap = cachedMonitorRadians[closestWrapPixel];
|
|
||||||
}
|
|
||||||
|
|
||||||
var spacingRadians = lengthToRadianFn(monitorSpacingPixels);
|
|
||||||
if (closestWrapPixel !== monitorBeginPixel) {
|
|
||||||
var gapPixels = monitorBeginPixel - closestWrapPixel;
|
|
||||||
var gapRadians = lengthToRadianFn(gapPixels);
|
|
||||||
var appliedSpacingRadians = Math.floor(gapPixels / monitorLengthPixels) * spacingRadians;
|
|
||||||
closestWrap = closestWrap + gapRadians + appliedSpacingRadians;
|
|
||||||
closestWrapPixel = monitorBeginPixel;
|
|
||||||
cachedMonitorRadians[closestWrapPixel] = closestWrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
var monitorRadians = lengthToRadianFn(monitorLengthPixels);
|
|
||||||
var centerRadians = closestWrap + monitorRadians / 2;
|
|
||||||
var endRadians = closestWrap + monitorRadians;
|
|
||||||
|
|
||||||
var nextMonitorPixel = monitorBeginPixel + monitorLengthPixels;
|
|
||||||
if (cachedMonitorRadians[nextMonitorPixel] === undefined)
|
|
||||||
cachedMonitorRadians[nextMonitorPixel] = endRadians + spacingRadians;
|
|
||||||
|
|
||||||
return {
|
|
||||||
begin: closestWrap,
|
|
||||||
center: centerRadians,
|
|
||||||
end: endRadians
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function horizontalMonitorSort(monitors) {
|
|
||||||
return monitors.map(function(monitor, index) {
|
|
||||||
return { originalIndex: index, monitorDetails: monitor };
|
|
||||||
}).sort(function(a, b) {
|
|
||||||
var aMon = a.monitorDetails;
|
|
||||||
var bMon = b.monitorDetails;
|
|
||||||
if (aMon.y !== bMon.y) return aMon.y - bMon.y;
|
|
||||||
return aMon.x - bMon.x;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function verticalMonitorSort(monitors) {
|
|
||||||
return monitors.map(function(monitor, index) {
|
|
||||||
return { originalIndex: index, monitorDetails: monitor };
|
|
||||||
}).sort(function(a, b) {
|
|
||||||
var aMon = a.monitorDetails;
|
|
||||||
var bMon = b.monitorDetails;
|
|
||||||
if (aMon.x !== bMon.x) return aMon.x - bMon.x;
|
|
||||||
return aMon.y - bMon.y;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// fovDetails: { widthPixels, heightPixels, defaultDistanceHorizontalRadians, defaultDistanceVerticalRadians, completeScreenDistancePixels, monitorWrappingScheme, curvedDisplay }
|
|
||||||
// monitorDetailsList: [{x, y, width, height}, ...]
|
|
||||||
// monitorSpacing: number (percentage, e.g. 0.05 for 5%)
|
|
||||||
function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
|
function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
|
||||||
var monitorPlacements = [];
|
return SharedPlacement.monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing)
|
||||||
var cachedMonitorRadians = {};
|
.map(p => ({
|
||||||
|
originalIndex: p.originalIndex,
|
||||||
var conversionFns = fovDetails.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat;
|
monitorCenterNorth: p.monitorCenterNorth,
|
||||||
|
centerNoRotate: Qt.vector3d(p.centerNoRotate[0], p.centerNoRotate[1], p.centerNoRotate[2]),
|
||||||
if (fovDetails.monitorWrappingScheme === 'horizontal') {
|
centerLook: Qt.vector3d(p.centerLook[0], p.centerLook[1], p.centerLook[2]),
|
||||||
// monitors wrap around us horizontally
|
rotationAngleRadians: p.rotationAngleRadians
|
||||||
|
}));
|
||||||
var sideEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.sizeAdjustedWidthPixels);
|
|
||||||
var monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedWidthPixels;
|
|
||||||
|
|
||||||
// targetWidth is assumed to aleady be size adjusted
|
|
||||||
var lengthToRadianFn = function(targetWidth) {
|
|
||||||
return conversionFns.lengthToRadians(
|
|
||||||
fovDetails.defaultDistanceHorizontalRadians,
|
|
||||||
fovDetails.widthPixels,
|
|
||||||
sideEdgeRadius,
|
|
||||||
targetWidth
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
cachedMonitorRadians[0] = -lengthToRadianFn(fovDetails.sizeAdjustedWidthPixels) / 2;
|
|
||||||
horizontalMonitorSort(monitorDetailsList).forEach(function(obj) {
|
|
||||||
var monitorDetails = obj.monitorDetails;
|
|
||||||
var originalIndex = obj.originalIndex;
|
|
||||||
var monitorWrapDetails = monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorDetails.x, monitorDetails.width, lengthToRadianFn);
|
|
||||||
var monitorCenterRadius = conversionFns.fovEdgeToScreenCenterDistance(sideEdgeRadius, monitorDetails.width);
|
|
||||||
var upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.sizeAdjustedHeightPixels) * monitorSpacingPixels;
|
|
||||||
var upCenterOffsetPixels = (monitorDetails.height - fovDetails.sizeAdjustedHeightPixels) / 2;
|
|
||||||
var upCenterPixels = upTopPixels - upCenterOffsetPixels;
|
|
||||||
|
|
||||||
monitorPlacements.push({
|
|
||||||
originalIndex: originalIndex,
|
|
||||||
monitorCenterNorth: monitorCenterRadius,
|
|
||||||
centerNoRotate: Qt.vector3d(
|
|
||||||
monitorCenterRadius,
|
|
||||||
0,
|
|
||||||
upCenterPixels
|
|
||||||
),
|
|
||||||
centerLook: Qt.vector3d(
|
|
||||||
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
|
|
||||||
-monitorCenterRadius * Math.sin(monitorWrapDetails.center),
|
|
||||||
upCenterPixels
|
|
||||||
),
|
|
||||||
rotationAngleRadians: {
|
|
||||||
x: 0,
|
|
||||||
y: -monitorWrapDetails.center
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (fovDetails.monitorWrappingScheme === 'vertical') {
|
|
||||||
var topEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.sizeAdjustedHeightPixels);
|
|
||||||
var monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedHeightPixels;
|
|
||||||
var lengthToRadianFn = function(targetHeight) {
|
|
||||||
return conversionFns.lengthToRadians(
|
|
||||||
fovDetails.defaultDistanceVerticalRadians,
|
|
||||||
fovDetails.heightPixels,
|
|
||||||
topEdgeRadius,
|
|
||||||
targetHeight
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
cachedMonitorRadians[0] = -lengthToRadianFn(fovDetails.sizeAdjustedHeightPixels) / 2;
|
|
||||||
verticalMonitorSort(monitorDetailsList).forEach(function(obj) {
|
|
||||||
var monitorDetails = obj.monitorDetails;
|
|
||||||
var originalIndex = obj.originalIndex;
|
|
||||||
var monitorWrapDetails = monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorDetails.y, monitorDetails.height, lengthToRadianFn);
|
|
||||||
var monitorCenterRadius = conversionFns.fovEdgeToScreenCenterDistance(topEdgeRadius, monitorDetails.height);
|
|
||||||
var westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.sizeAdjustedWidthPixels) * monitorSpacingPixels;
|
|
||||||
var westCenterOffsetPixels = (monitorDetails.width - fovDetails.sizeAdjustedWidthPixels) / 2;
|
|
||||||
var westCenterPixels = westLeftPixels - westCenterOffsetPixels;
|
|
||||||
|
|
||||||
monitorPlacements.push({
|
|
||||||
originalIndex: originalIndex,
|
|
||||||
monitorCenterNorth: monitorCenterRadius,
|
|
||||||
centerNoRotate: Qt.vector3d(
|
|
||||||
monitorCenterRadius,
|
|
||||||
westCenterPixels,
|
|
||||||
0
|
|
||||||
),
|
|
||||||
centerLook: Qt.vector3d(
|
|
||||||
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
|
|
||||||
westCenterPixels,
|
|
||||||
-monitorCenterRadius * Math.sin(monitorWrapDetails.center)
|
|
||||||
),
|
|
||||||
rotationAngleRadians: {
|
|
||||||
x: -monitorWrapDetails.center,
|
|
||||||
y: 0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
var monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedWidthPixels;
|
|
||||||
monitorDetailsList.forEach(function(monitorDetails, index) {
|
|
||||||
var upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.sizeAdjustedHeightPixels) * monitorSpacingPixels;
|
|
||||||
var westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.sizeAdjustedWidthPixels) * monitorSpacingPixels;
|
|
||||||
var westCenterOffsetPixels = (monitorDetails.width - fovDetails.sizeAdjustedWidthPixels) / 2;
|
|
||||||
var upCenterOffsetPixels = (monitorDetails.height - fovDetails.sizeAdjustedHeightPixels) / 2;
|
|
||||||
var westCenterPixels = westLeftPixels - westCenterOffsetPixels;
|
|
||||||
var upCenterPixels = upTopPixels - upCenterOffsetPixels;
|
|
||||||
|
|
||||||
monitorPlacements.push({
|
|
||||||
originalIndex: index,
|
|
||||||
monitorCenterNorth: fovDetails.completeScreenDistancePixels,
|
|
||||||
centerNoRotate: Qt.vector3d(
|
|
||||||
fovDetails.completeScreenDistancePixels,
|
|
||||||
westCenterPixels,
|
|
||||||
upCenterPixels
|
|
||||||
),
|
|
||||||
centerLook: Qt.vector3d(
|
|
||||||
fovDetails.completeScreenDistancePixels,
|
|
||||||
westCenterPixels,
|
|
||||||
upCenterPixels
|
|
||||||
),
|
|
||||||
rotationAngleRadians: {
|
|
||||||
x: 0,
|
|
||||||
y: 0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// put them back in the original monitor order before returning
|
|
||||||
monitorPlacements.sort(function(a, b) { return a.originalIndex - b.originalIndex; });
|
|
||||||
|
|
||||||
return monitorPlacements;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
// 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 = monitorVector.length();
|
|
||||||
const distanceAdjustment = monitorDistance / fovDetails.completeScreenDistancePixels;
|
|
||||||
|
|
||||||
var vectorUpPixels = upAngleToLength(
|
|
||||||
fovDetails.defaultDistanceVerticalRadians,
|
|
||||||
fovDetails.heightPixels,
|
|
||||||
monitorDistance,
|
|
||||||
monitorVector.z,
|
|
||||||
monitorVector.x
|
|
||||||
) * distanceAdjustment;
|
|
||||||
var upPercentage = Math.abs(lookUpPixels * distanceAdjustment - vectorUpPixels) / monitorDetails.height;
|
|
||||||
|
|
||||||
var vectorWestPixels = westAngleToLength(
|
|
||||||
fovDetails.defaultDistanceHorizontalRadians,
|
|
||||||
fovDetails.widthPixels,
|
|
||||||
monitorDistance,
|
|
||||||
monitorVector.y,
|
|
||||||
monitorVector.x
|
|
||||||
) * distanceAdjustment;
|
|
||||||
var westPercentage = Math.abs(lookWestPixels * distanceAdjustment - vectorWestPixels) / monitorDetails.width;
|
|
||||||
|
|
||||||
// how close we are to any edge is the largest of the two percentages
|
|
||||||
return Math.max(upPercentage, westPercentage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wraps SharedPlacement.findFocusedMonitor, adapting Qt types to plain arrays.
|
||||||
function findFocusedMonitor(quaternion, position, monitorVectors, currentFocusedIndex, smoothFollowEnabled, fovDetails, monitorsDetails) {
|
function findFocusedMonitor(quaternion, position, monitorVectors, currentFocusedIndex, smoothFollowEnabled, fovDetails, monitorsDetails) {
|
||||||
if (currentFocusedIndex !== -1 && smoothFollowEnabled) return currentFocusedIndex;
|
// convert Qt.quaternion [scalar, x, y, z] → shared [x, y, z, w]
|
||||||
|
const quatArray = [quaternion.x, quaternion.y, quaternion.z, quaternion.scalar];
|
||||||
|
|
||||||
var lookVector = Qt.vector3d(1.0, 0.0, 0.0); // NWU vector pointing to the center of the screen
|
// convert Qt.vector3d position → plain array
|
||||||
var rotatedLookVector = quaternion.times(lookVector);
|
const posArray = [position.x, position.y, position.z];
|
||||||
|
|
||||||
// TODO - right now we're using the curved functions to figure out distances even for flat monitors
|
// convert Qt.vector3d monitor centerLook vectors → plain arrays
|
||||||
// because it will account for the monitors facing towards us, but this will lose some accuracy
|
const vectorArrays = monitorVectors.map(v => [v.x, v.y, v.z]);
|
||||||
var upConversionFns = fovDetails.monitorWrappingScheme === "vertical" ? fovConversionFns.curved : fovConversionFns.flat;
|
|
||||||
var lookUpPixels = upConversionFns.angleToLength(
|
// adapt monitorsDetails from screen.geometry shape to plain {x,y,width,height}
|
||||||
fovDetails.defaultDistanceVerticalRadians,
|
const detailsArrays = monitorsDetails.map(d => ({
|
||||||
fovDetails.heightPixels,
|
x: d.x, y: d.y, width: d.width, height: d.height
|
||||||
fovDetails.completeScreenDistancePixels,
|
}));
|
||||||
rotatedLookVector.z,
|
|
||||||
rotatedLookVector.x
|
// KWin passes focusedDisplayDistance / allDisplaysDistance inline; use 1.0 as neutral default
|
||||||
|
// and rely on the caller to scale if needed (matches legacy behaviour where ratio wasn't passed)
|
||||||
|
return SharedPlacement.findFocusedMonitor(
|
||||||
|
quatArray, posArray, vectorArrays,
|
||||||
|
currentFocusedIndex,
|
||||||
|
effect.focusedDisplayDistance / effect.allDisplaysDistance,
|
||||||
|
smoothFollowEnabled,
|
||||||
|
fovDetails,
|
||||||
|
detailsArrays
|
||||||
);
|
);
|
||||||
var westConversionFns = fovDetails.monitorWrappingScheme === "horizontal" ? fovConversionFns.curved : fovConversionFns.flat;
|
|
||||||
var lookWestPixels = westConversionFns.angleToLength(
|
|
||||||
fovDetails.defaultDistanceHorizontalRadians,
|
|
||||||
fovDetails.widthPixels,
|
|
||||||
fovDetails.completeScreenDistancePixels,
|
|
||||||
rotatedLookVector.y,
|
|
||||||
rotatedLookVector.x
|
|
||||||
);
|
|
||||||
|
|
||||||
function vectorRelativeToPosition(vector) {
|
|
||||||
return vector.minus(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check current focused monitor first
|
|
||||||
if (currentFocusedIndex !== -1) {
|
|
||||||
var focusedDistance = getMonitorDistance(
|
|
||||||
fovDetails,
|
|
||||||
lookUpPixels,
|
|
||||||
lookWestPixels,
|
|
||||||
vectorRelativeToPosition(monitorVectors[currentFocusedIndex]),
|
|
||||||
monitorsDetails[currentFocusedIndex],
|
|
||||||
upConversionFns.angleToLength,
|
|
||||||
westConversionFns.angleToLength
|
|
||||||
) * effect.focusedDisplayDistance / effect.allDisplaysDistance;
|
|
||||||
|
|
||||||
if (focusedDistance < unfocusThreshold) return currentFocusedIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
var closestIndex = -1;
|
|
||||||
var closestDistance = Number.POSITIVE_INFINITY;
|
|
||||||
|
|
||||||
// Find the closest monitor
|
|
||||||
for (var i = 0; i < monitorVectors.length; ++i) {
|
|
||||||
if (i === currentFocusedIndex)
|
|
||||||
continue;
|
|
||||||
var distance = getMonitorDistance(
|
|
||||||
fovDetails,
|
|
||||||
lookUpPixels,
|
|
||||||
lookWestPixels,
|
|
||||||
vectorRelativeToPosition(monitorVectors[i]),
|
|
||||||
monitorsDetails[i],
|
|
||||||
upConversionFns.angleToLength,
|
|
||||||
westConversionFns.angleToLength
|
|
||||||
);
|
|
||||||
|
|
||||||
if (distance < closestDistance) {
|
|
||||||
closestIndex = i;
|
|
||||||
closestDistance = distance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (smoothFollowEnabled || closestDistance < focusThreshold)
|
|
||||||
return closestIndex;
|
|
||||||
|
|
||||||
// Unfocus all displays
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function slerpVector(from, to, progress) {
|
|
||||||
const inverseProgress = 1.0 - progress;
|
|
||||||
const finalVector = Qt.vector3d(
|
|
||||||
from.x * inverseProgress + to.x * progress,
|
|
||||||
from.y * inverseProgress + to.y * progress,
|
|
||||||
from.z * inverseProgress + to.z * progress
|
|
||||||
);
|
|
||||||
|
|
||||||
return finalVector;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,365 @@
|
||||||
|
import { applyQuaternionToVector, fovConversionFns, vectorMagnitude } from './math.js';
|
||||||
|
|
||||||
|
// if nothing is in focus, take it as soon as it crosses into the monitor's bounds
|
||||||
|
export const FOCUS_THRESHOLD = 0.95 / 2.0;
|
||||||
|
|
||||||
|
// if we leave the monitor with some margin, unfocus even if no other monitor is in focus
|
||||||
|
export const UNFOCUS_THRESHOLD = 1.1 / 2.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the known radian positions of previously-placed monitors, compute the begin/center/end
|
||||||
|
* radian positions for one monitor along a wrapped axis.
|
||||||
|
*
|
||||||
|
* All vector arguments use plain JS arrays; callers on Qt platforms convert before/after.
|
||||||
|
*
|
||||||
|
* @param {Object} cachedMonitorRadians - mutable pixel→radian cache shared across all monitors in one axis
|
||||||
|
* @param {number} monitorSpacingPixels
|
||||||
|
* @param {number} monitorBeginPixel
|
||||||
|
* @param {number} monitorLengthPixels
|
||||||
|
* @param {function} lengthToRadianFn
|
||||||
|
* @returns {{begin: number, center: number, end: number}}
|
||||||
|
*/
|
||||||
|
export function monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorBeginPixel, monitorLengthPixels, lengthToRadianFn) {
|
||||||
|
// Monitor coordinates can become fractional due to size adjustment.
|
||||||
|
// If a monitor edge lands extremely close to a cached pixel key, snap to it;
|
||||||
|
// otherwise tiny negative gaps can cause us to subtract a full spacing interval.
|
||||||
|
let beginPixel = monitorBeginPixel;
|
||||||
|
const pixelEpsilon = Math.max(1e-6, Math.abs(monitorLengthPixels) * 1e-6);
|
||||||
|
|
||||||
|
let closestWrapPixel = beginPixel;
|
||||||
|
let closestWrap = cachedMonitorRadians[beginPixel];
|
||||||
|
if (closestWrap === undefined) {
|
||||||
|
closestWrapPixel = Object.keys(cachedMonitorRadians).reduce((previousPixel, currentPixel) => {
|
||||||
|
if (previousPixel === undefined) return currentPixel;
|
||||||
|
|
||||||
|
const currentDelta = currentPixel - monitorBeginPixel;
|
||||||
|
const previousDelta = previousPixel - monitorBeginPixel;
|
||||||
|
|
||||||
|
// always prefer an exact monitor width match
|
||||||
|
if (previousDelta % monitorLengthPixels !== 0) {
|
||||||
|
if (currentDelta % monitorLengthPixels === 0) return currentPixel;
|
||||||
|
|
||||||
|
// prefer placing a monitor to the right or below, even if there's a closer placement to the left or above
|
||||||
|
if (previousDelta < 0 && currentDelta > 0) return currentPixel;
|
||||||
|
|
||||||
|
// otherwise, just prefer the closest one
|
||||||
|
if (Math.abs(currentDelta) < Math.abs(previousDelta)) return currentPixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return previousPixel;
|
||||||
|
}, undefined);
|
||||||
|
closestWrap = cachedMonitorRadians[closestWrapPixel];
|
||||||
|
}
|
||||||
|
|
||||||
|
const closestWrapPixelNumber = Number(closestWrapPixel);
|
||||||
|
if (Number.isFinite(closestWrapPixelNumber) && Math.abs(closestWrapPixelNumber - beginPixel) < pixelEpsilon) {
|
||||||
|
beginPixel = closestWrapPixelNumber;
|
||||||
|
closestWrapPixel = closestWrapPixelNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spacingRadians = lengthToRadianFn(monitorSpacingPixels);
|
||||||
|
if (closestWrapPixel !== beginPixel) {
|
||||||
|
// there's a gap between the cached wrap value and this one
|
||||||
|
const gapPixels = beginPixel - closestWrapPixel;
|
||||||
|
const gapRadians = lengthToRadianFn(gapPixels);
|
||||||
|
|
||||||
|
// use Math.floor so if it's negative (this monitor is to the left of or above the closest) it will always
|
||||||
|
// compensate for the spacing that's needed at the right/bottom
|
||||||
|
const appliedSpacingRadians = Math.floor(gapPixels / monitorLengthPixels) * spacingRadians;
|
||||||
|
|
||||||
|
closestWrap = closestWrap + gapRadians + appliedSpacingRadians;
|
||||||
|
closestWrapPixel = beginPixel;
|
||||||
|
cachedMonitorRadians[closestWrapPixel] = closestWrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monitorRadians = lengthToRadianFn(monitorLengthPixels);
|
||||||
|
const centerRadians = closestWrap + monitorRadians / 2;
|
||||||
|
const endRadians = closestWrap + monitorRadians;
|
||||||
|
|
||||||
|
// cache the end position so adjacent monitors can snap to it
|
||||||
|
const nextMonitorPixel = beginPixel + monitorLengthPixels;
|
||||||
|
if (cachedMonitorRadians[nextMonitorPixel] === undefined)
|
||||||
|
cachedMonitorRadians[nextMonitorPixel] = endRadians + spacingRadians;
|
||||||
|
|
||||||
|
return {
|
||||||
|
begin: closestWrap,
|
||||||
|
center: centerRadians,
|
||||||
|
end: endRadians
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort monitors left-to-right, top-to-bottom before placing them to avoid odd gaps
|
||||||
|
export function horizontalMonitorSort(monitors) {
|
||||||
|
return monitors.map((monitor, index) => ({originalIndex: index, monitorDetails: monitor})).sort((a, b) => {
|
||||||
|
const aMon = a.monitorDetails;
|
||||||
|
const bMon = b.monitorDetails;
|
||||||
|
if (aMon.y !== bMon.y) return aMon.y - bMon.y;
|
||||||
|
return aMon.x - bMon.x;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort monitors top-to-bottom, left-to-right before placing them to avoid odd gaps
|
||||||
|
export function verticalMonitorSort(monitors) {
|
||||||
|
return monitors.map((monitor, index) => ({originalIndex: index, monitorDetails: monitor})).sort((a, b) => {
|
||||||
|
const aMon = a.monitorDetails;
|
||||||
|
const bMon = b.monitorDetails;
|
||||||
|
if (aMon.x !== bMon.x) return aMon.x - bMon.x;
|
||||||
|
return aMon.y - bMon.y;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect whether a multi-monitor layout is wider or taller relative to the viewport,
|
||||||
|
* returning 'horizontal' or 'vertical'. Used when wrappingScheme is 'automatic'.
|
||||||
|
*
|
||||||
|
* @param {Object[]} monitors - [{x, y, width, height}]
|
||||||
|
* @param {number} viewportWidth
|
||||||
|
* @param {number} viewportHeight
|
||||||
|
* @returns {'horizontal'|'vertical'}
|
||||||
|
*/
|
||||||
|
export function autoDetectWrapScheme(monitors, viewportWidth, viewportHeight) {
|
||||||
|
const minX = Math.min(...monitors.map(m => m.x));
|
||||||
|
const maxX = Math.max(...monitors.map(m => m.x + m.width));
|
||||||
|
const minY = Math.min(...monitors.map(m => m.y));
|
||||||
|
const maxY = Math.max(...monitors.map(m => m.y + m.height));
|
||||||
|
return (maxX - minX) / viewportWidth >= (maxY - minY) / viewportHeight ? 'horizontal' : 'vertical';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns how far the look vector is from the center of a monitor, as a percentage of
|
||||||
|
* the monitor's dimensions (0 = center, 0.5 = exactly at edge, >0.5 = outside).
|
||||||
|
*
|
||||||
|
* All vector arguments are plain arrays in NWU order: [north, west, up].
|
||||||
|
*
|
||||||
|
* @param {Object} fovDetails
|
||||||
|
* @param {number} lookUpPixels
|
||||||
|
* @param {number} lookWestPixels
|
||||||
|
* @param {number[]} monitorVector - [north, west, up] center of the monitor relative to lens
|
||||||
|
* @param {{width: number, height: number}} monitorDetails
|
||||||
|
* @param {function} upAngleToLength
|
||||||
|
* @param {function} westAngleToLength
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function getMonitorDistance(fovDetails, lookUpPixels, lookWestPixels, monitorVector, monitorDetails, upAngleToLength, westAngleToLength) {
|
||||||
|
const monitorDistance = vectorMagnitude(monitorVector);
|
||||||
|
const distanceAdjustment = monitorDistance / fovDetails.completeScreenDistancePixels;
|
||||||
|
|
||||||
|
// monitorVector[0]=north, monitorVector[1]=west, monitorVector[2]=up
|
||||||
|
const vectorUpPixels = upAngleToLength(
|
||||||
|
fovDetails.defaultDistanceVerticalRadians,
|
||||||
|
fovDetails.heightPixels,
|
||||||
|
monitorDistance,
|
||||||
|
monitorVector[2],
|
||||||
|
monitorVector[0]
|
||||||
|
) * distanceAdjustment;
|
||||||
|
const upPercentage = Math.abs(lookUpPixels * distanceAdjustment - vectorUpPixels) / monitorDetails.height;
|
||||||
|
|
||||||
|
const vectorWestPixels = westAngleToLength(
|
||||||
|
fovDetails.defaultDistanceHorizontalRadians,
|
||||||
|
fovDetails.widthPixels,
|
||||||
|
monitorDistance,
|
||||||
|
monitorVector[1],
|
||||||
|
monitorVector[0]
|
||||||
|
) * distanceAdjustment;
|
||||||
|
const westPercentage = Math.abs(lookWestPixels * distanceAdjustment - vectorWestPixels) / monitorDetails.width;
|
||||||
|
|
||||||
|
return Math.max(upPercentage, westPercentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find which monitor the user is looking at.
|
||||||
|
*
|
||||||
|
* All vectors use plain NWU arrays: [north, west, up].
|
||||||
|
* Quaternion is [x, y, z, w].
|
||||||
|
*
|
||||||
|
* @param {number[]} quaternion - current head orientation [x, y, z, w]
|
||||||
|
* @param {number[]} position - lens position [north, west, up] in pixel units
|
||||||
|
* @param {number[][]} monitorVectors - centerLook for each monitor
|
||||||
|
* @param {number} currentFocusedIndex
|
||||||
|
* @param {number} focusedMonitorDistance - display_distance / display_distance_default, < 1 when zoomed in
|
||||||
|
* @param {boolean} smoothFollowEnabled
|
||||||
|
* @param {Object} fovDetails
|
||||||
|
* @param {Object[]} monitorsDetails - [{x, y, width, height}]
|
||||||
|
* @returns {number} index of focused monitor, or -1 if none
|
||||||
|
*/
|
||||||
|
export 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);
|
||||||
|
|
||||||
|
// TODO - right now we're using the curved functions to figure out distances even for flat monitors
|
||||||
|
// because it will account for the monitors facing towards us, but this will lose some accuracy
|
||||||
|
const upConversionFns = fovDetails.monitorWrappingScheme === 'vertical' ? fovConversionFns.curved : fovConversionFns.flat;
|
||||||
|
const lookUpPixels = upConversionFns.angleToLength(
|
||||||
|
fovDetails.defaultDistanceVerticalRadians,
|
||||||
|
fovDetails.heightPixels,
|
||||||
|
fovDetails.completeScreenDistancePixels,
|
||||||
|
rotatedLookVector[2],
|
||||||
|
rotatedLookVector[0]
|
||||||
|
);
|
||||||
|
const westConversionFns = fovDetails.monitorWrappingScheme === 'horizontal' ? fovConversionFns.curved : fovConversionFns.flat;
|
||||||
|
const lookWestPixels = westConversionFns.angleToLength(
|
||||||
|
fovDetails.defaultDistanceHorizontalRadians,
|
||||||
|
fovDetails.widthPixels,
|
||||||
|
fovDetails.completeScreenDistancePixels,
|
||||||
|
rotatedLookVector[1],
|
||||||
|
rotatedLookVector[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const focusedDistance = getMonitorDistance(
|
||||||
|
fovDetails,
|
||||||
|
lookUpPixels,
|
||||||
|
lookWestPixels,
|
||||||
|
vectorRelativeToLensPosition(monitorVectors[currentFocusedIndex]),
|
||||||
|
monitorsDetails[currentFocusedIndex],
|
||||||
|
upConversionFns.angleToLength,
|
||||||
|
westConversionFns.angleToLength
|
||||||
|
) * focusedMonitorDistance;
|
||||||
|
|
||||||
|
if (focusedDistance < UNFOCUS_THRESHOLD) return currentFocusedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
let closestIndex = -1;
|
||||||
|
let closestDistance = Infinity;
|
||||||
|
|
||||||
|
monitorVectors.forEach((monitorVector, index) => {
|
||||||
|
if (index === currentFocusedIndex) return;
|
||||||
|
|
||||||
|
const distance = getMonitorDistance(
|
||||||
|
fovDetails,
|
||||||
|
lookUpPixels,
|
||||||
|
lookWestPixels,
|
||||||
|
vectorRelativeToLensPosition(monitorVector),
|
||||||
|
monitorsDetails[index],
|
||||||
|
upConversionFns.angleToLength,
|
||||||
|
westConversionFns.angleToLength
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance < closestDistance) {
|
||||||
|
closestIndex = index;
|
||||||
|
closestDistance = distance;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (smoothFollowEnabled || closestDistance < FOCUS_THRESHOLD) return closestIndex;
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert monitor layout details into NWU placement vectors for rendering.
|
||||||
|
*
|
||||||
|
* Vectors in the returned objects are plain arrays [north, west, up].
|
||||||
|
* Qt callers should wrap centerNoRotate/centerLook with Qt.vector3d after calling.
|
||||||
|
*
|
||||||
|
* @param {Object} fovDetails - widthPixels, heightPixels, sizeAdjustedWidthPixels, sizeAdjustedHeightPixels,
|
||||||
|
* defaultDistanceHorizontalRadians, defaultDistanceVerticalRadians,
|
||||||
|
* completeScreenDistancePixels, monitorWrappingScheme, curvedDisplay
|
||||||
|
* @param {Object[]} monitorDetailsList - [{x, y, width, height}] in size-adjusted viewport-relative coords
|
||||||
|
* @param {number} monitorSpacing - visual spacing as a fraction of viewport width (e.g. 0.02)
|
||||||
|
* @returns {Object[]} - [{originalIndex, monitorCenterNorth, centerNoRotate, centerLook, rotationAngleRadians}]
|
||||||
|
*/
|
||||||
|
export function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) {
|
||||||
|
const monitorPlacements = [];
|
||||||
|
const cachedMonitorRadians = {};
|
||||||
|
|
||||||
|
const conversionFns = fovDetails.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat;
|
||||||
|
|
||||||
|
if (fovDetails.monitorWrappingScheme === 'horizontal') {
|
||||||
|
const sideEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.sizeAdjustedWidthPixels);
|
||||||
|
const monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedWidthPixels;
|
||||||
|
|
||||||
|
const lengthToRadianFn = (targetWidth) => conversionFns.lengthToRadians(
|
||||||
|
fovDetails.defaultDistanceHorizontalRadians,
|
||||||
|
fovDetails.widthPixels,
|
||||||
|
sideEdgeRadius,
|
||||||
|
targetWidth
|
||||||
|
);
|
||||||
|
|
||||||
|
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.sizeAdjustedHeightPixels) * monitorSpacingPixels;
|
||||||
|
const upCenterOffsetPixels = (monitorDetails.height - fovDetails.sizeAdjustedHeightPixels) / 2;
|
||||||
|
const upCenterPixels = upTopPixels - upCenterOffsetPixels;
|
||||||
|
|
||||||
|
monitorPlacements.push({
|
||||||
|
originalIndex,
|
||||||
|
monitorCenterNorth: monitorCenterRadius,
|
||||||
|
centerNoRotate: [monitorCenterRadius, 0, upCenterPixels],
|
||||||
|
centerLook: [
|
||||||
|
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
|
||||||
|
-monitorCenterRadius * Math.sin(monitorWrapDetails.center),
|
||||||
|
upCenterPixels
|
||||||
|
],
|
||||||
|
rotationAngleRadians: { x: 0, y: -monitorWrapDetails.center }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (fovDetails.monitorWrappingScheme === 'vertical') {
|
||||||
|
const topEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.sizeAdjustedHeightPixels);
|
||||||
|
const monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedHeightPixels;
|
||||||
|
|
||||||
|
const lengthToRadianFn = (targetHeight) => conversionFns.lengthToRadians(
|
||||||
|
fovDetails.defaultDistanceVerticalRadians,
|
||||||
|
fovDetails.heightPixels,
|
||||||
|
topEdgeRadius,
|
||||||
|
targetHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
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.sizeAdjustedWidthPixels) * monitorSpacingPixels;
|
||||||
|
const westCenterOffsetPixels = (monitorDetails.width - fovDetails.sizeAdjustedWidthPixels) / 2;
|
||||||
|
const westCenterPixels = westLeftPixels - westCenterOffsetPixels;
|
||||||
|
|
||||||
|
monitorPlacements.push({
|
||||||
|
originalIndex,
|
||||||
|
monitorCenterNorth: monitorCenterRadius,
|
||||||
|
centerNoRotate: [monitorCenterRadius, westCenterPixels, 0],
|
||||||
|
centerLook: [
|
||||||
|
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
|
||||||
|
westCenterPixels,
|
||||||
|
-monitorCenterRadius * Math.sin(monitorWrapDetails.center)
|
||||||
|
],
|
||||||
|
rotationAngleRadians: { x: -monitorWrapDetails.center, y: 0 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedWidthPixels;
|
||||||
|
|
||||||
|
monitorDetailsList.forEach((monitorDetails, index) => {
|
||||||
|
const upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.sizeAdjustedHeightPixels) * monitorSpacingPixels;
|
||||||
|
const westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.sizeAdjustedWidthPixels) * monitorSpacingPixels;
|
||||||
|
const westCenterOffsetPixels = (monitorDetails.width - fovDetails.sizeAdjustedWidthPixels) / 2;
|
||||||
|
const upCenterOffsetPixels = (monitorDetails.height - fovDetails.sizeAdjustedHeightPixels) / 2;
|
||||||
|
const westCenterPixels = westLeftPixels - westCenterOffsetPixels;
|
||||||
|
const upCenterPixels = upTopPixels - upCenterOffsetPixels;
|
||||||
|
|
||||||
|
monitorPlacements.push({
|
||||||
|
originalIndex: index,
|
||||||
|
monitorCenterNorth: fovDetails.completeScreenDistancePixels,
|
||||||
|
centerNoRotate: [fovDetails.completeScreenDistancePixels, westCenterPixels, upCenterPixels],
|
||||||
|
centerLook: [fovDetails.completeScreenDistancePixels, westCenterPixels, upCenterPixels],
|
||||||
|
rotationAngleRadians: { x: 0, y: 0 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
monitorPlacements.sort((a, b) => a.originalIndex - b.originalIndex);
|
||||||
|
|
||||||
|
return monitorPlacements;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
export function degreeToRadian(degree) {
|
||||||
|
return degree * Math.PI / 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 unit distance of 1.0
|
||||||
|
const diagonalLengthUnitDistance = 2 * Math.tan(diagonalFOVRadians / 2);
|
||||||
|
|
||||||
|
// then convert to flat plane horizontal and vertical FOVs
|
||||||
|
const heightUnitDistance = diagonalLengthUnitDistance / Math.sqrt(1 + aspectRatio * aspectRatio);
|
||||||
|
const widthUnitDistance = heightUnitDistance * aspectRatio;
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentsPerRadian = 20.0 / degreeToRadian(90.0);
|
||||||
|
|
||||||
|
// displays are placed around a circle, these functions help determine radians and distances from the original
|
||||||
|
// FOV measurements scaled to the display dimensions
|
||||||
|
export const fovConversionFns = {
|
||||||
|
// convert curved FOV for flat displays
|
||||||
|
flat: {
|
||||||
|
// distance to an edge is the hypothenuse of the triangle where the opposite side is half the width of the reference fov screen
|
||||||
|
centerToFovEdgeDistance: (centerDistance, fovLength) => Math.sqrt(Math.pow(fovLength / 2, 2) + Math.pow(centerDistance, 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,
|
||||||
|
angleToLength: (fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) => {
|
||||||
|
return toAngleOpposite / toAngleAdjacent * screenDistance;
|
||||||
|
},
|
||||||
|
fovRadiansAtDistance: (fovRadians, unitLength, newScreenDistance) => {
|
||||||
|
return 2 * Math.atan(unitLength / 2 / newScreenDistance);
|
||||||
|
},
|
||||||
|
radiansToSegments: (screenRadians) => 1
|
||||||
|
},
|
||||||
|
|
||||||
|
// convert curved FOV for curved displays, scaling either involves no change or is linear
|
||||||
|
curved: {
|
||||||
|
centerToFovEdgeDistance: (centerDistance, fovLength) => centerDistance,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// quaternion is [x, y, z, w], vector is [x, y, z]
|
||||||
|
export const applyQuaternionToVector = (vector, quaternion) => {
|
||||||
|
const t = [
|
||||||
|
2.0 * (quaternion[1] * vector[2] - quaternion[2] * vector[1]),
|
||||||
|
2.0 * (quaternion[2] * vector[0] - quaternion[0] * vector[2]),
|
||||||
|
2.0 * (quaternion[0] * vector[1] - quaternion[1] * vector[0])
|
||||||
|
];
|
||||||
|
return [
|
||||||
|
vector[0] + quaternion[3] * t[0] + quaternion[1] * t[2] - quaternion[2] * t[1],
|
||||||
|
vector[1] + quaternion[3] * t[1] + quaternion[2] * t[0] - quaternion[0] * t[2],
|
||||||
|
vector[2] + quaternion[3] * t[2] + quaternion[0] * t[1] - quaternion[1] * t[0]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = vectorMagnitude(vector);
|
||||||
|
return [vector[0] / length, vector[1] / length, vector[2] / length];
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue