3 Commits

Author SHA1 Message Date
7fbd4c0226 Fix CI. (#22) 2026-02-15 01:41:27 +00:00
ed604eb655 ToolBar buttons to add and remove objects. (#18) 2025-04-12 16:53:40 +00:00
f40366cb8d ToolBox widget cleanup. (#17) 2025-03-23 22:28:41 +00:00
13 changed files with 220 additions and 36 deletions

View File

@@ -43,13 +43,16 @@ jobs:
with: with:
version: ${{ env.QT_VERSION }} version: ${{ env.QT_VERSION }}
# Windows - name: Install pkgconfiglite
- name: Chocolatey Action
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
uses: crazy-max/ghaction-chocolatey@v2 uses: crazy-max/ghaction-chocolatey@v2
with: with:
args: install pkgconfiglite --checksum e87b5ea3c9142256af60f2d5b917aa63b571e6a0 --checksum-type sha1 args: install pkgconfiglite --checksum e87b5ea3c9142256af60f2d5b917aa63b571e6a0 --checksum-type sha1
- name: Install nsis
if: matrix.os == 'windows-latest'
uses: crazy-max/ghaction-chocolatey@v2
with:
args: install nsis
- name: Install Debian packaging dependencies - name: Install Debian packaging dependencies
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
@@ -191,6 +194,11 @@ jobs:
uses: crazy-max/ghaction-chocolatey@v2 uses: crazy-max/ghaction-chocolatey@v2
with: with:
args: install pkgconfiglite --checksum e87b5ea3c9142256af60f2d5b917aa63b571e6a0 --checksum-type sha1 args: install pkgconfiglite --checksum e87b5ea3c9142256af60f2d5b917aa63b571e6a0 --checksum-type sha1
- name: Install nsis
if: matrix.os == 'windows-latest'
uses: crazy-max/ghaction-chocolatey@v2
with:
args: install nsis
- name: Configure Qtk Library - name: Configure Qtk Library
shell: bash shell: bash

View File

@@ -39,7 +39,7 @@ jobs:
# Check the entire repo for source files to tidy # Check the entire repo for source files to tidy
files-changed-only: false files-changed-only: false
# Ignore qtk build and external assimp directories # Ignore qtk build and external assimp directories
ignore: '.github|build|extern/assimp/assimp' ignore: '.github|build|extern'
# Point to compile_commands.json produced by build # Point to compile_commands.json produced by build
database: 'build' database: 'build'
# Use thread comments as feedback # Use thread comments as feedback
@@ -61,8 +61,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: clang-format Check - uses: cpp-linter/cpp-linter-action@v2
uses: jidicula/clang-format-action@v4.9.0
with: with:
clang-format-version: '18' version: '18'
check-path: ${{ matrix.path }} style: 'file'
tidy-checks: ''
files-changed-only: false
ignore: '.github|build|extern'
extensions: 'cpp,h'
files: ${{ matrix.path }}

View File

@@ -157,8 +157,15 @@ list(APPEND VAR_NAMES QT6_INSTALL_PLUGINS)
# Find Assimp. # Find Assimp.
if(QTK_SUBMODULES) if(QTK_SUBMODULES)
if(APPLE)
# Avoid zlib redefining fdopen, causing build failures in apple clang.
# https://github.com/assimp/assimp/issues/6118
add_compile_definitions(-Dfdopen=fdopen)
endif()
if(NOT WIN32)
# Required to statically link. # Required to statically link.
add_compile_options(-fPIC) add_compile_options(-fPIC)
endif()
set(BUILD_SHARED_LIBS OFF CACHE STRING "Build static assimp libs" FORCE) set(BUILD_SHARED_LIBS OFF CACHE STRING "Build static assimp libs" FORCE)
set(ASSIMP_BUILD_ZLIB ON CACHE STRING "Build Zlib with assimp." FORCE) set(ASSIMP_BUILD_ZLIB ON CACHE STRING "Build Zlib with assimp." FORCE)
set( set(
@@ -175,6 +182,14 @@ if(QTK_SUBMODULES)
"${CMAKE_CURRENT_SOURCE_DIR}/extern/assimp/assimp/" "${CMAKE_CURRENT_SOURCE_DIR}/extern/assimp/assimp/"
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
install(
TARGETS assimp zlibstatic
EXPORT qtk_export
COMPONENT qtk
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
else() else()
find_package(assimp REQUIRED) find_package(assimp REQUIRED)
endif() endif()

View File

@@ -90,7 +90,7 @@ The Ubuntu apt repositories contain all the packages we need to build all target
To build Qtk desktop application with the scene in the screenshots below run the following commands. To build Qtk desktop application with the scene in the screenshots below run the following commands.
```bash ```bash
sudo apt update && sudo apt install cmake build-essential git ccache libxkbcommon-dev libassimp-dev qt6-base-dev qt6-tools-dev sudo apt update && sudo apt install cmake build-essential git ccache libxkbcommon-dev libassimp-dev qt6-base-dev qt6-tools-dev zlib1g-dev
cmake -DQTK_GUI_SCENE=ON -B build cmake -DQTK_GUI_SCENE=ON -B build
cmake --build build cmake --build build
./build/bin/qtk_gui ./build/bin/qtk_gui

View File

@@ -48,6 +48,16 @@ MainWindow::MainWindow(QWidget * parent) : QMainWindow(parent)
&Qtk::ToolBox::updateFocus); &Qtk::ToolBox::updateFocus);
} }
connect(ui_->actionDelete_Object,
&QAction::triggered,
this,
&MainWindow::deleteObject);
connect(ui_->actionLoad_Model,
&QAction::triggered,
this,
&MainWindow::loadObject);
// TODO: Fix / use MainWindow in Qt Designer to add these dock widgets. // TODO: Fix / use MainWindow in Qt Designer to add these dock widgets.
// For now we will add them manually, but we should be able to do this in the // For now we will add them manually, but we should be able to do this in the
// designer. At the moment if you edit the UI in designer the dock widget // designer. At the moment if you edit the UI in designer the dock widget
@@ -104,6 +114,26 @@ void MainWindow::refreshScene(const QString & sceneName)
ui_->qtk__TreeView->updateView(getQtkWidget()->getScene()); ui_->qtk__TreeView->updateView(getQtkWidget()->getScene());
} }
void MainWindow::deleteObject()
{
if (auto object = ui_->qtk__ToolBox->getObjectFocus(); object != Q_NULLPTR) {
auto scene = getQtkWidget()->getScene();
switch (object->getType()) {
case Qtk::Object::Type::QTK_MESH:
scene->removeObject(dynamic_cast<Qtk::MeshRenderer *>(object));
ui_->qtk__ToolBox->clearFocus();
break;
case Qtk::Object::Type::QTK_MODEL:
scene->removeObject(dynamic_cast<Qtk::Model *>(object));
ui_->qtk__ToolBox->clearFocus();
break;
default:
qDebug() << "Failed to delete model with invalid type";
break;
}
}
}
void MainWindow::setScene(Qtk::Scene * scene) void MainWindow::setScene(Qtk::Scene * scene)
{ {
connect(scene, connect(scene,

View File

@@ -11,8 +11,8 @@
#include <unordered_map> #include <unordered_map>
#include <QFileDialog>
#include <QMainWindow> #include <QMainWindow>
#include <QPlainTextEdit>
#include "designer-plugins/debugconsole.h" #include "designer-plugins/debugconsole.h"
@@ -144,6 +144,23 @@ class MainWindow : public QMainWindow
*/ */
void refreshScene(const QString & sceneName); void refreshScene(const QString & sceneName);
/**
* Opens a QFileDialog for selecting an object file to load into the scene.
*/
void loadObject()
{
const QUrl file = QFileDialog::getOpenFileName(
this, tr("Load Model"), QDir::homePath(), tr("Object Files (*.obj)"));
getQtkWidget()->getScene()->loadModel(file.fileName().replace(".obj", ""),
file.toString());
}
/**
* Deletes the currently selected object from the scene.
*/
void deleteObject();
private: private:
/*************************************************************************** /***************************************************************************
* Private Members * Private Members

View File

@@ -223,7 +223,7 @@
</action> </action>
<action name="actionLoad_Model"> <action name="actionLoad_Model">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>true</bool>
</property> </property>
<property name="icon"> <property name="icon">
<iconset> <iconset>
@@ -238,7 +238,7 @@
</action> </action>
<action name="actionDelete_Object"> <action name="actionDelete_Object">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>true</bool>
</property> </property>
<property name="icon"> <property name="icon">
<iconset> <iconset>

View File

@@ -20,7 +20,7 @@ ToolBox::ToolBox(QWidget * parent) :
QDockWidget(parent), objectDetails_(this), transformPanel_(this), QDockWidget(parent), objectDetails_(this), transformPanel_(this),
scalePanel_(this), vertex_(this, "Vertex Shader:"), scalePanel_(this), vertex_(this, "Vertex Shader:"),
fragment_(this, "Fragment Shader:"), properiesForm_(new QFormLayout), fragment_(this, "Fragment Shader:"), properiesForm_(new QFormLayout),
shaderForm_(new QFormLayout), ui(new Ui::ToolBox) shaderForm_(new QFormLayout), ui(new Ui::ToolBox), objectFocus_(Q_NULLPTR)
{ {
ui->setupUi(this); ui->setupUi(this);
setMinimumWidth(350); setMinimumWidth(350);
@@ -44,10 +44,24 @@ void ToolBox::updateFocus(const QString & name)
{ {
auto object = auto object =
QtkWidget::mWidgetManager.get_widget()->getScene()->getObject(name); QtkWidget::mWidgetManager.get_widget()->getScene()->getObject(name);
if (object != Q_NULLPTR) { // If we can't find the object show a warning.
if (object == Q_NULLPTR) {
qDebug() << "Failed to find selected object: " << name
<< "; Clearing object panels.";
}
// We should still pass the nullptr here if we failed to find the object
// above.
objectFocus_ = object;
refreshProperties(object); refreshProperties(object);
refreshShaders(object); refreshShaders(object);
} }
void ToolBox::clearFocus()
{
objectFocus_ = Q_NULLPTR;
refreshProperties(objectFocus_);
refreshShaders(objectFocus_);
} }
ToolBox::SpinBox3D::SpinBox3D(QWidget * parent, const char * l) : ToolBox::SpinBox3D::SpinBox3D(QWidget * parent, const char * l) :
@@ -70,6 +84,13 @@ void ToolBox::SpinBox::disconnect() const
void ToolBox::TransformPanel::setObject(const Qtk::Object * object) void ToolBox::TransformPanel::setObject(const Qtk::Object * object)
{ {
// Zero the panel contents if there is no object selected.
if (object == Q_NULLPTR) {
spinBox3D.clear();
return;
}
// Reconnect translation panel controls to the new object. // Reconnect translation panel controls to the new object.
const std::vector binds = {&Object::setTranslationX, const std::vector binds = {&Object::setTranslationX,
&Object::setTranslationY, &Object::setTranslationY,
@@ -90,6 +111,12 @@ void ToolBox::TransformPanel::setObject(const Qtk::Object * object)
void ToolBox::ScalePanel::setObject(const Qtk::Object * object) void ToolBox::ScalePanel::setObject(const Qtk::Object * object)
{ {
// Zero the panel contents if there is no object selected.
if (object == Q_NULLPTR) {
spinBox3D.clear();
return;
}
// Reconnect scale panel controls to the new object. // Reconnect scale panel controls to the new object.
const std::vector binds = { const std::vector binds = {
&Object::setScaleX, &Object::setScaleY, &Object::setScaleZ}; &Object::setScaleX, &Object::setScaleY, &Object::setScaleZ};
@@ -123,8 +150,21 @@ void ToolBox::refreshProperties(const Object * object)
void ToolBox::refreshShaders(const Object * object) void ToolBox::refreshShaders(const Object * object)
{ {
// Zero the panel contents if there is no object selected.
if (object == Q_NULLPTR) {
vertex_.clear();
fragment_.clear();
return;
}
vertex_.path.setValue(object->getVertexShader().c_str()); vertex_.path.setValue(object->getVertexShader().c_str());
vertex_.editor->setText(object->getVertexShaderSourceCode().c_str()); vertex_.editor->setText(object->getVertexShaderSourceCode().c_str());
fragment_.path.setValue(object->getFragmentShader().c_str()); fragment_.path.setValue(object->getFragmentShader().c_str());
fragment_.editor->setText(object->getFragmentShaderSourceCode().c_str()); fragment_.editor->setText(object->getFragmentShaderSourceCode().c_str());
} }
void ToolBox::refresh(const Object * object)
{
refreshProperties(object);
refreshShaders(object);
}

View File

@@ -18,8 +18,8 @@
#include <QLabel> #include <QLabel>
#include <QTextEdit> #include <QTextEdit>
#include "qtk/object.h"
#include "qtk/scene.h"
namespace Ui namespace Ui
{ {
@@ -45,8 +45,13 @@ namespace Qtk
void refreshShaders(const Object * object); void refreshShaders(const Object * object);
void refresh(const Object * object);
void updateFocus(const QString & name); void updateFocus(const QString & name);
[[nodiscard]] Object * getObjectFocus() const { return objectFocus_; }
void clearFocus();
private: private:
/************************************************************************* /*************************************************************************
@@ -65,12 +70,12 @@ namespace Qtk
{ {
} }
void setValue(const QString & v) { value->setText(v); } void setValue(const char * v) const { value->setText(tr(v)); }
void setItem(const QString & l, const QString & v) void setItem(const char * l, const char * v) const
{ {
label->setText(l); label->setText(tr(l));
value->setText(v); value->setText(tr(v));
} }
QLabel * label; QLabel * label;
@@ -79,16 +84,22 @@ namespace Qtk
/// We pass the parent widget so that Qt handles releasing memory. /// We pass the parent widget so that Qt handles releasing memory.
explicit ObjectDetails(QWidget * parent) : explicit ObjectDetails(QWidget * parent) :
name(parent, "Name:"), objectType(parent, "ObjectType:") name(parent, "Name:"), objectType(parent, "Object Type:")
{ {
} }
/// Refresh to display the new object's details /// Refresh to display the new object's details
void setObject(const Qtk::Object * object) void setObject(const Qtk::Object * object) const
{ {
name.setItem(tr("Name:"), object->getName()); // Zero contents if there is no object selected.
if (object == Q_NULLPTR) {
name.setValue("");
objectType.setValue("No object selected");
return;
}
name.setItem("Name:", object->getName().toStdString().c_str());
objectType.setItem( objectType.setItem(
tr("Type:"), "Type:",
object->getType() == Object::QTK_MESH ? "Mesh" : "Model"); object->getType() == Object::QTK_MESH ? "Mesh" : "Model");
} }
@@ -119,6 +130,15 @@ namespace Qtk
/// Assigning a QWidget to a QLayout also ensures Qt will clean up. /// Assigning a QWidget to a QLayout also ensures Qt will clean up.
explicit SpinBox3D(QWidget * parent, const char * l = "SpinBox3D:"); explicit SpinBox3D(QWidget * parent, const char * l = "SpinBox3D:");
/// Zero the SpinBox3D display.
void clear()
{
for (auto & field : fields) {
field->disconnect();
field->spinBox->setValue(0.0);
}
}
/// The main layout for the SpinBox3D widget. /// The main layout for the SpinBox3D widget.
QHBoxLayout * layout; QHBoxLayout * layout;
@@ -176,6 +196,13 @@ namespace Qtk
layout->addWidget(editor); layout->addWidget(editor);
} }
/// Zero the ShaderView display.
void clear() const
{
path.setValue("");
editor->setText("");
}
/// The main layout for the ShaderView widget. /// The main layout for the ShaderView widget.
QVBoxLayout * layout; QVBoxLayout * layout;
@@ -190,6 +217,8 @@ namespace Qtk
QFormLayout * properiesForm_; QFormLayout * properiesForm_;
QFormLayout * shaderForm_; QFormLayout * shaderForm_;
Object * objectFocus_ {};
Ui::ToolBox * ui; Ui::ToolBox * ui;
}; };
} // namespace Qtk } // namespace Qtk

View File

@@ -55,6 +55,34 @@ template <> Model * Scene::addObject(Model * object)
return object; return object;
} }
template <> void Scene::removeObject(MeshRenderer * object)
{
auto it = std::find(mMeshes.begin(), mMeshes.end(), object);
if (it == mMeshes.end()) {
qDebug() << "[Scene::removeObject]: Failed to remove object: "
<< object->getName() << " (" << object << ")";
return;
}
--mObjectCount[object->getName()];
mMeshes.erase(it);
emit sceneUpdated(mSceneName);
}
template <> void Scene::removeObject(Model * object)
{
auto it = std::find(mModels.begin(), mModels.end(), object);
if (it == mModels.end()) {
qDebug() << "[Scene::removeObject]: Failed to remove object: "
<< object->getName() << " (" << object << ")";
return;
}
--mObjectCount[object->getName()];
mModels.erase(it);
emit sceneUpdated(mSceneName);
}
void Scene::draw() void Scene::draw()
{ {
if (!mInit) { if (!mInit) {

View File

@@ -85,16 +85,16 @@ namespace Qtk
void loadModel(const QUrl & url) void loadModel(const QUrl & url)
{ {
auto fileName = url.fileName().replace(".obj", "").toStdString(); auto fileName = url.fileName().replace(".obj", "");
auto filePath = url.toLocalFile().toStdString(); auto filePath = url.toLocalFile();
loadModel(fileName, filePath); loadModel(fileName, filePath);
} }
void loadModel(const std::string & name, const std::string & path) void loadModel(const QString & name, const QString & path)
{ {
// Add the dropped model to the load queue. // Add the dropped model to the load queue.
// This is consumed during rendering of the scene if not empty. // This is consumed during rendering of the scene if not empty.
mModelLoadQueue.emplace(name, path); mModelLoadQueue.emplace(name.toStdString(), path.toStdString());
} }
/************************************************************************* /*************************************************************************
@@ -182,8 +182,9 @@ namespace Qtk
/** /**
* Adds objects to the scene. * Adds objects to the scene.
* This template provides explicit specializations for valid types. * This template provides explicit specializations for the valid types:
* Adding any object other than these types will cause errors. * MeshRenderer, Model
* Any other object type will cause errors.
* TODO: Refactor to use Object base class container for scene objects. * TODO: Refactor to use Object base class container for scene objects.
* *
* If creating a new object type for a scene, it must inherit Qtk::Object * If creating a new object type for a scene, it must inherit Qtk::Object
@@ -194,6 +195,17 @@ namespace Qtk
*/ */
template <typename T> T * addObject(T * object); template <typename T> T * addObject(T * object);
/**
* Removes an object from the scene.
* This template provides explicit specializations for the valid types:
* MeshRenderer, Model
* Any other object type will cause errors.
* TODO: Refactor to use Object base class container for scene objects.
*
* @param object Pointer to the object to remove from the scene.
*/
template <typename T> void removeObject(T * object);
/** /**
* @param name The name to use for this scene. * @param name The name to use for this scene.
*/ */

View File

@@ -109,11 +109,12 @@ void main()
#version 330 #version 330
uniform samplerCube uTexture; uniform samplerCube uTexture;
varying vec3 vTexCoord; in vec3 vTexCoord;
out vec4 FragColor;
void main() void main()
{ {
gl_FragColor = texture(uTexture, vTexCoord); FragColor = texture(uTexture, vTexCoord);
} }
)" )"