Add 6DoF support for KWin (#146), v2.6.3

This commit is contained in:
Wayne Heaney 2026-01-16 21:08:56 -08:00 committed by GitHub
parent d1e36d02e1
commit e96ccc4f0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 540 additions and 152 deletions

View File

@ -1 +1 @@
2.6.2
2.6.3

View File

@ -6,17 +6,22 @@
<kcfgfile name="kwinrc"/>
<group name="Effect-breezy_desktop">
<entry name="FocusedDisplayDistance" type="Int">
<default>85</default>
<min>20</min>
<max>230</max>
<default>22</default>
<min>10</min>
<max>250</max>
<label>Focused Display Distance</label>
</entry>
<entry name="AllDisplaysDistance" type="Int">
<default>105</default>
<min>20</min>
<max>230</max>
<default>25</default>
<min>10</min>
<max>250</max>
<label>All Displays Distance</label>
</entry>
<entry name="measurement_units" type="String">
<default>cm</default>
<label>Measurement units</label>
<description>Units for displaying distances in the KCM UI. Valid values: "cm" or "in".</description>
</entry>
<entry name="ZoomOnFocusEnabled" type="Bool">
<default>false</default>
<label>Zoom on Focus Enabled</label>
@ -29,6 +34,13 @@
<label>Display Spacing</label>
<description>How far apart the displays are visually (not logically)</description>
</entry>
<entry name="DisplaySize" type="Int">
<default>97</default>
<min>10</min>
<max>250</max>
<label>Display Size</label>
<description>Scale the displays in XR (percentage)</description>
</entry>
<entry name="DisplayHorizontalOffset" type="Int">
<default>0</default>
<min>-250</min>
@ -91,5 +103,9 @@
<label>Curved display</label>
<description>Curve the displays around you</description>
</entry>
<entry name="DeveloperMode" type="Bool">
<default>false</default>
</entry>
</group>
</kcfg>

View File

@ -157,9 +157,9 @@ BreezyDesktopEffect::BreezyDesktopEffect()
!m_shmFileWatcher->files().contains(DataView::SHM_PATH)
)) {
m_shmFileWatcher->removePath(DataView::SHM_PATH);
disconnect(m_shmFileWatcher, &QFileSystemWatcher::fileChanged, this, &BreezyDesktopEffect::updatePoseOrientation);
disconnect(m_shmFileWatcher, &QFileSystemWatcher::fileChanged, this, &BreezyDesktopEffect::updatePose);
m_shmFileWatcher->addPath(DataView::SHM_PATH);
connect(m_shmFileWatcher, &QFileSystemWatcher::fileChanged, this, &BreezyDesktopEffect::updatePoseOrientation);
connect(m_shmFileWatcher, &QFileSystemWatcher::fileChanged, this, &BreezyDesktopEffect::updatePose);
}
};
@ -173,7 +173,7 @@ BreezyDesktopEffect::BreezyDesktopEffect()
m_watchdogTimer->setInterval(1000);
connect(m_watchdogTimer, &QTimer::timeout, this, [this]() {
if (!m_enabled) return;
this->updatePoseOrientation();
this->updatePose();
});
m_watchdogTimer->start();
@ -243,6 +243,7 @@ void BreezyDesktopEffect::reconfigure(ReconfigureFlags)
setFocusedDisplayDistance(BreezyDesktopConfig::focusedDisplayDistance() / 100.0f);
setAllDisplaysDistance(BreezyDesktopConfig::allDisplaysDistance() / 100.0f);
setDisplaySpacing(BreezyDesktopConfig::displaySpacing() / 1000.0f);
setDisplaySize(BreezyDesktopConfig::displaySize() / 100.0f);
setZoomOnFocusEnabled(BreezyDesktopConfig::zoomOnFocusEnabled());
setSmoothFollowThreshold(BreezyDesktopConfig::smoothFollowThreshold());
@ -262,6 +263,9 @@ void BreezyDesktopEffect::reconfigure(ReconfigureFlags)
if (m_removeVirtualDisplaysOnDisable != removeVD) { m_removeVirtualDisplaysOnDisable = removeVD; Q_EMIT removeVirtualDisplaysOnDisableChanged(); }
if (m_mirrorPhysicalDisplays != mirrorPhysicalDisplays) { m_mirrorPhysicalDisplays = mirrorPhysicalDisplays; Q_EMIT mirrorPhysicalDisplaysChanged(); }
const bool developerMode = BreezyDesktopConfig::developerMode();
if (m_developerMode != developerMode) { m_developerMode = developerMode; Q_EMIT developerModeChanged(); }
bool curved = BreezyDesktopConfig::curvedDisplay() && m_curvedDisplaySupported;
if (m_curvedDisplay != curved) { m_curvedDisplay = curved; Q_EMIT curvedDisplayChanged(); }
@ -269,6 +273,11 @@ void BreezyDesktopEffect::reconfigure(ReconfigureFlags)
m_allDisplaysFollowMode = BreezyDesktopConfig::allDisplaysFollowMode();
}
bool BreezyDesktopEffect::developerMode() const
{
return m_developerMode;
}
QVariantMap BreezyDesktopEffect::initialProperties(Output *screen)
{
return QVariantMap{
@ -460,6 +469,10 @@ quint64 BreezyDesktopEffect::poseTimestamp() const {
return m_poseTimestamp;
}
bool BreezyDesktopEffect::poseHasPosition() const {
return m_poseHasPosition;
}
QList<qreal> BreezyDesktopEffect::lookAheadConfig() const {
return m_lookAheadConfig;
}
@ -485,7 +498,7 @@ qreal BreezyDesktopEffect::focusedDisplayDistance() const {
void BreezyDesktopEffect::setFocusedDisplayDistance(qreal distance) {
if (distance != m_focusedDisplayDistance) {
m_focusedDisplayDistance = std::clamp(distance, 0.2, m_allDisplaysDistance);
m_focusedDisplayDistance = std::clamp(distance, 0.1, m_allDisplaysDistance);
Q_EMIT focusedDisplayDistanceChanged();
if (m_smoothFollowEnabled) updateDriverSmoothFollowSettings();
@ -498,7 +511,7 @@ qreal BreezyDesktopEffect::allDisplaysDistance() const {
void BreezyDesktopEffect::setAllDisplaysDistance(qreal distance) {
if (distance != m_allDisplaysDistance) {
qreal min = m_zoomOnFocusEnabled ? m_focusedDisplayDistance : 0.2;
qreal min = m_zoomOnFocusEnabled ? m_focusedDisplayDistance : 0.1;
m_allDisplaysDistance = std::clamp(distance, min, 2.5);
Q_EMIT allDisplaysDistanceChanged();
}
@ -515,6 +528,18 @@ void BreezyDesktopEffect::setDisplaySpacing(qreal spacing) {
}
}
qreal BreezyDesktopEffect::displaySize() const {
return m_displaySize;
}
void BreezyDesktopEffect::setDisplaySize(qreal size) {
const qreal clamped = std::clamp(size, 0.1, 4.0);
if (!qFuzzyCompare(clamped, m_displaySize)) {
m_displaySize = clamped;
Q_EMIT displaySizeChanged();
}
}
qreal BreezyDesktopEffect::displayHorizontalOffset() const {
return m_displayHorizontalOffset;
}
@ -604,7 +629,7 @@ bool BreezyDesktopEffect::checkParityByte(const char* data) {
static qint64 lastConfigUpdate = 0;
static qint64 activatedAt = 0;
void BreezyDesktopEffect::updatePoseOrientation() {
void BreezyDesktopEffect::updatePose() {
// Reentrancy guard: if an update is already in progress, skip
bool expected = false;
if (!m_poseUpdateInProgress.compare_exchange_strong(expected, true)) {
@ -697,7 +722,16 @@ void BreezyDesktopEffect::updatePoseOrientation() {
<< "diagonalFOV:" << m_diagonalFOV;
activate();
m_enabled = true;
m_poseHasPosition = false;
auto driverStateOpt = XRDriverIPC::instance().retrieveDriverState();
if (driverStateOpt) {
QJsonObject driverState = driverStateOpt.value();
if (driverState.contains(QStringLiteral("connected_device_pose_has_position"))) {
m_poseHasPosition = driverState.value(QStringLiteral("connected_device_pose_has_position")).toBool();
}
}
Q_EMIT enabledStateChanged();
Q_EMIT poseHasPositionChanged();
activatedAt = currentTimeMs;
}
@ -788,7 +822,7 @@ void BreezyDesktopEffect::setSmoothFollowThreshold(float threshold) {
}
void BreezyDesktopEffect::updateDriverSmoothFollowSettings() {
qreal adjustedDistance = m_focusedDisplayDistance;
qreal adjustedDistance = m_focusedDisplayDistance / (m_displaySize * m_allDisplaysDistance);
if (m_lookingAtScreenIndex != -1 && !m_displayResolution.isEmpty()) {
// Adjust display distance by relative monitor size compared to the FOV monitor
@ -803,7 +837,7 @@ void BreezyDesktopEffect::updateDriverSmoothFollowSettings() {
const qreal ratioH = static_cast<qreal>(focusedSize.height()) / fovH;
const qreal focusedMonitorSizeAdjustment = std::max(ratioW, ratioH);
adjustedDistance = m_focusedDisplayDistance / focusedMonitorSizeAdjustment;
adjustedDistance /= focusedMonitorSizeAdjustment;
}
}

View File

@ -25,6 +25,7 @@ namespace KWin
Q_PROPERTY(bool zoomOnFocusEnabled READ isZoomOnFocusEnabled WRITE setZoomOnFocusEnabled NOTIFY zoomOnFocusChanged)
Q_PROPERTY(int lookingAtScreenIndex READ lookingAtScreenIndex WRITE setLookingAtScreenIndex)
Q_PROPERTY(bool poseResetState READ poseResetState NOTIFY poseResetStateChanged)
Q_PROPERTY(bool poseHasPosition READ poseHasPosition NOTIFY poseResetStateChanged)
Q_PROPERTY(QList<QQuaternion> poseOrientations READ poseOrientations)
Q_PROPERTY(QVector3D posePosition READ posePosition)
Q_PROPERTY(quint32 poseTimeElapsedMs READ poseTimeElapsedMs)
@ -38,6 +39,7 @@ namespace KWin
Q_PROPERTY(qreal focusedDisplayDistance READ focusedDisplayDistance NOTIFY focusedDisplayDistanceChanged)
Q_PROPERTY(qreal allDisplaysDistance READ allDisplaysDistance NOTIFY allDisplaysDistanceChanged)
Q_PROPERTY(qreal displaySpacing READ displaySpacing NOTIFY displaySpacingChanged)
Q_PROPERTY(qreal displaySize READ displaySize NOTIFY displaySizeChanged)
Q_PROPERTY(qreal displayHorizontalOffset READ displayHorizontalOffset NOTIFY displayOffsetChanged)
Q_PROPERTY(qreal displayVerticalOffset READ displayVerticalOffset NOTIFY displayOffsetChanged)
Q_PROPERTY(int displayWrappingScheme READ displayWrappingScheme NOTIFY displayWrappingSchemeChanged)
@ -52,6 +54,7 @@ namespace KWin
Q_PROPERTY(bool mirrorPhysicalDisplays READ mirrorPhysicalDisplays NOTIFY mirrorPhysicalDisplaysChanged)
Q_PROPERTY(bool curvedDisplay READ curvedDisplay NOTIFY curvedDisplayChanged)
Q_PROPERTY(bool curvedDisplaySupported READ curvedDisplaySupported WRITE setCurvedDisplaySupported NOTIFY curvedDisplaySupportedChanged)
Q_PROPERTY(bool developerMode READ developerMode NOTIFY developerModeChanged)
public:
@ -79,6 +82,7 @@ namespace KWin
quint32 poseTimeElapsedMs() const;
quint64 poseTimestamp() const;
bool poseResetState() const;
bool poseHasPosition() const;
QList<qreal> lookAheadConfig() const;
qreal lookAheadOverride() const;
void setLookAheadOverride(qreal override);
@ -89,6 +93,8 @@ namespace KWin
void setAllDisplaysDistance(qreal distance);
qreal displaySpacing() const;
void setDisplaySpacing(qreal spacing);
qreal displaySize() const;
void setDisplaySize(qreal size);
qreal displayHorizontalOffset() const;
qreal displayVerticalOffset() const;
int displayWrappingScheme() const;
@ -102,6 +108,7 @@ namespace KWin
bool removeVirtualDisplaysOnDisable() const;
bool mirrorPhysicalDisplays() const;
bool curvedDisplay() const;
bool developerMode() const;
void setCurvedDisplaySupported(bool supported);
void showCursor();
@ -114,7 +121,7 @@ namespace KWin
void disableDriver();
void toggle();
void addVirtualDisplay(QSize size);
void updatePoseOrientation();
void updatePose();
void updateCursorImage();
void updateCursorPos();
QVariantList listVirtualDisplays() const;
@ -127,11 +134,13 @@ namespace KWin
void focusedDisplayDistanceChanged();
void allDisplaysDistanceChanged();
void displaySpacingChanged();
void displaySizeChanged();
void displayOffsetChanged();
void displayWrappingSchemeChanged();
void enabledStateChanged();
void zoomOnFocusChanged();
void poseResetStateChanged();
void poseHasPositionChanged();
void sbsEnabledChanged();
void smoothFollowEnabledChanged();
void devicePropertiesChanged();
@ -140,6 +149,7 @@ namespace KWin
void mirrorPhysicalDisplaysChanged();
void curvedDisplayChanged();
void curvedDisplaySupportedChanged();
void developerModeChanged();
void cursorImageSourceChanged();
void cursorPosChanged();
@ -168,6 +178,7 @@ namespace KWin
int m_lookingAtScreenIndex = -1;
int m_effectTargetScreenIndex = -1;
bool m_poseResetState;
bool m_poseHasPosition = false;
QList<QQuaternion> m_poseOrientations;
QVector3D m_posePosition;
quint32 m_poseTimeElapsedMs;
@ -191,6 +202,7 @@ namespace KWin
qreal m_focusedDisplayDistance = 0.85;
qreal m_allDisplaysDistance = 1.05;
qreal m_displaySpacing = 0.0;
qreal m_displaySize = 1.0;
qreal m_displayHorizontalOffset = 0.0;
qreal m_displayVerticalOffset = 0.0;
int m_displayWrappingScheme = 0; // 0=auto,1=horizontal,2=vertical,3=flat
@ -199,6 +211,7 @@ namespace KWin
bool m_mirrorPhysicalDisplays = false;
bool m_curvedDisplay = false;
bool m_curvedDisplaySupported = false;
bool m_developerMode = false;
float m_smoothFollowThreshold = 1.0f;
bool m_allDisplaysFollowMode = false;
bool m_focusedSmoothFollowEnabled = false;

View File

@ -48,6 +48,9 @@
#include <QFile>
#include <QDir>
#include <QJsonDocument>
#include <QDebug>
#include <QLocale>
#include <QSignalBlocker>
#include <cmath>
#include <algorithm>
@ -201,6 +204,27 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu
ui.setupUi(widget());
addConfig(BreezyDesktopConfig::self(), widget());
// Advanced tab: measurement units selector (stored as "cm" or "in")
if (ui.comboMeasurementUnits) {
ui.comboMeasurementUnits->clear();
ui.comboMeasurementUnits->addItem(i18n("Centimeters (cm)"), QStringLiteral("cm"));
ui.comboMeasurementUnits->addItem(i18n("Inches (in)"), QStringLiteral("in"));
{
QSignalBlocker b(ui.comboMeasurementUnits);
const QString saved = KConfigGroup(BreezyDesktopConfig::self()->sharedConfig(), QLatin1String(EFFECT_GROUP))
.readEntry(QStringLiteral("measurement_units"), QStringLiteral("cm"));
const int idx = ui.comboMeasurementUnits->findData(saved);
ui.comboMeasurementUnits->setCurrentIndex(idx >= 0 ? idx : 0);
}
connect(ui.comboMeasurementUnits, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int) {
if (m_updatingFromConfig) return;
applyDistanceLabelFormatters();
save();
});
}
// One-time check if the KWin effect backend is actually loaded. If not, disable UI early.
checkEffectLoaded();
@ -330,6 +354,7 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu
connect(ui.kcfg_ZoomOnFocusEnabled, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_FocusedDisplayDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_AllDisplaysDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_DisplaySize, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_DisplaySpacing, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_SmoothFollowThreshold, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_DisplayHorizontalOffset, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save);
@ -393,6 +418,8 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu
lookAheadOverrideSlider->setValueText(-1, i18n("Default"));
}
applyDistanceLabelFormatters();
renderVirtualDisplays(dbusListVirtualDisplays());
m_virtualDisplayPollTimer.setInterval(15000);
@ -430,6 +457,14 @@ void BreezyDesktopEffectConfig::save()
m_updatingFromConfig = true;
updateConfigFromUi();
BreezyDesktopConfig::self()->save();
// Store measurement_units explicitly (snake_case key) without depending on KConfigXT accessor naming.
{
KConfigGroup grp(BreezyDesktopConfig::self()->sharedConfig(), QLatin1String(EFFECT_GROUP));
grp.writeEntry(QStringLiteral("measurement_units"), measurementUnitsFromUi());
grp.sync();
}
KCModule::save();
ui.kcfg_FocusedDisplayDistance->setEnabled(
ui.kcfg_ZoomOnFocusEnabled->isChecked() || ui.SmoothFollowEnabled->isChecked());
@ -456,6 +491,7 @@ void BreezyDesktopEffectConfig::updateUiFromConfig()
{
ui.kcfg_FocusedDisplayDistance->setValue(BreezyDesktopConfig::self()->focusedDisplayDistance());
ui.kcfg_AllDisplaysDistance->setValue(BreezyDesktopConfig::self()->allDisplaysDistance());
ui.kcfg_DisplaySize->setValue(BreezyDesktopConfig::self()->displaySize());
ui.kcfg_DisplaySpacing->setValue(BreezyDesktopConfig::self()->displaySpacing());
ui.kcfg_DisplayHorizontalOffset->setValue(BreezyDesktopConfig::self()->displayHorizontalOffset());
ui.kcfg_DisplayVerticalOffset->setValue(BreezyDesktopConfig::self()->displayVerticalOffset());
@ -470,11 +506,29 @@ void BreezyDesktopEffectConfig::updateUiFromConfig()
ui.kcfg_FocusedDisplayDistance->setEnabled(
ui.kcfg_ZoomOnFocusEnabled->isChecked() || ui.SmoothFollowEnabled->isChecked());
ui.kcfg_SmoothFollowThreshold->setValue(BreezyDesktopConfig::self()->smoothFollowThreshold());
if (ui.comboMeasurementUnits) {
QSignalBlocker b(ui.comboMeasurementUnits);
const QString saved = KConfigGroup(BreezyDesktopConfig::self()->sharedConfig(), QLatin1String(EFFECT_GROUP))
.readEntry(QStringLiteral("measurement_units"), QStringLiteral("cm"));
const int idx = ui.comboMeasurementUnits->findData(saved);
ui.comboMeasurementUnits->setCurrentIndex(idx >= 0 ? idx : 0);
}
applyDistanceLabelFormatters();
}
void BreezyDesktopEffectConfig::updateUiFromDefaultConfig()
{
ui.shortcutsEditor->allDefault();
if (ui.comboMeasurementUnits) {
QSignalBlocker b(ui.comboMeasurementUnits);
const int idx = ui.comboMeasurementUnits->findData(QStringLiteral("cm"));
ui.comboMeasurementUnits->setCurrentIndex(idx >= 0 ? idx : 0);
}
applyDistanceLabelFormatters();
}
void BreezyDesktopEffectConfig::updateUnmanagedState()
@ -492,7 +546,7 @@ void BreezyDesktopEffectConfig::checkEffectLoaded() {
QPalette pal = warn->palette();
pal.setColor(QPalette::WindowText, QColor(Qt::red));
warn->setPalette(pal);
warn->setText(tr("The Breezy Desktop KWin effect is not loaded. Please log out and back in to enable it."));
warn->setText(tr("The Breezy Desktop KWin effect is disabled or not loaded. Please check the Desktop Effects dialog. Otherwise, log out and back in to enable it."));
warn->setVisible(true);
}
}
@ -646,6 +700,11 @@ void BreezyDesktopEffectConfig::pollDriverState()
auto stateJson = stateJsonOpt.value();
m_connectedDeviceBrand = stateJson.value(QStringLiteral("connected_device_brand")).toString();
m_connectedDeviceModel = stateJson.value(QStringLiteral("connected_device_model")).toString();
m_connectedDeviceFullDistanceCm = stateJson.value(QStringLiteral("connected_device_full_distance_cm")).toDouble(0.0);
m_connectedDeviceFullSizeCm = stateJson.value(QStringLiteral("connected_device_full_size_cm")).toDouble(0.0);
m_connectedDevicePoseHasPosition = stateJson.value(QStringLiteral("connected_device_pose_has_position")).toBool(false);
applyDistanceLabelFormatters();
const bool smoothFollow = smoothFollowEnabled(stateJsonOpt);
if (ui.SmoothFollowEnabled->isChecked() != smoothFollow) {
@ -709,6 +768,54 @@ void BreezyDesktopEffectConfig::pollDriverState()
m_driverStateInitialized = true;
}
QString BreezyDesktopEffectConfig::measurementUnitsFromUi() const
{
if (!ui.comboMeasurementUnits) return QStringLiteral("cm");
const QString v = ui.comboMeasurementUnits->currentData().toString();
if (v == QLatin1String("in")) return QStringLiteral("in");
return QStringLiteral("cm");
}
void BreezyDesktopEffectConfig::applyDistanceLabelFormatters()
{
auto *focused = ui.kcfg_FocusedDisplayDistance;
auto *all = ui.kcfg_AllDisplaysDistance;
if (!focused || !all) {
return;
}
// Only apply the unit conversion labels when the driver reports positional tracking.
if (!m_connectedDevicePoseHasPosition) {
focused->clearValueToDisplayStringFn();
all->clearValueToDisplayStringFn();
focused->setValueUnitsSuffix(QString());
all->setValueUnitsSuffix(QString());
return;
}
const double fullCm = static_cast<double>(m_connectedDeviceFullDistanceCm);
const QString units = measurementUnitsFromUi();
const QLocale loc;
// Units should appear only in the floating value bubble, not on tick labels.
focused->setValueUnitsSuffix(units);
all->setValueUnitsSuffix(units);
LabeledSlider::ValueToDisplayStringFn fn = [fullCm, units, loc](int raw) -> QString {
if (fullCm <= 0.0) return QString();
const double ratio = static_cast<double>(raw) / 100.0; // slider uses a 2-decimal fixed-point scale
const double cm = ratio * fullCm;
if (units == QLatin1String("in")) {
const double inches = cm / 2.54;
return loc.toString(inches, 'f', 1);
}
return loc.toString(cm, 'f', 0);
};
focused->setValueToDisplayStringFn(fn);
all->setValueToDisplayStringFn(fn);
}
double BreezyDesktopEffectConfig::neckSaverHorizontalMultiplier(std::optional<QJsonObject> configJsonOpt)
{
if (!configJsonOpt) return 1.0;

View File

@ -28,6 +28,9 @@ public Q_SLOTS:
void defaults() override;
private:
QString measurementUnitsFromUi() const;
void applyDistanceLabelFormatters();
void updateDriverEnabled();
void updateMultitapEnabled();
void updateSmoothFollowEnabled();
@ -73,6 +76,9 @@ private:
int m_smoothFollowThreshold = 1;
QString m_connectedDeviceBrand;
QString m_connectedDeviceModel;
float m_connectedDeviceFullDistanceCm = 0.0;
float m_connectedDeviceFullSizeCm = 0.0;
bool m_connectedDevicePoseHasPosition = false;
QTimer m_statePollTimer; // periodic driver state polling
QTimer m_virtualDisplayPollTimer; // periodic virtual display list polling
bool m_licenseLoading = false;

View File

@ -52,7 +52,16 @@
<string>&amp;General</string>
</attribute>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0" colspan="2">
<item row="0" column="0" colspan="2">
<widget class="QWidget" name="widgetGeneralCheckboxes" native="true">
<layout class="QGridLayout" name="gridLayoutGeneralCheckboxes" columnstretch="1,1">
<property name="leftMargin"><number>0</number></property>
<property name="topMargin"><number>0</number></property>
<property name="rightMargin"><number>0</number></property>
<property name="bottomMargin"><number>0</number></property>
<property name="horizontalSpacing"><number>12</number></property>
<property name="verticalSpacing"><number>6</number></property>
<item row="0" column="0">
<widget class="QCheckBox" name="EffectEnabled">
<property name="text">
<string>XR Effect enabled</string>
@ -61,8 +70,8 @@
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="kcfg_ZoomOnFocusEnabled">
<property name="text">
<string>Zoom on Focus</string>
@ -71,8 +80,8 @@
<bool>false</bool>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="kcfg_CurvedDisplay">
<property name="text">
<string>Curved display</string>
@ -81,8 +90,8 @@
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="SmoothFollowEnabled">
<property name="text">
<string>Follow mode</string>
@ -91,15 +100,18 @@
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="0">
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelFocusedDisplayDistance">
<property name="text">
<string>Focused Display Distance:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="1" column="1">
<widget class="LabeledSlider" name="kcfg_FocusedDisplayDistance">
<property name="decimalShift">
<double>2</double>
@ -108,10 +120,10 @@
<enum>QSlider::NoTicks</enum>
</property>
<property name="tickStartOffset">
<double>5</double>
<double>15</double>
</property>
<property name="tickInterval">
<double>25</double>
<double>50</double>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -121,14 +133,14 @@
</property>
</widget>
</item>
<item row="5" column="0">
<item row="2" column="0">
<widget class="QLabel" name="labelAllDisplaysDistance">
<property name="text">
<string>All Displays Distance:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<item row="2" column="1">
<widget class="LabeledSlider" name="kcfg_AllDisplaysDistance">
<property name="decimalShift">
<double>2</double>
@ -137,10 +149,10 @@
<enum>QSlider::NoTicks</enum>
</property>
<property name="tickStartOffset">
<double>5</double>
<double>15</double>
</property>
<property name="tickInterval">
<double>25</double>
<double>50</double>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -150,14 +162,43 @@
</property>
</widget>
</item>
<item row="6" column="0">
<item row="3" column="0">
<widget class="QLabel" name="labelDisplaySize">
<property name="text">
<string>Display Size:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="LabeledSlider" name="kcfg_DisplaySize">
<property name="decimalShift">
<double>2</double>
</property>
<property name="tickPosition">
<enum>QSlider::NoTicks</enum>
</property>
<property name="tickStartOffset">
<double>15</double>
</property>
<property name="tickInterval">
<double>50</double>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tracking">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelDisplaySpacing">
<property name="text">
<string>Display Spacing:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<item row="4" column="1">
<widget class="QSlider" name="kcfg_DisplaySpacing">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -167,14 +208,14 @@
</property>
</widget>
</item>
<item row="7" column="0">
<item row="5" column="0">
<widget class="QLabel" name="labelFollowThreshold">
<property name="text">
<string>Follow threshold:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="5" column="1">
<widget class="LabeledSlider" name="kcfg_SmoothFollowThreshold">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -196,7 +237,7 @@
</property>
</widget>
</item>
<item row="8" column="0">
<item row="6" column="0">
<widget class="QLabel" name="labelVirtualDisplays">
<property name="text">
<string>Add Virtual Display:</string>
@ -209,7 +250,7 @@
</property>
</widget>
</item>
<item row="8" column="1">
<item row="6" column="1">
<widget class="QWidget" name="widgetVirtualDisplayButtons">
<property name="visible">
<bool>false</bool>
@ -272,7 +313,7 @@
</layout>
</widget>
</item>
<item row="9" column="0" colspan="2">
<item row="7" column="0" colspan="2">
<widget class="QWidget" name="widgetVirtualDisplayList">
<property name="visible"><bool>false</bool></property>
<property name="enabled"><bool>false</bool></property>
@ -285,15 +326,22 @@
</layout>
</widget>
</item>
<item row="10" column="0" colspan="2">
<widget class="KShortcutsEditor" name="shortcutsEditor" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</layout>
</widget>
<widget class="QWidget" name="tabShortcuts">
<attribute name="title">
<string>&amp;Shortcuts</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayoutShortcuts">
<item>
<widget class="KShortcutsEditor" name="shortcutsEditor" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
@ -605,6 +653,16 @@
</property>
</widget>
</item>
<item row="13" column="0">
<widget class="QLabel" name="labelMeasurementUnits">
<property name="text">
<string>Measurement units:</string>
</property>
</widget>
</item>
<item row="13" column="1">
<widget class="QComboBox" name="comboMeasurementUnits"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabLicenseDetails">

View File

@ -5,6 +5,7 @@
#include <QPainterPath>
#include <QStyleOptionSlider>
#include <algorithm> // for std::max
#include <functional>
#include <QtCore/QMap>
#include <QtCore/QString>
#include <QStringList>
@ -32,7 +33,12 @@ class LabeledSlider : public QSlider {
// tickStartOffset: starting offset for label positions relative to minimum().
// Example: minimum=0, tickInterval=20, tickStartOffset=10 -> labels at 10,30,50,...
Q_PROPERTY(int tickStartOffset READ tickStartOffset WRITE setTickStartOffset)
// Optional units suffix shown ONLY in the floating value bubble.
// Tick labels never include units.
Q_PROPERTY(QString valueUnitsSuffix READ valueUnitsSuffix WRITE setValueUnitsSuffix)
public:
using ValueToDisplayStringFn = std::function<QString(int)>;
explicit LabeledSlider(QWidget *parent = nullptr)
: QSlider(Qt::Horizontal, parent)
{
@ -74,6 +80,32 @@ public:
QMap<int, QString> valueTexts() const { return m_valueTexts; }
QString valueUnitsSuffix() const { return m_valueUnitsSuffix; }
void setValueUnitsSuffix(const QString &suffix) {
if (m_valueUnitsSuffix == suffix) return;
m_valueUnitsSuffix = suffix;
updateGeometry();
update();
}
// Optional custom formatter for displayed values.
// If set, it is consulted for values without an explicit setValueText() override.
// Returning a null QString (QString()) falls back to the built-in formatting.
void setValueToDisplayStringFn(ValueToDisplayStringFn fn) {
m_valueToDisplayStringFn = std::move(fn);
updateGeometry();
update();
}
void clearValueToDisplayStringFn() {
if (!m_valueToDisplayStringFn) return;
m_valueToDisplayStringFn = nullptr;
updateGeometry();
update();
}
bool hasValueToDisplayStringFn() const { return static_cast<bool>(m_valueToDisplayStringFn); }
int decimalShift() const { return m_decimalShift; }
void setDecimalShift(int shift) {
// clamp to sensible range
@ -141,7 +173,7 @@ protected:
optPos.sliderValue = v;
QRect handleAtVal = style()->subControlRect(QStyle::CC_Slider, &optPos, QStyle::SC_SliderHandle, this);
int x = handleAtVal.center().x();
QString text = valueToDisplayString(v);
QString text = valueToDisplayString(v, /*forValueBubble=*/false);
int halfW = fm.horizontalAdvance(text) / 2;
QRect r(x - halfW, baselineY - fm.ascent(), fm.horizontalAdvance(text), fm.height());
p.drawText(r, Qt::AlignCenter, text);
@ -152,7 +184,7 @@ protected:
if (m_showValueBubble) {
// Handle rect
const QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this);
QString valText = valueToDisplayString(value());
QString valText = valueToDisplayString(value(), /*forValueBubble=*/true);
QFontMetrics fm(font());
QRect textRect = fm.boundingRect(valText);
textRect.adjust(-6, -4, 6, 4);
@ -187,29 +219,57 @@ protected:
}
private:
QString valueToDisplayString(int raw) const {
QString valueToDisplayString(int raw, bool forValueBubble) const {
// Use custom text if provided for this exact integer value
auto it = m_valueTexts.constFind(raw);
if (it != m_valueTexts.constEnd()) {
return *it;
}
if (m_decimalShift == 0) {
return QString::number(raw);
auto decimalShiftString = [this](int r) -> QString {
if (m_decimalShift == 0) {
return QString::number(r);
}
int divisor = 1;
for (int i = 0; i < m_decimalShift; ++i) divisor *= 10;
int whole = r / divisor;
int frac = std::abs(r % divisor);
QString fracStr = QString::number(frac).rightJustified(m_decimalShift, QLatin1Char('0'));
QString result = QString::number(std::abs(whole)) + QLatin1Char('.') + fracStr;
if (r < 0) result.prepend(QLatin1Char('-'));
return result;
};
QString base;
if (m_valueToDisplayStringFn) {
base = m_valueToDisplayStringFn(raw);
}
int divisor = 1;
for (int i = 0; i < m_decimalShift; ++i) divisor *= 10;
int whole = raw / divisor;
int frac = std::abs(raw % divisor);
QString fracStr = QString::number(frac).rightJustified(m_decimalShift, QLatin1Char('0'));
QString result = QString::number(std::abs(whole)) + QLatin1Char('.') + fracStr;
if (raw < 0) result.prepend(QLatin1Char('-'));
return result;
if (base.isNull()) {
base = decimalShiftString(raw);
}
if (!forValueBubble) {
return base;
}
const QString suffix = m_valueUnitsSuffix.trimmed();
if (suffix.isEmpty() || base.isEmpty()) {
return base;
}
// Avoid double-appending if caller included units in the formatter.
const QString baseTrimmed = base.trimmed();
if (baseTrimmed.endsWith(suffix) || baseTrimmed.endsWith(QLatin1Char(' ') + suffix)) {
return base;
}
return baseTrimmed + QLatin1Char(' ') + suffix;
}
bool m_showValueBubble = true;
int m_decimalShift = 0; // display-only decimal shift
int m_tickStartOffset = 0; // label positions start offset relative to minimum
QMap<int, QString> m_valueTexts; // optional text overrides for specific values
ValueToDisplayStringFn m_valueToDisplayStringFn; // optional custom formatter
QString m_valueUnitsSuffix; // shown only in the value bubble
private:
int labelInterval() const {
int ti = tickInterval();

View File

@ -8,6 +8,7 @@ Node {
property var viewportResolution: effect.displayResolution
property bool smoothFollowEnabled: effect.smoothFollowEnabled
required property var screens
required property var sizeAdjustedScreens
required property var fovDetails
required property var monitorPlacements
property int focusedMonitorIndex: -1
@ -28,14 +29,16 @@ Node {
function updateFocus(smoothFollowEnabledChanged = false) {
const orientations = smoothFollowEnabled ? effect.smoothFollowOrigin : effect.poseOrientations;
if (orientations && orientations.length > 0) {
const posePosition = effect.posePosition.times(breezyDesktop.fovDetails.fullScreenDistancePixels);
let focusedIndex = -1;
const lookingAtIndex = displays.findFocusedMonitor(
displays.eusToNwuQuat(orientations[0]),
displays.eusToNwuVector(posePosition),
breezyDesktop.monitorPlacements.map(monitorVectors => monitorVectors.centerLook),
breezyDesktop.focusedMonitorIndex,
smoothFollowEnabled,
breezyDesktop.fovDetails,
breezyDesktop.screens.map(screen => screen.geometry)
breezyDesktop.sizeAdjustedScreens.map(screen => screen.geometry)
);
if (breezyDesktop.lookingAtMonitorIndex !== lookingAtIndex) {
@ -158,6 +161,7 @@ Node {
model: breezyDesktop.screens.length
delegate: BreezyDesktopDisplay {
screen: breezyDesktop.screens[index]
sizeAdjustedScreen: breezyDesktop.sizeAdjustedScreens[index]
monitorPlacement: breezyDesktop.monitorPlacements[index]
fovDetails: breezyDesktop.fovDetails
@ -175,7 +179,9 @@ Node {
// only for the Rectangle geometry fallback
property vector3d rectangleFallbackScale: {
const geometry = screen.geometry;
if (!sizeAdjustedScreen) return Qt.vector3d(1, 1, 1);
const geometry = sizeAdjustedScreen.geometry;
// default geometry unit size is 100x100, so we scale it up to the screen size
return Qt.vector3d(geometry.width / 100, geometry.height / 100, 1);

View File

@ -4,7 +4,8 @@ import QtQuick3D
Model {
id: display
required property QtObject screen
required property var screen
required property var sizeAdjustedScreen
required property var monitorPlacement
required property int index
required property var fovDetails
@ -27,7 +28,7 @@ Model {
if (component.status === Component.Ready) {
const mesh = component.createObject(display, {
fovDetails: Qt.binding(() => display.fovDetails),
monitorGeometry: Qt.binding(() => display.screen ? display.screen.geometry : null),
monitorGeometry: Qt.binding(() => display.sizeAdjustedScreen ? display.sizeAdjustedScreen.geometry : null),
fovConversionFns: Qt.binding(() => displays.fovConversionFns)
});
if (mesh) {

View File

@ -17,10 +17,12 @@ Item {
property bool customBannerEnabled: effect.customBannerEnabled
property bool smoothFollowEnabled: effect.smoothFollowEnabled
property real lookAheadScanlineMs: effect.lookAheadConfig[2]
property var crossFovs: displays.diagonalToCrossFOVs(
property var fovLengths: displays.diagonalToCrossFOVs(
displays.degreeToRadian(effect.diagonalFOV),
aspectRatio
);
property real fovHalfVerticalTangent: fovLengths.heightUnitDistance / 2.0;
property real fovHalfHorizontalTangent: fovLengths.widthUnitDistance / 2.0;
// if true, then smoothFollowEnabled just cleared and the orientation data is slerping back,
// continue to use the origin data for the duration of the Timer
@ -58,7 +60,13 @@ Item {
effect.lookAheadOverride
)
);
camera.position = position.times(fovDetails.completeScreenDistancePixels).plus(orientations[0].times(Qt.vector3d(0, 0, -fovDetails.lensDistancePixels)));
let lensVector = Qt.vector3d(0, 0, -fovDetails.lensDistancePixels);
// if we only have 3DoF, account for a bit of positional change based on orientation,
// don't do this for 6DoF to prevent doubling the positional movement due to rotation
if (!effect.poseHasPosition) lensVector = orientations[0].times(lensVector);
camera.position = position.times(fovDetails.fullScreenDistancePixels).plus(lensVector);
}
// how far to look ahead is how old the pose data is plus a constant that is either the default for this device or an override
@ -84,7 +92,7 @@ Item {
}
function buildPerspectiveMatrix() {
const f = 1.0 / crossFovs.verticalTangent;
const f = 1.0 / fovHalfVerticalTangent;
const nf = 1.0 / (clipNear - clipFar);
const m00 = f / aspectRatio;
const m11 = f;
@ -102,13 +110,13 @@ Item {
function applyRollingShutterShear(rates) {
// Convert to maximum shift at bottom of frame
const maxDxNdc = (rates.yaw * lookAheadScanlineMs) / crossFovs.horizontalTangent;
const maxDyNdc = -(rates.pitch * lookAheadScanlineMs) / crossFovs.verticalTangent;
const maxDxNdc = (rates.yaw * lookAheadScanlineMs) / fovHalfHorizontalTangent;
const maxDyNdc = -(rates.pitch * lookAheadScanlineMs) / fovHalfVerticalTangent;
let shx = maxDxNdc / 2.0;
let shy = maxDyNdc / 2.0;
const f = 1.0 / crossFovs.verticalTangent;
const f = 1.0 / fovHalfVerticalTangent;
const nf = 1.0 / (clipNear - clipFar);
const m00 = f / aspectRatio;
const m11 = f;

View File

@ -26,27 +26,26 @@ ProceduralMesh {
const fov = mesh.fovDetails;
const monitor = mesh.monitorGeometry;
const conv = fov.curvedDisplay ? mesh.fovConversionFns.curved
: mesh.fovConversionFns.flat;
const horizontalWrap = fov.monitorWrappingScheme === 'horizontal';
const verticalWrap = fov.monitorWrappingScheme === 'vertical';
const horizontalConversions = horizontalWrap && fov.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat;
const sideEdgeDistance = conv.centerToFovEdgeDistance(
fov.completeScreenDistancePixels, fov.widthPixels);
const horizontalRadians = conv.lengthToRadians(
const sideEdgeDistancePixels = horizontalConversions.centerToFovEdgeDistance(
fov.completeScreenDistancePixels, fov.sizeAdjustedWidthPixels);
const horizontalRadians = horizontalConversions.lengthToRadians(
fov.defaultDistanceHorizontalRadians,
fov.widthPixels,
sideEdgeDistance,
sideEdgeDistancePixels,
monitor.width
);
const topEdgeDistance = conv.centerToFovEdgeDistance(
fov.completeScreenDistancePixels, fov.heightPixels);
const verticalRadians = conv.lengthToRadians(
const verticalWrap = fov.monitorWrappingScheme === 'vertical';
const verticalConversions = verticalWrap && fov.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat;
const topEdgeDistancePixels = verticalConversions.centerToFovEdgeDistance(
fov.completeScreenDistancePixels, fov.sizeAdjustedHeightPixels);
const verticalRadians = verticalConversions.lengthToRadians(
fov.defaultDistanceVerticalRadians,
fov.heightPixels,
topEdgeDistance,
topEdgeDistancePixels,
monitor.height
);
@ -78,8 +77,8 @@ ProceduralMesh {
}
let segments = 1;
if (horizontalWrap) segments = conv.radiansToSegments(horizontalRadians);
if (verticalWrap) segments = conv.radiansToSegments(verticalRadians);
if (horizontalWrap) segments = horizontalConversions.radiansToSegments(horizontalRadians);
if (verticalWrap) segments = verticalConversions.radiansToSegments(verticalRadians);
for (let i = 0; i <= segments; i++) {
const texFraction = i / segments;

View File

@ -4,7 +4,7 @@ import org.kde.kwin as KWinComponents
Item {
id: desktopView
required property QtObject screen
required property var screen
function overlapsScreen(win, screenGeom) {
if (!win) return false

View File

@ -18,22 +18,35 @@ QtObject {
return Qt.vector3d(-vector.y, vector.z, -vector.x);
}
function eusToNwuVector(vector) {
// Converts EUS vector to NWU vector
return Qt.vector3d(-vector.z, -vector.x, vector.y);
}
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
// Converts diagonal FOV in radians and aspect ratio to horizontal and vertical FOV measurements
function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) {
var diagonalTangent = Math.tan(diagonalFOVRadians / 2);
var verticalTangent = diagonalTangent / Math.sqrt(1 + aspectRatio * aspectRatio);
var horizontalTangent = verticalTangent * 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 {
diagonal: diagonalFOVRadians,
horizontal: 2 * Math.atan(horizontalTangent),
horizontalTangent: horizontalTangent,
vertical: 2 * Math.atan(verticalTangent),
verticalTangent: verticalTangent
// 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
}
}
@ -50,33 +63,55 @@ QtObject {
}
}
function buildFovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio, defaultDisplayDistance, wrappingChoice) {
function buildFovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio, defaultDisplayDistance, wrappingChoice, distanceAdjustedSize) {
const aspect = viewportWidth / viewportHeight;
const crossFovs = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect);
const defaultDistanceVerticalRadians = 2 * Math.atan(crossFovs.verticalTangent / defaultDisplayDistance);
const defaultDistanceHorizontalRadians = 2 * Math.atan(crossFovs.horizontalTangent / defaultDisplayDistance);
// distance needed for the FOV-sized monitor to fill up the screen
const fullScreenDistance = viewportHeight / (2 * crossFovs.verticalTangent);
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;
const fovLengths = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect);
let monitorWrappingScheme = actualWrapScheme(screens, viewportWidth, viewportHeight);
if (wrappingChoice === 1) monitorWrappingScheme = 'horizontal';
else if (wrappingChoice === 2) monitorWrappingScheme = 'vertical';
else if (wrappingChoice === 3) monitorWrappingScheme = 'flat';
const lensDistanceComplement = 1.0 - lensDistanceRatio;
const lensDistanceFactor = (1.0 / lensDistanceComplement) - 1.0;
const horizontalConversions = effect.curvedDisplay && monitorWrappingScheme === 'horizontal' ? fovConversionFns.curved : fovConversionFns.flat;
const verticalConversions = effect.curvedDisplay && monitorWrappingScheme === 'vertical' ? fovConversionFns.curved : fovConversionFns.flat;
const defaultDistanceVerticalRadians = verticalConversions.fovRadiansAtDistance(
fovLengths.verticalRadians,
fovLengths.heightUnitDistance,
defaultDisplayDistance
);
const defaultDistanceHorizontalRadians = horizontalConversions.fovRadiansAtDistance(
fovLengths.horizontalRadians,
fovLengths.widthUnitDistance,
defaultDisplayDistance
);
// distance needed for the FOV-sized monitor to fill up the screen, as measured from the lenses
const lensToUnitDistancePixels = viewportWidth / fovLengths.widthUnitDistance;
// distance from pivot point to lens
const lensDistancePixels = lensToUnitDistancePixels * lensDistanceFactor;
// distance from pivot point to full screen (monitor at unit distance from lens)
const fullScreenDistancePixels = lensToUnitDistancePixels + lensDistancePixels;
// distance of a display at the default (most zoomed out) distance from the pivot point
const completeScreenDistancePixels = fullScreenDistancePixels * defaultDisplayDistance;
return {
widthPixels: viewportWidth,
distanceAdjustedSize,
sizeAdjustedWidthPixels: viewportWidth * distanceAdjustedSize,
heightPixels: viewportHeight,
sizeAdjustedHeightPixels: viewportHeight * distanceAdjustedSize,
defaultDistanceVerticalRadians,
defaultDistanceHorizontalRadians,
lensDistancePixels,
fullScreenDistancePixels,
completeScreenDistancePixels,
monitorWrappingScheme: monitorWrappingScheme,
monitorWrappingScheme,
curvedDisplay: effect.curvedDisplay
};
}
@ -99,6 +134,9 @@ QtObject {
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: {
@ -114,6 +152,9 @@ QtObject {
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);
}
@ -198,8 +239,12 @@ QtObject {
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;
// monitors wrap around us horizontally
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,
@ -209,14 +254,14 @@ QtObject {
);
};
cachedMonitorRadians[0] = -fovDetails.defaultDistanceHorizontalRadians / 2;
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.heightPixels) * monitorSpacingPixels;
var upCenterOffsetPixels = (monitorDetails.height - fovDetails.heightPixels) / 2;
var upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.sizeAdjustedHeightPixels) * monitorSpacingPixels;
var upCenterOffsetPixels = (monitorDetails.height - fovDetails.sizeAdjustedHeightPixels) / 2;
var upCenterPixels = upTopPixels - upCenterOffsetPixels;
monitorPlacements.push({
@ -231,7 +276,7 @@ QtObject {
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
-monitorCenterRadius * Math.sin(monitorWrapDetails.center),
upCenterPixels
).normalized(),
),
rotationAngleRadians: {
x: 0,
y: -monitorWrapDetails.center
@ -239,8 +284,8 @@ QtObject {
});
});
} else if (fovDetails.monitorWrappingScheme === 'vertical') {
var topEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.heightPixels);
var monitorSpacingPixels = monitorSpacing * fovDetails.heightPixels;
var topEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.sizeAdjustedHeightPixels);
var monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedHeightPixels;
var lengthToRadianFn = function(targetHeight) {
return conversionFns.lengthToRadians(
fovDetails.defaultDistanceVerticalRadians,
@ -250,14 +295,14 @@ QtObject {
);
};
cachedMonitorRadians[0] = -fovDetails.defaultDistanceVerticalRadians / 2;
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.widthPixels) * monitorSpacingPixels;
var westCenterOffsetPixels = (monitorDetails.width - fovDetails.widthPixels) / 2;
var westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.sizeAdjustedWidthPixels) * monitorSpacingPixels;
var westCenterOffsetPixels = (monitorDetails.width - fovDetails.sizeAdjustedWidthPixels) / 2;
var westCenterPixels = westLeftPixels - westCenterOffsetPixels;
monitorPlacements.push({
@ -272,7 +317,7 @@ QtObject {
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
westCenterPixels,
-monitorCenterRadius * Math.sin(monitorWrapDetails.center)
).normalized(),
),
rotationAngleRadians: {
x: -monitorWrapDetails.center,
y: 0
@ -280,12 +325,12 @@ QtObject {
});
});
} else {
var monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels;
var monitorSpacingPixels = monitorSpacing * fovDetails.sizeAdjustedWidthPixels;
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 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;
@ -301,7 +346,7 @@ QtObject {
fovDetails.completeScreenDistancePixels,
westCenterPixels,
upCenterPixels
).normalized(),
),
rotationAngleRadians: {
x: 0,
y: 0
@ -318,35 +363,41 @@ QtObject {
// 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,
fovDetails.completeScreenDistancePixels,
monitorDistance,
monitorVector.z,
monitorVector.x
);
var upPercentage = Math.abs(lookUpPixels - vectorUpPixels) / monitorDetails.height;
) * distanceAdjustment;
var upPercentage = Math.abs(lookUpPixels * distanceAdjustment - vectorUpPixels) / monitorDetails.height;
var vectorWestPixels = westAngleToLength(
fovDetails.defaultDistanceHorizontalRadians,
fovDetails.widthPixels,
fovDetails.completeScreenDistancePixels,
monitorDistance,
monitorVector.y,
monitorVector.x
);
var westPercentage = Math.abs(lookWestPixels - vectorWestPixels) / monitorDetails.width;
) * 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);
}
function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, smoothFollowEnabled, fovDetails, monitorsDetails) {
function findFocusedMonitor(quaternion, position, monitorVectors, currentFocusedIndex, smoothFollowEnabled, fovDetails, monitorsDetails) {
if (currentFocusedIndex !== -1 && smoothFollowEnabled) return currentFocusedIndex;
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
// 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
var upConversionFns = fovDetails.monitorWrappingScheme === "vertical" ? fovConversionFns.curved : fovConversionFns.flat;
var lookUpPixels = upConversionFns.angleToLength(
fovDetails.defaultDistanceVerticalRadians,
@ -364,13 +415,17 @@ QtObject {
rotatedLookVector.x
);
function vectorRelativeToPosition(vector) {
return vector.minus(position);
}
// Check current focused monitor first
if (currentFocusedIndex !== -1) {
var focusedDistance = getMonitorDistance(
fovDetails,
lookUpPixels,
lookWestPixels,
monitorVectors[currentFocusedIndex],
vectorRelativeToPosition(monitorVectors[currentFocusedIndex]),
monitorsDetails[currentFocusedIndex],
upConversionFns.angleToLength,
westConversionFns.angleToLength
@ -390,7 +445,7 @@ QtObject {
fovDetails,
lookUpPixels,
lookWestPixels,
monitorVectors[i],
vectorRelativeToPosition(monitorVectors[i]),
monitorsDetails[i],
upConversionFns.angleToLength,
westConversionFns.angleToLength

View File

@ -30,22 +30,45 @@ Item {
property real viewportDiagonalFOVDegrees: effect.diagonalFOV
property var viewportResolution: effect.displayResolution
property bool mirrorPhysicalDisplays: effect.mirrorPhysicalDisplays
property bool developerMode: effect.developerMode
property var screens: KWinComponents.Workspace.screens.filter(function(screen) {
return mirrorPhysicalDisplays || screen.name.includes("BreezyDesktop") || supportedModels.some(model => screen.model.includes(model));
return developerMode || mirrorPhysicalDisplays || screen.name.includes("BreezyDesktop") || supportedModels.some(model => screen.model.includes(model));
})
property real distanceAdjustedSize: (effect.allDisplaysDistance - effect.lensDistanceRatio) * effect.displaySize
property var sizeAdjustedScreens: screens.map(function(screen) {
const sizeComplement = (1.0 - distanceAdjustedSize) / 2.0;
const sizeViewportOffsetX = sizeComplement * viewportResolution[0];
const sizeViewportOffsetY = sizeComplement * viewportResolution[1];
return {
geometry: {
x: screen.geometry.x * distanceAdjustedSize + sizeViewportOffsetX,
y: screen.geometry.y * distanceAdjustedSize + sizeViewportOffsetY,
width: screen.geometry.width * distanceAdjustedSize,
height: screen.geometry.height * distanceAdjustedSize
},
name: screen.name,
model: screen.model
};
})
property var sizeAdjustedViewport: {
return {
width: viewportResolution[0] * distanceAdjustedSize,
height: viewportResolution[1] * distanceAdjustedSize
};
}
// x value for placing the viewport in the middle of all screens
property real screensXMid: {
let xMin = Number.MAX_VALUE;
let xMax = Number.MIN_VALUE;
for (let i = 0; i < screens.length; i++) {
const geometry = screens[i].geometry;
for (let i = 0; i < sizeAdjustedScreens.length; i++) {
const geometry = sizeAdjustedScreens[i].geometry;
xMin = Math.min(xMin, geometry.x);
xMax = Math.max(xMax, geometry.x + geometry.width);
}
return (xMin + xMax) / 2 - (viewportResolution[0] / 2);
return (xMin + xMax) / 2 - (sizeAdjustedViewport.width / 2);
}
// y value for placing the viewport in the middle of all screens
@ -53,13 +76,13 @@ Item {
let yMin = Number.MAX_VALUE;
let yMax = Number.MIN_VALUE;
for (let i = 0; i < screens.length; i++) {
const geometry = screens[i].geometry;
for (let i = 0; i < sizeAdjustedScreens.length; i++) {
const geometry = sizeAdjustedScreens[i].geometry;
yMin = Math.min(yMin, geometry.y);
yMax = Math.max(yMax, geometry.y + geometry.height);
}
return (yMin + yMax) / 2 - (viewportResolution[1] / 2);
return (yMin + yMax) / 2 - (sizeAdjustedViewport.height / 2);
}
Displays {
@ -67,19 +90,20 @@ Item {
}
property var fovDetails: displays.buildFovDetails(
screens,
sizeAdjustedScreens,
viewportResolution[0],
viewportResolution[1],
viewportDiagonalFOVDegrees,
effect.lensDistanceRatio,
effect.allDisplaysDistance,
effect.displayWrappingScheme
effect.displayWrappingScheme,
distanceAdjustedSize
)
property var monitorPlacements: {
const dx = effect.displayHorizontalOffset * viewportResolution[0];
const dy = effect.displayVerticalOffset * viewportResolution[1];
const adjustedGeometries = screens.map(screen => {
const dx = effect.displayHorizontalOffset * sizeAdjustedViewport.width;
const dy = effect.displayVerticalOffset * sizeAdjustedViewport.height;
const adjustedGeometries = sizeAdjustedScreens.map(screen => {
const g = screen.geometry;
return {
x: g.x - screensXMid + dx,
@ -91,7 +115,7 @@ Item {
return displays.monitorsToPlacements(fovDetails, adjustedGeometries, effect.displaySpacing);
}
property bool targetScreenSupported: supportedModels.some(model => root.targetScreen.model.includes(model))
property bool targetScreenSupported: developerMode || supportedModels.some(model => root.targetScreen.model.includes(model))
property bool targetScreenIsVirtual: targetScreen.name.includes("BreezyDesktop")
property bool poseResetState: effect.poseResetState
property bool isEnabled: effect.isEnabled
@ -122,6 +146,7 @@ Item {
BreezyDesktop {
id: breezyDesktop
screens: root.screens
sizeAdjustedScreens: root.sizeAdjustedScreens
fovDetails: root.fovDetails
monitorPlacements: root.monitorPlacements
}

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

@ -1 +1 @@
Subproject commit 1655c2bb03a4e75bf2c9a9815a96b71342396fbd
Subproject commit 44657a5de6532ac0ec1a4ce6d3ccd73e2c87fdc6