diff --git a/kwin/src/breezydesktopconfig.kcfg b/kwin/src/breezydesktopconfig.kcfg index b2129c8..e6772ef 100644 --- a/kwin/src/breezydesktopconfig.kcfg +++ b/kwin/src/breezydesktopconfig.kcfg @@ -5,11 +5,17 @@ http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" > - + + 0.85 + 0.2 + 2.5 + + + 1.05 0.2 2.5 - + diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 6789161..81b056c 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -123,7 +123,8 @@ BreezyDesktopEffect::BreezyDesktopEffect() void BreezyDesktopEffect::reconfigure(ReconfigureFlags) { BreezyDesktopConfig::self()->read(); - setDisplayDistance(BreezyDesktopConfig::displayDistance()); + setFocusedDisplayDistance(BreezyDesktopConfig::focusedDisplayDistance()); + setAllDisplaysDistance(BreezyDesktopConfig::allDisplaysDistance()); } QVariantMap BreezyDesktopEffect::initialProperties(Output *screen) @@ -205,13 +206,24 @@ QList BreezyDesktopEffect::displayResolution() const { return m_displayResolution; } -qreal BreezyDesktopEffect::displayDistance() const { - return m_displayDistance; +qreal BreezyDesktopEffect::focusedDisplayDistance() const { + return m_focusedDisplayDistance; } -void BreezyDesktopEffect::setDisplayDistance(qreal distance) { - if (distance != m_displayDistance) { - m_displayDistance = std::clamp(distance, 0.2, 2.5); +void BreezyDesktopEffect::setFocusedDisplayDistance(qreal distance) { + if (distance != m_focusedDisplayDistance) { + m_focusedDisplayDistance = std::clamp(distance, 0.2, 2.5); + Q_EMIT displayDistanceChanged(); + } +} + +qreal BreezyDesktopEffect::allDisplaysDistance() const { + return m_allDisplaysDistance; +} + +void BreezyDesktopEffect::setAllDisplaysDistance(qreal distance) { + if (distance != m_allDisplaysDistance) { + m_allDisplaysDistance = std::clamp(distance, 0.2, 2.5); Q_EMIT displayDistanceChanged(); } } diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index d77df4d..fd208cc 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -22,7 +22,8 @@ namespace KWin Q_PROPERTY(QPointF cursorPos READ cursorPos NOTIFY cursorPosChanged) Q_PROPERTY(QList lookAheadConfig READ lookAheadConfig NOTIFY devicePropertiesChanged) Q_PROPERTY(QList displayResolution READ displayResolution NOTIFY devicePropertiesChanged) - Q_PROPERTY(qreal displayDistance READ displayDistance NOTIFY displayDistanceChanged) + Q_PROPERTY(qreal focusedDisplayDistance READ focusedDisplayDistance NOTIFY displayDistanceChanged) + Q_PROPERTY(qreal allDisplaysDistance READ allDisplaysDistance NOTIFY displayDistanceChanged) Q_PROPERTY(qreal diagonalFOV READ diagonalFOV NOTIFY devicePropertiesChanged) Q_PROPERTY(qreal lensDistanceRatio READ lensDistanceRatio NOTIFY devicePropertiesChanged) Q_PROPERTY(bool sbsEnabled READ sbsEnabled NOTIFY devicePropertiesChanged) @@ -46,8 +47,10 @@ namespace KWin bool imuResetState() const; QList lookAheadConfig() const; QList displayResolution() const; - qreal displayDistance() const; - void setDisplayDistance(qreal distance); + qreal focusedDisplayDistance() const; + void setFocusedDisplayDistance(qreal distance); + qreal allDisplaysDistance() const; + void setAllDisplaysDistance(qreal distance); qreal diagonalFOV() const; qreal lensDistanceRatio() const; bool sbsEnabled() const; @@ -101,7 +104,8 @@ namespace KWin QFileSystemWatcher *m_shmDirectoryWatcher = nullptr; QPointF m_cursorPos; QTimer *m_cursorUpdateTimer = nullptr; - qreal m_displayDistance = 1.05; + qreal m_focusedDisplayDistance = 0.85; + qreal m_allDisplaysDistance = 1.05; }; } // namespace KWin diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui index f134c1e..ac90744 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.ui +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -23,14 +23,34 @@ - + - Display Distance: + Focused Display Distance: - + + + 0.2 + + + 2.5 + + + 0.01 + + + + + + + All Displays Distance: + + + + + 0.2 diff --git a/kwin/src/qml/BreezyDesktop.qml b/kwin/src/qml/BreezyDesktop.qml index 602f7fe..103af5a 100644 --- a/kwin/src/qml/BreezyDesktop.qml +++ b/kwin/src/qml/BreezyDesktop.qml @@ -3,9 +3,14 @@ import QtQuick3D Node { + id: breezyDesktop + property var viewportResolution: effect.displayResolution property var screens: root.screens + property var fovDetails: root.fovDetails property var monitorPlacements: root.monitorPlacements + property var imuRotations: effect.imuRotations + property int focusedMonitorIndex: -1 // x value for placing the viewport in the middle of all screens property real screensXMid: { @@ -39,7 +44,6 @@ Node { id: displays } - Repeater3D { model: screens.length delegate: BreezyDesktopDisplay { @@ -60,13 +64,40 @@ Node { eulerRotation.y: screenRotationY eulerRotation.x: screenRotationX position: { - // rotate about the Y (up) axis, to create a horizontal movement - const transform = Qt.matrix4x4(); - 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(-monitorPlacement.centerNoRotate[1], monitorPlacement.centerNoRotate[2], -monitorPlacement.centerNoRotate[0])); + let positionVector = displays.nwuToEusVector(monitorPlacement.centerNoRotate); + if (focusedMonitorIndex === index) { + positionVector = positionVector.times(effect.focusedDisplayDistance / effect.allDisplaysDistance); + } + + // position vector is only translated in flat directions, without rotations applied, so apply them here + const rotationMatrix = Qt.matrix4x4(); + + // only one of these should ever be non-zero, since we only rotate in the direction of the "wrap" preference + rotationMatrix.rotate(screenRotationY, Qt.vector3d(0, 1, 0)); + rotationMatrix.rotate(screenRotationX, Qt.vector3d(1, 0, 0)); + + return rotationMatrix.times(positionVector); + } + } + } + + FrameAnimation { + running: true + onTriggered: { + if (breezyDesktop.imuRotations && breezyDesktop.imuRotations.length > 0) { + const focusedIndex = displays.findFocusedMonitor( + displays.eusToNwuQuat(breezyDesktop.imuRotations[0]), + breezyDesktop.monitorPlacements.map(monitorVectors => monitorVectors.centerLook), + breezyDesktop.focusedMonitorIndex, + false, // TODO smooth follow + breezyDesktop.fovDetails, + breezyDesktop.screens.map(screen => screen.geometry) + ); + + console.log(`\t\t\tBreezy - Next focused monitor index: ${focusedIndex}`); + if (focusedIndex !== breezyDesktop.focusedMonitorIndex) + breezyDesktop.focusedMonitorIndex = focusedIndex; } } } diff --git a/kwin/src/qml/CameraController.qml b/kwin/src/qml/CameraController.qml index 84804ff..15b5276 100644 --- a/kwin/src/qml/CameraController.qml +++ b/kwin/src/qml/CameraController.qml @@ -15,7 +15,6 @@ Item { property real lensDistanceRatio: effect.lensDistanceRatio property bool sbsEnabled: effect.sbsEnabled property bool customBannerEnabled: effect.customBannerEnabled - property bool useImuRotation: true // Set to true to use XR rotation when available implicitWidth: parent.width implicitHeight: parent.height @@ -72,7 +71,7 @@ Item { FrameAnimation { running: true onTriggered: { - if (root.useImuRotation && root.imuRotations && root.imuRotations.length > 0) { + if (root.imuRotations && root.imuRotations.length > 0) { updateCamera(applyLookAhead( root.imuRotations[0], root.imuRotations[1], diff --git a/kwin/src/qml/Displays.qml b/kwin/src/qml/Displays.qml index 95ddde8..a126c62 100644 --- a/kwin/src/qml/Displays.qml +++ b/kwin/src/qml/Displays.qml @@ -1,7 +1,8 @@ import QtQuick QtObject { - property real displayDistance: effect.displayDistance + readonly property real focusThreshold: 0.95 / 2.0 + readonly property real unfocusThreshold: 1.1 / 2.0 // Converts degrees to radians function degreeToRadian(degree) { @@ -12,6 +13,16 @@ QtObject { return radian * 180 / Math.PI; } + function nwuToEusVector(vector) { + // Converts NWU vector to EUS vector + return Qt.vector3d(-vector.y, vector.z, -vector.x); + } + + function eusToNwuQuat(quaternion) { + // Converts EUS quaternion to NWU quaternion + return Qt.quaternion(quaternion.scalar, -quaternion.z, -quaternion.x, quaternion.y); + } + // Converts diagonal FOV in radians and aspect ratio to horizontal and vertical FOVs function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) { var flatDiagonalFOV = 2 * Math.tan(diagonalFOVRadians / 2); @@ -24,10 +35,6 @@ QtObject { } } - function displayDistanceDefault() { - return displayDistance; - } - function actualWrapScheme(screens, viewportWidth, viewportHeight) { const minX = Math.min(...screens.map(screen => screen.geometry.x)); const maxX = Math.max(...screens.map(screen => screen.geometry.x + screen.geometry.width)); @@ -44,8 +51,8 @@ QtObject { function fovDetails(screens, 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()); + const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / effect.allDisplaysDistance); + const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / effect.allDisplaysDistance); // distance needed for the FOV-sized monitor to fill up the screen const fullScreenDistance = viewportHeight / 2 / Math.tan(fovRadians.vertical / 2); @@ -106,26 +113,6 @@ QtObject { } }) - // 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]; @@ -227,16 +214,16 @@ QtObject { monitorPlacements.push({ originalIndex: originalIndex, - centerNoRotate: [ + centerNoRotate: Qt.vector3d( monitorCenterRadius, 0, upCenterPixels - ], - centerLook: normalizeVector([ + ), + centerLook: Qt.vector3d( monitorCenterRadius * Math.cos(monitorWrapDetails.center), -monitorCenterRadius * Math.sin(monitorWrapDetails.center), upCenterPixels - ]), + ).normalized(), rotationAngleRadians: { x: 0, y: -monitorWrapDetails.center @@ -267,16 +254,16 @@ QtObject { monitorPlacements.push({ originalIndex: originalIndex, - centerNoRotate: [ + centerNoRotate: Qt.vector3d( monitorCenterRadius, westCenterPixels, 0 - ], - centerLook: normalizeVector([ + ), + centerLook: Qt.vector3d( monitorCenterRadius * Math.cos(monitorWrapDetails.center), westCenterPixels, -monitorCenterRadius * Math.sin(monitorWrapDetails.center) - ]), + ).normalized(), rotationAngleRadians: { x: -monitorWrapDetails.center, y: 0 @@ -295,16 +282,16 @@ QtObject { monitorPlacements.push({ originalIndex: index, - centerNoRotate: [ + centerNoRotate: Qt.vector3d( fovDetails.completeScreenDistancePixels, westCenterPixels, upCenterPixels - ], - centerLook: normalizeVector([ + ), + centerLook: Qt.vector3d( fovDetails.completeScreenDistancePixels, westCenterPixels, upCenterPixels - ]), + ).normalized(), rotationAngleRadians: { x: 0, y: 0 @@ -313,7 +300,108 @@ QtObject { }); } + // 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 width + function getMonitorDistance(fovDetails, lookUpPixels, lookWestPixels, monitorVector, monitorDetails, upAngleToLength, westAngleToLength) { + var monitorAspectRatio = monitorDetails.width / monitorDetails.height; + + // weight the up distance by the aspect ratio + var vectorUpPixels = upAngleToLength( + fovDetails.defaultDistanceVerticalRadians, + fovDetails.heightPixels, + fovDetails.completeScreenDistancePixels, + monitorVector.z, + monitorVector.x + ); + var upDeltaPixels = (lookUpPixels - vectorUpPixels) * monitorAspectRatio; + + var vectorWestPixels = westAngleToLength( + fovDetails.defaultDistanceHorizontalRadians, + fovDetails.widthPixels, + fovDetails.completeScreenDistancePixels, + monitorVector.y, + monitorVector.x + ); + var westDeltaPixels = lookWestPixels - vectorWestPixels; + var totalDeltaPixels = Math.sqrt(upDeltaPixels * upDeltaPixels + westDeltaPixels * westDeltaPixels); + + // threshold is a percentage of width, and height was already properly weighted + return totalDeltaPixels / monitorDetails.width; + } + + function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, smoothFollowEnabled, fovDetails, monitorsDetails) { + var lookVector = Qt.vector3d(1.0, 0.0, 0.0); // NWU vector pointing to the center of the screen + var rotatedLookVector = quaternion.times(lookVector); + + // Use curved or flat conversion functions depending on wrapping scheme + var upConversionFns = fovDetails.monitorWrappingScheme === "vertical" ? fovConversionFns.curved : fovConversionFns.flat; + var lookUpPixels = upConversionFns.angleToLength( + fovDetails.defaultDistanceVerticalRadians, + fovDetails.heightPixels, + fovDetails.completeScreenDistancePixels, + rotatedLookVector.z, + rotatedLookVector.x + ); + var westConversionFns = fovDetails.monitorWrappingScheme === "horizontal" ? fovConversionFns.curved : fovConversionFns.flat; + var lookWestPixels = westConversionFns.angleToLength( + fovDetails.defaultDistanceHorizontalRadians, + fovDetails.widthPixels, + fovDetails.completeScreenDistancePixels, + rotatedLookVector.y, + rotatedLookVector.x + ); + + var closestIndex = -1; + var closestDistance = Number.POSITIVE_INFINITY; + + // Check current focused monitor first + if (currentFocusedIndex !== -1) { + var focusedDistance = getMonitorDistance( + fovDetails, + lookUpPixels, + lookWestPixels, + monitorVectors[currentFocusedIndex], + monitorsDetails[currentFocusedIndex], + upConversionFns.angleToLength, + westConversionFns.angleToLength + ) * effect.focusedDisplayDistance / effect.allDisplaysDistance; + + console.log(`\t\t\tBreezy - Focused monitor index: ${currentFocusedIndex}, distance: ${focusedDistance}`); + + if (smoothFollowEnabled || focusedDistance < unfocusThreshold) + return currentFocusedIndex; + } + + // Find the closest monitor + for (var i = 0; i < monitorVectors.length; ++i) { + if (i === currentFocusedIndex) + continue; + var distance = getMonitorDistance( + fovDetails, + lookUpPixels, + lookWestPixels, + monitorVectors[i], + monitorsDetails[i], + upConversionFns.angleToLength, + westConversionFns.angleToLength + ); + console.log(`\t\t\tBreezy - Monitor index: ${i}, distance: ${distance}`); + + if (distance < closestDistance) { + closestIndex = i; + closestDistance = distance; + } + } + + if (smoothFollowEnabled || closestDistance < focusThreshold) + return closestIndex; + + // Unfocus all displays + return -1; + } } \ No newline at end of file diff --git a/kwin/src/qml/main.qml b/kwin/src/qml/main.qml index 468af61..a40c553 100644 --- a/kwin/src/qml/main.qml +++ b/kwin/src/qml/main.qml @@ -33,8 +33,9 @@ Item { id: displays } + property var fovDetails: displays.fovDetails(screens, viewportResolution[0], viewportResolution[1], viewportDiagonalFOVDegrees, effect.lensDistanceRatio) + property var monitorPlacements: { - const fovDetails = displays.fovDetails(screens, viewportResolution[0], viewportResolution[1], viewportDiagonalFOVDegrees, effect.lensDistanceRatio); const monitorSpacing = 0.0; return displays.monitorsToPlacements(fovDetails, screens.map(screen => screen.geometry), monitorSpacing); } @@ -58,7 +59,6 @@ Item { PerspectiveCamera { id: camera - frustumCullingEnabled: false } BreezyDesktop {