Port over display placement and related math functions from the GNOME extension
This commit is contained in:
parent
4093123332
commit
5bfba85557
|
|
@ -14,8 +14,8 @@ export function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) {
|
|||
// then convert back to spherical FOV
|
||||
return {
|
||||
diagonal: diagonalFOVRadians,
|
||||
horizontal: 2 * Math.atan(Math.tan(flatHorizontalFOV / 2)),
|
||||
vertical: 2 * Math.atan(Math.tan(flatVerticalFOV / 2))
|
||||
horizontal: 2 * Math.atan(flatHorizontalFOV / 2),
|
||||
vertical: 2 * Math.atan(flatVerticalFOV / 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
}
|
||||
],
|
||||
"Category": "Tools",
|
||||
"Description": "Breezy Desktop XR Effect 2",
|
||||
"Description": "Breezy Desktop XR Effect",
|
||||
"EnabledByDefault": true,
|
||||
"License": "GPL",
|
||||
"Name": "Breezy Desktop XR",
|
||||
|
|
|
|||
|
|
@ -16,15 +16,8 @@ Node {
|
|||
"Rokid Air"
|
||||
]
|
||||
|
||||
property real viewportDiagonalFOV: effect.diagonalFOV
|
||||
property real viewportDiagonalFOVDegrees: effect.diagonalFOV
|
||||
property var viewportResolution: effect.displayResolution
|
||||
property real viewportHorizontalFOV: {
|
||||
const aspectRatio = viewportResolution[0] / viewportResolution[1];
|
||||
return viewportDiagonalFOV * aspectRatio / Math.sqrt(aspectRatio * aspectRatio + 1);
|
||||
}
|
||||
property real viewportCenterRadius: {
|
||||
return viewportResolution[0] / (2 * Math.tan(Math.PI * viewportHorizontalFOV / 360))
|
||||
}
|
||||
property var screens: KWinComponents.Workspace.screens
|
||||
// .filter(function(screen) {
|
||||
// return supportedModels.includes(screen.model);
|
||||
|
|
@ -58,16 +51,24 @@ Node {
|
|||
return (yMin + yMax) / 2 - (viewportResolution[1] / 2);
|
||||
}
|
||||
|
||||
Displays {
|
||||
id: displays
|
||||
}
|
||||
|
||||
property var monitorPlacements: {
|
||||
const fovDetails = displays.fovDetails(viewportResolution[0], viewportResolution[1], viewportDiagonalFOVDegrees, effect.lensDistanceRatio);
|
||||
const monitorSpacing = 0.0;
|
||||
return displays.monitorsToPlacements(fovDetails, screens.map(screen => screen.geometry), monitorSpacing);
|
||||
}
|
||||
|
||||
Repeater3D {
|
||||
model: screens.length
|
||||
delegate: BreezyDesktopDisplay {
|
||||
screen: screens[index]
|
||||
monitorPlacement: monitorPlacements[index]
|
||||
|
||||
property real screenRotation: {
|
||||
const geometry = screen.geometry;
|
||||
const rot = (viewportHorizontalFOV / viewportResolution[0]) * (geometry.x - screensXMid);
|
||||
return -rot;
|
||||
}
|
||||
property real screenRotationY: displays.radianToDegree(monitorPlacement.rotationAngleRadians.y)
|
||||
property real screenRotationX: displays.radianToDegree(monitorPlacement.rotationAngleRadians.x)
|
||||
|
||||
property vector3d screenScale: {
|
||||
const geometry = screen.geometry;
|
||||
|
|
@ -77,14 +78,16 @@ Node {
|
|||
}
|
||||
|
||||
scale: screenScale
|
||||
eulerRotation.y: screenRotation
|
||||
position: {
|
||||
eulerRotation.y: screenRotationY
|
||||
eulerRotation.x: screenRotationX
|
||||
position: {
|
||||
// rotate about the Y (up) axis, to create a horizontal movement
|
||||
const transform = Qt.matrix4x4();
|
||||
transform.rotate(screenRotation, Qt.vector3d(0, 1, 0));
|
||||
transform.rotate(screenRotationY, Qt.vector3d(0, 1, 0));
|
||||
transform.rotate(screenRotationX, Qt.vector3d(1, 0, 0));
|
||||
|
||||
// camera looks along the negative Z axis
|
||||
return transform.times(Qt.vector3d(0, 0, -viewportCenterRadius));
|
||||
return transform.times(Qt.vector3d(-monitorPlacement.centerNoRotate[1], monitorPlacement.centerNoRotate[2], -monitorPlacement.centerNoRotate[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Model {
|
|||
id: display
|
||||
|
||||
required property QtObject screen
|
||||
required property var monitorPlacement
|
||||
required property int index
|
||||
|
||||
source: "#Rectangle"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,304 @@
|
|||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
// Converts degrees to radians
|
||||
function degreeToRadian(degree) {
|
||||
return degree * Math.PI / 180;
|
||||
}
|
||||
|
||||
function radianToDegree(radian) {
|
||||
return radian * 180 / Math.PI;
|
||||
}
|
||||
|
||||
// Converts diagonal FOV in radians and aspect ratio to horizontal and vertical FOVs
|
||||
function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) {
|
||||
var flatDiagonalFOV = 2 * Math.tan(diagonalFOVRadians / 2);
|
||||
var flatVerticalFOV = flatDiagonalFOV / Math.sqrt(1 + aspectRatio * aspectRatio);
|
||||
var flatHorizontalFOV = flatVerticalFOV * aspectRatio;
|
||||
return {
|
||||
diagonal: diagonalFOVRadians,
|
||||
horizontal: 2 * Math.atan(flatHorizontalFOV / 2),
|
||||
vertical: 2 * Math.atan(flatVerticalFOV / 2)
|
||||
}
|
||||
}
|
||||
|
||||
function displayDistanceDefault() {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
function fovDetails(viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio) {
|
||||
const aspect = viewportWidth / viewportHeight;
|
||||
const fovRadians = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect);
|
||||
const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / displayDistanceDefault());
|
||||
const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / displayDistanceDefault());
|
||||
|
||||
// distance needed for the FOV-sized monitor to fill up the screen
|
||||
const fullScreenDistance = viewportHeight / 2 / Math.tan(fovRadians.vertical / 2);
|
||||
const lensDistancePixels = fullScreenDistance / (1.0 - lensDistanceRatio) - fullScreenDistance;
|
||||
|
||||
// distance of a display at the default (most zoomed out) distance, plus the lens distance constant
|
||||
const lensToScreenDistance = viewportHeight / 2 / Math.tan(defaultDistanceVerticalRadians / 2);
|
||||
const completeScreenDistancePixels = lensToScreenDistance + lensDistancePixels;
|
||||
|
||||
return {
|
||||
widthPixels: viewportWidth,
|
||||
heightPixels: viewportHeight,
|
||||
defaultDistanceVerticalRadians,
|
||||
defaultDistanceHorizontalRadians,
|
||||
lensDistancePixels,
|
||||
completeScreenDistancePixels,
|
||||
monitorWrappingScheme: 'horizontal', // or 'vertical' or 'none'
|
||||
curvedDisplay: false // or true
|
||||
};
|
||||
}
|
||||
|
||||
// Utility constant
|
||||
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;
|
||||
},
|
||||
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);
|
||||
},
|
||||
radiansToSegments: function(screenRadians) {
|
||||
return Math.ceil(screenRadians * segmentsPerRadian);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Applies a quaternion to a 3D vector
|
||||
function applyQuaternionToVector(vector, quaternion) {
|
||||
var 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]
|
||||
];
|
||||
}
|
||||
|
||||
// Normalizes a 3D vector
|
||||
function normalizeVector(vector) {
|
||||
var length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
|
||||
return [vector[0] / length, vector[1] / length, vector[2] / length];
|
||||
}
|
||||
|
||||
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) {
|
||||
var monitorPlacements = [];
|
||||
var cachedMonitorRadians = {};
|
||||
|
||||
var conversionFns = fovDetails.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat;
|
||||
|
||||
if (fovDetails.monitorWrappingScheme === 'horizontal') {
|
||||
var sideEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.widthPixels);
|
||||
var monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels;
|
||||
var lengthToRadianFn = function(targetWidth) {
|
||||
return conversionFns.lengthToRadians(
|
||||
fovDetails.defaultDistanceHorizontalRadians,
|
||||
fovDetails.widthPixels,
|
||||
sideEdgeRadius,
|
||||
targetWidth
|
||||
);
|
||||
};
|
||||
|
||||
cachedMonitorRadians[0] = -fovDetails.defaultDistanceHorizontalRadians / 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.heightPixels) * monitorSpacingPixels;
|
||||
var upCenterOffsetPixels = (monitorDetails.height - fovDetails.heightPixels) / 2;
|
||||
var upCenterPixels = upTopPixels - upCenterOffsetPixels;
|
||||
|
||||
monitorPlacements.push({
|
||||
originalIndex: originalIndex,
|
||||
centerNoRotate: [
|
||||
monitorCenterRadius,
|
||||
0,
|
||||
upCenterPixels
|
||||
],
|
||||
centerLook: normalizeVector([
|
||||
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.heightPixels);
|
||||
var monitorSpacingPixels = monitorSpacing * fovDetails.heightPixels;
|
||||
var lengthToRadianFn = function(targetHeight) {
|
||||
return conversionFns.lengthToRadians(
|
||||
fovDetails.defaultDistanceVerticalRadians,
|
||||
fovDetails.heightPixels,
|
||||
topEdgeRadius,
|
||||
targetHeight
|
||||
);
|
||||
};
|
||||
|
||||
cachedMonitorRadians[0] = -fovDetails.defaultDistanceVerticalRadians / 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.widthPixels) * monitorSpacingPixels;
|
||||
var westCenterOffsetPixels = (monitorDetails.width - fovDetails.widthPixels) / 2;
|
||||
var westCenterPixels = westLeftPixels - westCenterOffsetPixels;
|
||||
|
||||
monitorPlacements.push({
|
||||
originalIndex: originalIndex,
|
||||
centerNoRotate: [
|
||||
monitorCenterRadius,
|
||||
westCenterPixels,
|
||||
0
|
||||
],
|
||||
centerLook: normalizeVector([
|
||||
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
|
||||
westCenterPixels,
|
||||
-monitorCenterRadius * Math.sin(monitorWrapDetails.center)
|
||||
]),
|
||||
rotationAngleRadians: {
|
||||
x: -monitorWrapDetails.center,
|
||||
y: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
var monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels;
|
||||
monitorDetailsList.forEach(function(monitorDetails, index) {
|
||||
var upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels;
|
||||
var westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels;
|
||||
var westCenterOffsetPixels = (monitorDetails.width - fovDetails.widthPixels) / 2;
|
||||
var upCenterOffsetPixels = (monitorDetails.height - fovDetails.heightPixels) / 2;
|
||||
var westCenterPixels = westLeftPixels - westCenterOffsetPixels;
|
||||
var upCenterPixels = upTopPixels - upCenterOffsetPixels;
|
||||
|
||||
monitorPlacements.push({
|
||||
originalIndex: index,
|
||||
centerNoRotate: [
|
||||
fovDetails.completeScreenDistancePixels,
|
||||
westCenterPixels,
|
||||
upCenterPixels
|
||||
],
|
||||
centerLook: normalizeVector([
|
||||
fovDetails.completeScreenDistancePixels,
|
||||
westCenterPixels,
|
||||
upCenterPixels
|
||||
]),
|
||||
rotationAngleRadians: {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
monitorPlacements.sort(function(a, b) { return a.originalIndex - b.originalIndex; });
|
||||
return monitorPlacements;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue