diff --git a/CHANGELOG.md b/CHANGELOG.md index c26409509..9a733fe5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `QSet::reserve` to reserve capacity up-front. - Support for further types: `QUuid` - New example: Basic greeter app -- Support for further types: `qreal`, `qint64`, `qintptr`, `qsizetype`, `quint64`, `quintptr` +- Support for further types: `qreal`, `qint64`, `qintptr`, `qsizetype`, `quint64`, `quintptr`, `QQuaternion` - Support for `cfg` attributes through to C++ generation - CXX-Qt-build: Improved compile time and propagation of initializers between crates - CXX-Qt-build: Multi-crate projects are now possible with Cargo and CMake (see `examples/qml_multi_crates`) diff --git a/crates/cxx-qt-lib/build.rs b/crates/cxx-qt-lib/build.rs index d67e36251..0336593db 100644 --- a/crates/cxx-qt-lib/build.rs +++ b/crates/cxx-qt-lib/build.rs @@ -201,6 +201,7 @@ fn main() { rust_bridges.extend([ "core/qlist/qlist_qcolor", "core/qvariant/qvariant_qcolor", + "core/qvariant/qvariant_qquaternion", "core/qvariant/qvariant_qvector2d", "core/qvariant/qvariant_qvector3d", "core/qvariant/qvariant_qvector4d", @@ -209,6 +210,7 @@ fn main() { "gui/qfont", "gui/qguiapplication", "gui/qimage", + "gui/qquaternion", "gui/qpainterpath", "gui/qpainter", "gui/qpen", @@ -286,6 +288,7 @@ fn main() { "gui/qpen", "gui/qpolygon", "gui/qpolygonf", + "gui/qquaternion", "gui/qregion", "gui/qvector2d", "gui/qvector3d", diff --git a/crates/cxx-qt-lib/include/common.h b/crates/cxx-qt-lib/include/common.h index a4948d034..c3f438a41 100644 --- a/crates/cxx-qt-lib/include/common.h +++ b/crates/cxx-qt-lib/include/common.h @@ -89,6 +89,13 @@ operatorDiv(const S scalar, const T& t) return t / scalar; } +template +T +operatorNeg(const T& t) +{ + return -t; +} + template std::unique_ptr make_unique(Args... args) diff --git a/crates/cxx-qt-lib/include/core/qvariant.h b/crates/cxx-qt-lib/include/core/qvariant.h index 00f114cb8..101e19206 100644 --- a/crates/cxx-qt-lib/include/core/qvariant.h +++ b/crates/cxx-qt-lib/include/core/qvariant.h @@ -33,6 +33,7 @@ #ifdef CXX_QT_GUI_FEATURE #include +#include #include #include #include @@ -83,6 +84,7 @@ CXX_QT_QVARIANT_CAN_CONVERT(I64) CXX_QT_QVARIANT_CAN_CONVERT(QByteArray) #ifdef CXX_QT_GUI_FEATURE CXX_QT_QVARIANT_CAN_CONVERT(QColor) +CXX_QT_QVARIANT_CAN_CONVERT(QQuaternion) CXX_QT_QVARIANT_CAN_CONVERT(QVector2D) CXX_QT_QVARIANT_CAN_CONVERT(QVector3D) CXX_QT_QVARIANT_CAN_CONVERT(QVector4D) diff --git a/crates/cxx-qt-lib/include/gui/qgenericmatrix.h b/crates/cxx-qt-lib/include/gui/qgenericmatrix.h new file mode 100644 index 000000000..6f49e89a1 --- /dev/null +++ b/crates/cxx-qt-lib/include/gui/qgenericmatrix.h @@ -0,0 +1,9 @@ +// clang-format off +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// clang-format on +// SPDX-FileContributor: Joshua Booth +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +#pragma once + +#include diff --git a/crates/cxx-qt-lib/include/gui/qquaternion.h b/crates/cxx-qt-lib/include/gui/qquaternion.h new file mode 100644 index 000000000..a8eb905fb --- /dev/null +++ b/crates/cxx-qt-lib/include/gui/qquaternion.h @@ -0,0 +1,48 @@ +// clang-format off +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// clang-format on +// SPDX-FileContributor: Joshua Booth +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +#pragma once + +#include + +namespace rust { +namespace cxxqtlib1 { + +inline float (*qquaternionDotProduct)(const QQuaternion&, const QQuaternion&) = + QQuaternion::dotProduct; + +inline QQuaternion (*qquaternionFromAxes)(const QVector3D&, + const QVector3D&, + const QVector3D&) = + QQuaternion::fromAxes; + +inline QQuaternion (*qquaternionFromAxisAndAngle)(float, float, float, float) = + QQuaternion::fromAxisAndAngle; + +inline QQuaternion (*qquaternionFromDirection)(const QVector3D&, + const QVector3D&) = + QQuaternion::fromDirection; + +inline QQuaternion (*qquaternionFromEulerAngles)(float, float, float) = + QQuaternion::fromEulerAngles; + +inline QQuaternion (*qquaternionFromRotationMatrix)(const QMatrix3x3&) = + QQuaternion::fromRotationMatrix; + +inline QQuaternion (*qquaternionNlerp)(const QQuaternion&, + const QQuaternion&, + float) = QQuaternion::nlerp; + +inline QQuaternion (*qquaternionRotationTo)(const QVector3D&, + const QVector3D&) = + QQuaternion::rotationTo; + +inline QQuaternion (*qquaternionSlerp)(const QQuaternion&, + const QQuaternion&, + float) = QQuaternion::slerp; + +} +} diff --git a/crates/cxx-qt-lib/include/qgenericmatrix.h b/crates/cxx-qt-lib/include/qgenericmatrix.h new file mode 100644 index 000000000..bb373ec10 --- /dev/null +++ b/crates/cxx-qt-lib/include/qgenericmatrix.h @@ -0,0 +1,9 @@ +// clang-format off +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// clang-format on +// SPDX-FileContributor: Joshua Booth +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +#pragma once + +#include "gui/qgenericmatrix.h" diff --git a/crates/cxx-qt-lib/include/qquaternion.h b/crates/cxx-qt-lib/include/qquaternion.h new file mode 100644 index 000000000..7a6ff62bb --- /dev/null +++ b/crates/cxx-qt-lib/include/qquaternion.h @@ -0,0 +1,9 @@ +// clang-format off +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// clang-format on +// SPDX-FileContributor: Joshua Booth +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +#pragma once + +#include "gui/qquaternion.h" diff --git a/crates/cxx-qt-lib/src/core/qvariant/generate.sh b/crates/cxx-qt-lib/src/core/qvariant/generate.sh index 8b5840ccb..2745aeae3 100755 --- a/crates/cxx-qt-lib/src/core/qvariant/generate.sh +++ b/crates/cxx-qt-lib/src/core/qvariant/generate.sh @@ -108,6 +108,7 @@ generate_bridge_qt "QModelIndex" "qmodelindex" generate_bridge_qt "QPersistentModelIndex" "qpersistentmodelindex" generate_bridge_qt "QPoint" "qpoint" generate_bridge_qt "QPointF" "qpointf" +generate_bridge_qt "QQuaternion" "qquaternion" generate_bridge_qt "QRect" "qrect" generate_bridge_qt "QRectF" "qrectf" generate_bridge_qt "QSize" "qsize" diff --git a/crates/cxx-qt-lib/src/core/qvariant/mod.rs b/crates/cxx-qt-lib/src/core/qvariant/mod.rs index 0008d7558..1f302a2f2 100644 --- a/crates/cxx-qt-lib/src/core/qvariant/mod.rs +++ b/crates/cxx-qt-lib/src/core/qvariant/mod.rs @@ -204,6 +204,8 @@ impl_qvariant_value!(crate::QModelIndex, qvariant_qmodelindex); impl_qvariant_value!(crate::QPersistentModelIndex, qvariant_qpersistentmodelindex); impl_qvariant_value!(crate::QPoint, qvariant_qpoint); impl_qvariant_value!(crate::QPointF, qvariant_qpointf); +#[cfg(feature = "qt_gui")] +impl_qvariant_value!(crate::QQuaternion, qvariant_qquaternion); impl_qvariant_value!(crate::QRect, qvariant_qrect); impl_qvariant_value!(crate::QRectF, qvariant_qrectf); impl_qvariant_value!(crate::QSize, qvariant_qsize); diff --git a/crates/cxx-qt-lib/src/core/qvariant/qvariant.cpp b/crates/cxx-qt-lib/src/core/qvariant/qvariant.cpp index 0eb2285c9..7bdf60228 100644 --- a/crates/cxx-qt-lib/src/core/qvariant/qvariant.cpp +++ b/crates/cxx-qt-lib/src/core/qvariant/qvariant.cpp @@ -68,6 +68,7 @@ CXX_QT_QVARIANT_CAN_CONVERT_IMPL(::std::int64_t, I64) CXX_QT_QVARIANT_CAN_CONVERT_IMPL(::QByteArray, QByteArray) #ifdef CXX_QT_GUI_FEATURE CXX_QT_QVARIANT_CAN_CONVERT_IMPL(::QColor, QColor) +CXX_QT_QVARIANT_CAN_CONVERT_IMPL(::QQuaternion, QQuaternion) CXX_QT_QVARIANT_CAN_CONVERT_IMPL(::QVector2D, QVector2D) CXX_QT_QVARIANT_CAN_CONVERT_IMPL(::QVector3D, QVector3D) CXX_QT_QVARIANT_CAN_CONVERT_IMPL(::QVector4D, QVector4D) diff --git a/crates/cxx-qt-lib/src/core/qvariant/qvariant_qquaternion.rs b/crates/cxx-qt-lib/src/core/qvariant/qvariant_qquaternion.rs new file mode 100644 index 000000000..7eee22602 --- /dev/null +++ b/crates/cxx-qt-lib/src/core/qvariant/qvariant_qquaternion.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#[cxx::bridge] +pub mod ffi { + unsafe extern "C++" { + include!("cxx-qt-lib/qquaternion.h"); + type QQuaternion = crate::QQuaternion; + + include!("cxx-qt-lib/qvariant.h"); + type QVariant = crate::QVariant; + } + + #[namespace = "rust::cxxqtlib1::qvariant"] + unsafe extern "C++" { + #[rust_name = "can_convert_QQuaternion"] + fn qvariantCanConvertQQuaternion(variant: &QVariant) -> bool; + #[rust_name = "construct_QQuaternion"] + fn qvariantConstruct(value: &QQuaternion) -> QVariant; + #[rust_name = "value_or_default_QQuaternion"] + fn qvariantValueOrDefault(variant: &QVariant) -> QQuaternion; + } +} + +pub(crate) fn can_convert(variant: &ffi::QVariant) -> bool { + ffi::can_convert_QQuaternion(variant) +} + +pub(crate) fn construct(value: &ffi::QQuaternion) -> ffi::QVariant { + ffi::construct_QQuaternion(value) +} + +pub(crate) fn value_or_default(variant: &ffi::QVariant) -> ffi::QQuaternion { + ffi::value_or_default_QQuaternion(variant) +} diff --git a/crates/cxx-qt-lib/src/gui/mod.rs b/crates/cxx-qt-lib/src/gui/mod.rs index 5d05db328..ead19616e 100644 --- a/crates/cxx-qt-lib/src/gui/mod.rs +++ b/crates/cxx-qt-lib/src/gui/mod.rs @@ -6,9 +6,18 @@ mod qcolor; pub use qcolor::{QColor, QColorNameFormat, QColorSpec}; +mod qgenericmatrix; +pub use qgenericmatrix::{ + QGenericMatrix, QMatrix2x2, QMatrix2x3, QMatrix2x4, QMatrix3x2, QMatrix3x3, QMatrix3x4, + QMatrix4x2, QMatrix4x3, +}; + mod qguiapplication; pub use qguiapplication::QGuiApplication; +mod qquaternion; +pub use qquaternion::QQuaternion; + mod qvector2d; pub use qvector2d::QVector2D; diff --git a/crates/cxx-qt-lib/src/gui/qgenericmatrix.rs b/crates/cxx-qt-lib/src/gui/qgenericmatrix.rs new file mode 100644 index 000000000..88b2165cf --- /dev/null +++ b/crates/cxx-qt-lib/src/gui/qgenericmatrix.rs @@ -0,0 +1,358 @@ +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Joshua Booth +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::slice; + +use cxx::{type_id, ExternType}; + +/// The QGenericMatrix class represents a quaternion consisting of a vector and scalar. +/// +/// Qt Documentation: [QGenericMatrix](https://doc.qt.io/qt/qgenericmatrix.html#details) +#[repr(transparent)] +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] +pub struct QGenericMatrix { + data: [[f32; M]; N], +} + +impl QGenericMatrix { + /// Constructs a matrix from floating-point `values` in row-major order. + pub const fn new(values: &[[f32; N]; M]) -> Self { + let mut data = [[0.0; M]; N]; + let mut col = 0; + while col < N { + let mut row = 0; + while row < M { + data[col][row] = values[row][col]; + row += 1; + } + col += 1; + } + Self { data } + } + + /// Returns a reference to the raw data of this matrix. + pub const fn data(&self) -> &[f32] { + // TODO: Replace with `array::as_flattened` once MSRV is 1.80.0. + unsafe { slice::from_raw_parts(self.data.as_ptr().cast(), N * M) } + } + + /// Returns a mutable reference to the raw data of this matrix. + pub fn data_mut(&mut self) -> &mut [f32] { + // TODO: Replace with `array::as_flattened_mut` once MSRV is 1.80.0. + unsafe { slice::from_raw_parts_mut(self.data.as_mut_ptr().cast(), N * M) } + } + + /// Retrieves the N * M items in this matrix and copies them to `values` in row-major order. + pub fn copy_data_to(&self, values: &mut [f32]) { + for (col, data) in self.data.iter().enumerate() { + for (row, &value) in data.iter().enumerate() { + values[row * N + col] = value; + } + } + } + + /// Fills all elements of this matrix with `value`. + pub fn fill(&mut self, value: f32) { + self.data_mut().fill(value); + } + + /// Constructs a matrix with all values set to `value`. + pub const fn filled(value: f32) -> Self { + Self { + data: [[value; M]; N], + } + } + + /// Constructs a NxM identity matrix. + pub const fn identity() -> Self { + let mut data = [[0.0; M]; N]; + let mut i = 0; + let size = if M < N { M } else { N }; + while i < size { + data[i][i] = 1.0; + i += 1; + } + Self { data } + } + + /// Returns `true` if this matrix is the identity; `false` otherwise. + pub fn is_identity(&self) -> bool { + for (col, data) in self.data.iter().enumerate() { + for (row, &value) in data.iter().enumerate() { + if value != (if row == col { 1.0 } else { 0.0 }) { + return false; + } + } + } + true + } + + /// Constructs a two-dimensional array from the matrix in row-major order. + pub const fn rows(&self) -> [[f32; N]; M] { + self.transposed().data + } + + /// Sets this matrix to the identity. + pub fn set_to_identity(&mut self) { + for (col, data) in self.data.iter_mut().enumerate() { + for (row, value) in data.iter_mut().enumerate() { + *value = if row == col { 1.0 } else { 0.0 }; + } + } + } + + /// Returns this matrix, transposed about its diagonal. + pub const fn transposed(&self) -> QGenericMatrix { + let mut transposed = [[0.0; N]; M]; + let mut col = 0; + while col < N { + let mut row = 0; + while row < M { + transposed[row][col] = self.data[col][row]; + row += 1; + } + col += 1; + } + QGenericMatrix { data: transposed } + } +} + +impl Default for QGenericMatrix { + /// Constructs a NxM identity matrix. + fn default() -> Self { + Self::identity() + } +} + +impl std::ops::Index<(usize, usize)> for QGenericMatrix { + type Output = f32; + + /// Returns a reference to the element at position (row, column) in this matrix. + fn index(&self, (row, column): (usize, usize)) -> &Self::Output { + &self.data[column][row] + } +} + +impl std::ops::IndexMut<(usize, usize)> for QGenericMatrix { + /// Returns a mutable reference to the element at position (row, column) in this matrix. + fn index_mut(&mut self, (row, column): (usize, usize)) -> &mut Self::Output { + &mut self.data[column][row] + } +} + +impl std::ops::AddAssign for QGenericMatrix { + fn add_assign(&mut self, rhs: Self) { + for (lhs, &rhs) in self.data_mut().iter_mut().zip(rhs.data()) { + *lhs += rhs; + } + } +} + +impl std::ops::Add for QGenericMatrix { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + self += rhs; + self + } +} + +impl std::ops::SubAssign for QGenericMatrix { + fn sub_assign(&mut self, rhs: Self) { + for (lhs, &rhs) in self.data_mut().iter_mut().zip(rhs.data()) { + *lhs -= rhs; + } + } +} + +impl std::ops::Sub for QGenericMatrix { + type Output = Self; + + fn sub(mut self, rhs: Self) -> Self::Output { + self -= rhs; + self + } +} + +impl std::ops::MulAssign for QGenericMatrix { + fn mul_assign(&mut self, rhs: f32) { + for value in self.data_mut() { + *value *= rhs; + } + } +} + +impl std::ops::Mul for QGenericMatrix { + type Output = Self; + + fn mul(mut self, rhs: f32) -> Self::Output { + self *= rhs; + self + } +} + +impl std::ops::DivAssign for QGenericMatrix { + fn div_assign(&mut self, rhs: f32) { + for value in self.data_mut() { + *value /= rhs; + } + } +} + +impl std::ops::Div for QGenericMatrix { + type Output = Self; + + fn div(mut self, rhs: f32) -> Self::Output { + self /= rhs; + self + } +} + +impl std::ops::Neg for QGenericMatrix { + type Output = Self; + + fn neg(mut self) -> Self::Output { + for value in self.data_mut() { + *value = -*value; + } + self + } +} + +impl From<&[f32]> for QGenericMatrix { + /// Constructs a matrix from the given N * M floating-point `values`. The contents of the array `values` is assumed to be in row-major order. + fn from(values: &[f32]) -> Self { + let mut matrix = [[0.0; M]; N]; + for (col, data) in matrix.iter_mut().enumerate() { + for (row, value) in data.iter_mut().enumerate() { + *value = values[row * N + col]; + } + } + Self { data: matrix } + } +} + +impl From<&[[f32; N]; M]> for QGenericMatrix { + /// Constructs a matrix from the given N * M floating-point `values` in row-major order. + fn from(values: &[[f32; N]; M]) -> Self { + Self::new(values) + } +} + +impl From<&QGenericMatrix> for [[f32; N]; M] { + /// Constructs a two-dimensional array from the matrix in row-major order. + fn from(value: &QGenericMatrix) -> Self { + value.rows() + } +} + +macro_rules! impl_matrix { + ($i:ident, $id:literal, $n:literal, $m:literal) => { + pub type $i = QGenericMatrix<$n, $m>; + // Safety: + // + // Static checks on the C++ side. + unsafe impl ExternType for $i { + type Id = type_id!($id); + type Kind = cxx::kind::Trivial; + } + }; +} + +impl_matrix!(QMatrix2x2, "QMatrix2x2", 2, 2); +impl_matrix!(QMatrix2x3, "QMatrix2x3", 2, 3); +impl_matrix!(QMatrix2x4, "QMatrix2x4", 2, 4); +impl_matrix!(QMatrix3x2, "QMatrix3x2", 3, 2); +impl_matrix!(QMatrix3x3, "QMatrix3x3", 3, 3); +impl_matrix!(QMatrix3x4, "QMatrix3x4", 3, 4); +impl_matrix!(QMatrix4x2, "QMatrix4x2", 4, 2); +impl_matrix!(QMatrix4x3, "QMatrix4x3", 4, 3); + +#[cfg(test)] +mod test { + use super::*; + + #[rustfmt::skip] + const MATRIX: &QGenericMatrix<4, 2> = &QGenericMatrix::new(&[ + [5.0, 4.0, 3.0, 2.0], + [6.0, 7.0, 8.0, 9.0], + ]); + + #[test] + fn index() { + assert_eq!(MATRIX[(1, 2)], 8.0); + } + + #[test] + fn data() { + assert_eq!(MATRIX.data(), [5.0, 6.0, 4.0, 7.0, 3.0, 8.0, 2.0, 9.0]); + } + + #[test] + fn copy_data_to() { + let mut dest = [0.0; 8]; + MATRIX.copy_data_to(&mut dest); + assert_eq!(dest, [5.0, 4.0, 3.0, 2.0, 6.0, 7.0, 8.0, 9.0]); + } + + #[test] + fn fill() { + let mut filled = *MATRIX; + filled.fill(11.0); + assert_eq!(filled.data(), [11.0; 8]); + } + + #[test] + fn filled() { + let filled = QGenericMatrix::<4, 2>::filled(11.0); + assert_eq!(filled.data(), [11.0; 8]); + } + + #[test] + fn identity() { + let matrix = QGenericMatrix::<4, 2>::identity(); + assert_eq!(matrix.rows(), [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]]); + } + + #[test] + fn is_identity() { + let matrix = QGenericMatrix::new(&[ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + ]); + assert!(matrix.is_identity()); + } + + #[test] + fn is_not_identity() { + let matrix = QGenericMatrix::new(&[ + [1.0, 1.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + ]); + assert!(!matrix.is_identity()); + } + + #[test] + fn rows() { + let rows = [[5.0, 4.0, 3.0, 2.0], [6.0, 7.0, 8.0, 9.0]]; + let matrix = QGenericMatrix::new(&rows); + assert_eq!(matrix.rows(), rows); + } + + #[test] + fn set_to_identity() { + let mut matrix = *MATRIX; + matrix.set_to_identity(); + assert_eq!(matrix, QGenericMatrix::identity()); + } + + #[test] + fn transposed() { + let rows = MATRIX.transposed().rows(); + assert_eq!(rows, [[5.0, 6.0], [4.0, 7.0], [3.0, 8.0], [2.0, 9.0]]); + } +} diff --git a/crates/cxx-qt-lib/src/gui/qquaternion.cpp b/crates/cxx-qt-lib/src/gui/qquaternion.cpp new file mode 100644 index 000000000..d751a741c --- /dev/null +++ b/crates/cxx-qt-lib/src/gui/qquaternion.cpp @@ -0,0 +1,20 @@ +// clang-format off +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// clang-format on +// SPDX-FileContributor: Joshua Booth +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#include "cxx-qt-lib/qquaternion.h" + +#include + +assert_alignment_and_size(QQuaternion, { + float wp; + float xp; + float yp; + float zp; +}); + +static_assert(::std::is_trivially_copyable::value, + "QQuaternion should be trivially copyable"); diff --git a/crates/cxx-qt-lib/src/gui/qquaternion.rs b/crates/cxx-qt-lib/src/gui/qquaternion.rs new file mode 100644 index 000000000..bbe9bf237 --- /dev/null +++ b/crates/cxx-qt-lib/src/gui/qquaternion.rs @@ -0,0 +1,424 @@ +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Joshua Booth +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::fmt; +use std::mem::MaybeUninit; + +use cxx::{type_id, ExternType}; + +use crate::{QMatrix3x3, QVector3D, QVector4D}; + +#[cxx::bridge] +mod ffi { + extern "C++" { + include!("cxx-qt-lib/qstring.h"); + type QString = crate::QString; + + include!("cxx-qt-lib/qgenericmatrix.h"); + type QMatrix3x3 = crate::QMatrix3x3; + include!("cxx-qt-lib/qvector3d.h"); + type QVector3D = crate::QVector3D; + include!("cxx-qt-lib/qvector4d.h"); + type QVector4D = crate::QVector4D; + } + + unsafe extern "C++" { + include!("cxx-qt-lib/qquaternion.h"); + type QQuaternion = super::QQuaternion; + + /// Returns the conjugate of this quaternion, which is (-x, -y, -z, scalar). + fn conjugated(&self) -> QQuaternion; + + #[doc(hidden)] + #[rust_name = "get_axes_raw"] + unsafe fn getAxes( + &self, + x_axis: *mut QVector3D, + y_axis: *mut QVector3D, + z_axis: *mut QVector3D, + ); + + #[doc(hidden)] + #[rust_name = "get_axis_and_angle_raw"] + unsafe fn getAxisAndAngle(&self, x: *mut f32, y: *mut f32, z: *mut f32, angle: *mut f32); + + #[doc(hidden)] + #[rust_name = "get_euler_angles_raw"] + unsafe fn getEulerAngles(&self, pitch: *mut f32, yaw: *mut f32, roll: *mut f32); + + /// Returns the inverse of this quaternion. If this quaternion is null, then a null quaternion is returned. + fn inverted(&self) -> QQuaternion; + + /// Returns `true` if the x, y, and z components of this quaternion are set to 0.0, and the scalar component is set to 1.0; otherwise returns `false`. + #[rust_name = "is_identity"] + fn isIdentity(&self) -> bool; + + /// Returns `true` if the x, y, z, and scalar components of this quaternion are set to 0.0; otherwise returns `false`. + #[rust_name = "is_null"] + fn isNull(&self) -> bool; + + /// Returns the length of the quaternion. This is also called the "norm". + fn length(&self) -> f32; + + /// Returns the squared length of the quaternion. + /// + /// **Note:** Though cheap to compute, this is susceptible to overflow and underflow that [`length`](Self::length) avoids in many cases. + #[rust_name = "length_squared"] + fn lengthSquared(&self) -> f32; + + /// Normalizes the current quaternion in place. Nothing happens if this is a null quaternion or the length of the quaternion is very close to 1. + fn normalize(&mut self); + + /// Returns the normalized unit form of this quaternion. + /// + /// If this quaternion is null, then a null quaternion is returned. If the length of the quaternion is very close to 1, then the quaternion will be returned as-is. Otherwise the normalized form of the quaternion of length 1 will be returned. + fn normalized(&self) -> QQuaternion; + + /// Rotates `vector` with this quaternion to produce a new vector in 3D space. + #[rust_name = "rotated_vector"] + fn rotatedVector(&self, vector: &QVector3D) -> QVector3D; + + /// Returns the scalar component of this quaternion. + fn scalar(&self) -> f32; + + /// Sets the scalar component of this quaternion to `scalar`. + #[rust_name = "set_scalar"] + fn setScalar(&mut self, scalar: f32); + + /// Sets the vector component of this quaternion to `vector`. + #[rust_name = "set_vector"] + fn setVector(&mut self, vector: &QVector3D); + + /// Sets the x coordinate of this quaternion's vector to the given `x` coordinate. + #[rust_name = "set_x"] + fn setX(&mut self, x: f32); + + /// Sets the y coordinate of this quaternion's vector to the given `y` coordinate. + #[rust_name = "set_y"] + fn setY(&mut self, y: f32); + + /// Sets the z coordinate of this quaternion's vector to the given `z` coordinate. + #[rust_name = "set_z"] + fn setZ(&mut self, z: f32); + + /// Calculates roll, pitch, and yaw Euler angles (in degrees) that corresponds to this quaternion. + #[rust_name = "to_euler_angles"] + fn toEulerAngles(&self) -> QVector3D; + + /// Creates a rotation matrix that corresponds to this quaternion. + /// + /// **Note:** If this quaternion is not normalized, the resulting rotation matrix will contain scaling information. + #[rust_name = "to_rotation_matrix"] + fn toRotationMatrix(&self) -> QMatrix3x3; + + #[doc(hidden)] + #[rust_name = "to_vector_4d"] + fn toVector4D(&self) -> QVector4D; + + /// Returns the vector component of this quaternion. + fn vector(&self) -> QVector3D; + + /// Returns the x coordinate of this quaternion's vector. + fn x(&self) -> f32; + + /// Returns the y coordinate of this quaternion's vector. + fn y(&self) -> f32; + + /// Returns the z coordinate of this quaternion's vector. + fn z(&self) -> f32; + } + + #[namespace = "rust::cxxqtlib1"] + unsafe extern "C++" { + #[doc(hidden)] + #[rust_name = "qquaternion_dot_product"] + fn qquaternionDotProduct(q1: &QQuaternion, q2: &QQuaternion) -> f32; + + #[doc(hidden)] + #[rust_name = "qquaternion_from_axes"] + fn qquaternionFromAxes( + x_axis: &QVector3D, + y_axis: &QVector3D, + z_axis: &QVector3D, + ) -> QQuaternion; + #[doc(hidden)] + #[rust_name = "qquaternion_from_axis_and_angle"] + fn qquaternionFromAxisAndAngle(x: f32, y: f32, z: f32, angle: f32) -> QQuaternion; + #[doc(hidden)] + #[rust_name = "qquaternion_from_euler_angles"] + fn qquaternionFromEulerAngles(pitch: f32, yaw: f32, roll: f32) -> QQuaternion; + #[doc(hidden)] + #[rust_name = "qquaternion_from_rotation_matrix"] + fn qquaternionFromRotationMatrix(rot3x3: &QMatrix3x3) -> QQuaternion; + + #[doc(hidden)] + #[rust_name = "qquaternion_nlerp"] + fn qquaternionNlerp(q1: &QQuaternion, q2: &QQuaternion, t: f32) -> QQuaternion; + #[doc(hidden)] + #[rust_name = "qquaternion_slerp"] + fn qquaternionSlerp(q1: &QQuaternion, q2: &QQuaternion, t: f32) -> QQuaternion; + + #[doc(hidden)] + #[rust_name = "qquaternion_rotation_to"] + fn qquaternionRotationTo(from: &QVector3D, to: &QVector3D) -> QQuaternion; + } + + #[namespace = "rust::cxxqtlib1"] + unsafe extern "C++" { + include!("cxx-qt-lib/common.h"); + + #[doc(hidden)] + #[rust_name = "qquaternion_init_qvector4d"] + fn construct(vector: &QVector4D) -> QQuaternion; + #[doc(hidden)] + #[rust_name = "qquaternion_init_float_qvector3d"] + fn construct(scalar: f32, vector: &QVector3D) -> QQuaternion; + + #[doc(hidden)] + #[rust_name = "qquaternion_init_default"] + fn construct() -> QQuaternion; + + #[doc(hidden)] + #[rust_name = "qquaternion_to_debug_qstring"] + fn toDebugQString(value: &QQuaternion) -> QString; + #[doc(hidden)] + #[rust_name = "qquaternion_plus"] + fn operatorPlus(a: &QQuaternion, b: &QQuaternion) -> QQuaternion; + #[doc(hidden)] + #[rust_name = "qquaternion_minus"] + fn operatorMinus(a: &QQuaternion, b: &QQuaternion) -> QQuaternion; + #[doc(hidden)] + #[rust_name = "qquaternion_mul"] + fn operatorMul(a: f32, b: &QQuaternion) -> QQuaternion; + #[doc(hidden)] + #[rust_name = "qquaternion_div"] + fn operatorDiv(a: f32, b: &QQuaternion) -> QQuaternion; + #[doc(hidden)] + #[rust_name = "qquaternion_neg"] + fn operatorNeg(a: &QQuaternion) -> QQuaternion; + } +} + +/// The QQuaternion class represents a quaternion consisting of a vector and scalar. +/// +/// Qt Documentation: [QQuaternion](https://doc.qt.io/qt/qquaternion.html#details) +#[derive(Debug, Clone, PartialEq)] +#[repr(C)] +pub struct QQuaternion { + wp: f32, + xp: f32, + yp: f32, + zp: f32, +} + +impl QQuaternion { + /// Returns the dot product of `q1` and `q2`. + pub fn dot_product(q1: &QQuaternion, q2: &QQuaternion) -> f32 { + ffi::qquaternion_dot_product(q1, q2) + } + + /// Interpolates along the shortest linear path between the rotational positions `q1` and `q2`. The value `t` should be between 0 and 1, indicating the distance to travel between `q1` and `q2`. The result will be [`normalized`](Self::normalized). + /// + /// If `t` is less than or equal to 0, then `q1` will be returned. If `t` is greater than or equal to 1, then `q2` will be returned. + /// + /// This function is typically faster than [`slerp`](Self::slerp) and will give approximate results to spherical interpolation that are good enough for some applications. + pub fn nlerp(q1: &Self, q2: &Self, t: f32) -> Self { + ffi::qquaternion_nlerp(q1, q2, t) + } + + /// Returns the shortest arc quaternion to rotate from the direction described by the vector `from` to the direction described by the vector `to`. + pub fn rotation_to(from: &QVector3D, to: &QVector3D) -> Self { + ffi::qquaternion_rotation_to(from, to) + } + + /// Interpolates along the shortest linear path between the rotational positions `q1` and `q2`. The value `t` should be between 0 and 1, indicating the distance to travel between `q1` and `q2`. The result will be [`normalized`](Self::normalized). + /// + /// If `t` is less than or equal to 0, then `q1` will be returned. If `t` is greater than or equal to 1, then `q2` will be returned. + pub fn slerp(q1: &Self, q2: &Self, t: f32) -> Self { + ffi::qquaternion_slerp(q1, q2, t) + } + + /// Constructs a quaternion vector from the specified `vector` and `scalar`. + pub fn new(scalar: f32, vector: &QVector3D) -> Self { + ffi::qquaternion_init_float_qvector3d(scalar, vector) + } + + /// Constructs the quaternion using 3 axes (`x_axis`, `y_axis`, `z_axis`). + /// + /// **Note:** The axes are assumed to be orthonormal. + pub fn from_axes(x_axis: &QVector3D, y_axis: &QVector3D, z_axis: &QVector3D) -> Self { + ffi::qquaternion_from_axes(x_axis, y_axis, z_axis) + } + + /// Creates a normalized quaternion that corresponds to rotating through `angle` degrees about the specified 3D `axis`. + pub fn from_axis_and_angle(x: f32, y: f32, z: f32, angle: f32) -> Self { + ffi::qquaternion_from_axis_and_angle(x, y, z, angle) + } + + /// Creates a quaternion that corresponds to a rotation of `roll` degrees around the z axis, `pitch` degrees around the x axis, and `yaw` degrees around the y axis (in that order). + pub fn from_euler_angles(pitch: f32, yaw: f32, roll: f32) -> Self { + ffi::qquaternion_from_euler_angles(pitch, yaw, roll) + } + + /// Creates a quaternion that corresponds to a rotation matrix `rot3x3`. + /// + /// **Note:** If a given rotation matrix is not normalized, the resulting quaternion will contain scaling information. + pub fn from_rotation_matrix(rot3x3: &QMatrix3x3) -> Self { + ffi::qquaternion_from_rotation_matrix(rot3x3) + } + + /// Returns the 3 orthonormal axes (`x_axis`, `y_axis`, `z_axis`) defining the quaternion. + pub fn get_axes(&self) -> (QVector3D, QVector3D, QVector3D) { + let mut x = MaybeUninit::uninit(); + let mut y = MaybeUninit::uninit(); + let mut z = MaybeUninit::uninit(); + unsafe { + // SAFETY: All pointers are valid. + self.get_axes_raw(x.as_mut_ptr(), y.as_mut_ptr(), z.as_mut_ptr()); + // SAFETY: Qt has initialized all values. + (x.assume_init(), y.assume_init(), z.assume_init()) + } + } + + /// Extracts a 3D axis and a rotating angle (in degrees) (`x`, `y`, `z`, `angle`) that corresponds to this quaternion. + pub fn get_axis_and_angle(&self) -> (f32, f32, f32, f32) { + let mut x = MaybeUninit::uninit(); + let mut y = MaybeUninit::uninit(); + let mut z = MaybeUninit::uninit(); + let mut angle = MaybeUninit::uninit(); + unsafe { + // SAFETY: All pointers are valid. + self.get_axis_and_angle_raw( + x.as_mut_ptr(), + y.as_mut_ptr(), + z.as_mut_ptr(), + angle.as_mut_ptr(), + ); + // SAFETY: Qt has initialized all values. + ( + x.assume_init(), + y.assume_init(), + z.assume_init(), + angle.assume_init(), + ) + } + } + + /// Calculates (`pitch`, `yaw`, `roll`) Euler angles (in degrees) that corresponds to this quaternion. + pub fn get_euler_angles(&self) -> (f32, f32, f32) { + let mut pitch = MaybeUninit::uninit(); + let mut yaw = MaybeUninit::uninit(); + let mut roll = MaybeUninit::uninit(); + unsafe { + // SAFETY: All pointers are valid. + self.get_euler_angles_raw(pitch.as_mut_ptr(), yaw.as_mut_ptr(), roll.as_mut_ptr()); + // SAFETY: Qt has initialized all values. + (pitch.assume_init(), yaw.assume_init(), roll.assume_init()) + } + } +} + +impl Default for QQuaternion { + /// Constructs an identity quaternion (1, 0, 0, 0), i.e. with the vector (0, 0, 0) and scalar 1. + fn default() -> Self { + ffi::qquaternion_init_default() + } +} + +impl fmt::Display for QQuaternion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + ffi::qquaternion_to_debug_qstring(self).fmt(f) + } +} + +impl std::ops::Add for QQuaternion { + type Output = Self; + fn add(self, other: Self) -> Self { + ffi::qquaternion_plus(&self, &other) + } +} + +impl std::ops::Sub for QQuaternion { + type Output = Self; + fn sub(self, other: Self) -> Self { + ffi::qquaternion_minus(&self, &other) + } +} + +impl std::ops::Mul for QQuaternion { + type Output = Self; + fn mul(self, rhs: f32) -> Self { + ffi::qquaternion_mul(rhs, &self) + } +} + +impl std::ops::Div for QQuaternion { + type Output = Self; + fn div(self, rhs: f32) -> Self { + ffi::qquaternion_div(rhs, &self) + } +} + +impl std::ops::Neg for QQuaternion { + type Output = Self; + + fn neg(self) -> Self::Output { + ffi::qquaternion_neg(&self) + } +} + +impl From<&QVector4D> for QQuaternion { + /// Constructs a quaternion from the components of vector. + fn from(value: &QVector4D) -> Self { + ffi::qquaternion_init_qvector4d(value) + } +} + +// Safety: +// +// Static checks on the C++ side ensure that QQuaternion is trivial. +unsafe impl ExternType for QQuaternion { + type Id = type_id!("QQuaternion"); + type Kind = cxx::kind::Trivial; +} + +#[cfg(test)] +mod test { + use super::*; + + fn round(n: f32) -> i32 { + n.round() as i32 + } + + #[test] + fn get_axes() { + let axis1 = QVector3D::new(1.0, 0.0, 0.0); + let axis2 = QVector3D::new(0.0, 1.0, 0.0); + let axis3 = QVector3D::new(0.0, 0.0, 1.0); + let qq = QQuaternion::from_axes(&axis1, &axis2, &axis3); + assert_eq!(qq.get_axes(), (axis1, axis2, axis3)); + } + + #[test] + fn get_axis_and_angle() { + let qq = QQuaternion::from_axis_and_angle(1.0, 0.0, 0.0, 40.0); + let (a, b, c, d) = qq.get_axis_and_angle(); + assert_eq!((round(a), round(b), round(c), round(d)), (1, 0, 0, 40)); + } + + #[test] + fn get_euler_angles() { + let qq = QQuaternion::from_euler_angles(10.0, 20.0, 30.0); + let (a, b, c) = qq.get_euler_angles(); + assert_eq!((round(a), round(b), round(c)), (10, 20, 30)); + } + + #[test] + fn to_rotation_matrix() { + let qq = QQuaternion::from_axis_and_angle(1.0, 0.0, 0.0, 40.0); + let matrix = qq.to_rotation_matrix(); + assert_eq!(QQuaternion::from_rotation_matrix(&matrix), qq); + } +}