From 57aa9e6ae2df126c3d6bf4f98d8e23657d68c6eb Mon Sep 17 00:00:00 2001 From: James Gre Date: Mon, 13 Apr 2026 18:37:13 -0400 Subject: [PATCH 01/28] Add Qt6 3D model viewport with custom OpenGL renderer Upgrade to Qt6 and add an interactive 3D preview panel for MDL files. Includes orbit camera, ASCII MDL parser, Blinn-Phong shading with smooth normals, TGA texture loading, and Go CLI integration for decompiling binary MDLs before preview. --- CMakeLists.txt | 43 +- camera.cpp | 67 +++ camera.h | 33 ++ gpumesh.cpp | 93 ++++ gpumesh.h | 43 ++ gputexture.cpp | 166 +++++++ gputexture.h | 27 + main.cpp | 9 + mainwindow.cpp | 1111 +++++++++--------------------------------- mainwindow.h | 12 +- mainwindow_clean.cpp | 321 ++++++------ mdlscene.cpp | 261 ++++++++++ mdlscene.h | 58 +++ modelviewport.cpp | 233 +++++++++ modelviewport.h | 51 ++ renderer.cpp | 382 +++++++++++++++ renderer.h | 69 +++ 17 files changed, 1935 insertions(+), 1044 deletions(-) create mode 100644 camera.cpp create mode 100644 camera.h create mode 100644 gpumesh.cpp create mode 100644 gpumesh.h create mode 100644 gputexture.cpp create mode 100644 gputexture.h create mode 100644 mdlscene.cpp create mode 100644 mdlscene.h create mode 100644 modelviewport.cpp create mode 100644 modelviewport.h create mode 100644 renderer.cpp create mode 100644 renderer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 82ba38c..994d883 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,33 +1,30 @@ -cmake_minimum_required(VERSION 3.10) -project(cleanmodels-qt) +cmake_minimum_required(VERSION 3.16) +project(cleanmodels-qt LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(QT_COMPONENTS Core Widgets Gui) - -if(DEFINED ENV{STATIC_BUILD} AND NOT $ENV{STATIC_BUILD} STREQUAL "") - if(DEFINED ENV{QT_STATIC_PATH} AND NOT $ENV{QT_STATIC_PATH} STREQUAL "") - set(CMAKE_PREFIX_PATH "$ENV{QT_STATIC_PATH}/lib/cmake/Qt5") - set(QT_ROOT "$ENV{QT_STATIC_PATH}/lib/cmake/") - include("./CMakeModules/Qt.cmake") - endif() -endif() - -find_package(Qt5 COMPONENTS ${QT_COMPONENTS} REQUIRED) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) -add_executable(${PROJECT_NAME} main.cpp mainwindow.cpp mainwindow_clean.cpp fsmodel.cpp icons.qrc prolog_files.qrc mainwindow.ui) +find_package(Qt6 REQUIRED COMPONENTS Core Widgets Gui OpenGL OpenGLWidgets) -include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Widgets Qt5::Gui) +add_executable(${PROJECT_NAME} + main.cpp + mainwindow.cpp + mainwindow_clean.cpp + fsmodel.cpp + camera.cpp + mdlscene.cpp + gpumesh.cpp + gputexture.cpp + renderer.cpp + modelviewport.cpp + icons.qrc + mainwindow.ui +) -if(DEFINED ENV{STATIC_BUILD} AND NOT $ENV{STATIC_BUILD} STREQUAL "") - if(DEFINED ENV{QT_STATIC_PATH} AND NOT $ENV{QT_STATIC_PATH} STREQUAL "") - qt5_import_plugins(${PROJECT_NAME} - EXCLUDE_BY_TYPE imageformats - ) - endif() -endif() \ No newline at end of file +target_link_libraries(${PROJECT_NAME} PRIVATE + Qt6::Core Qt6::Widgets Qt6::Gui Qt6::OpenGL Qt6::OpenGLWidgets) diff --git a/camera.cpp b/camera.cpp new file mode 100644 index 0000000..fd0d824 --- /dev/null +++ b/camera.cpp @@ -0,0 +1,67 @@ +#include "camera.h" +#include +#include + +Camera::Camera() = default; + +QVector3D Camera::position() const +{ + float x = m_distance * cosf(m_pitch) * cosf(m_yaw); + float y = m_distance * cosf(m_pitch) * sinf(m_yaw); + float z = m_distance * sinf(m_pitch); + return m_target + QVector3D(x, y, z); +} + +QMatrix4x4 Camera::viewMatrix() const +{ + QMatrix4x4 view; + view.lookAt(position(), m_target, QVector3D(0, 0, 1)); + return view; +} + +QMatrix4x4 Camera::projectionMatrix() const +{ + QMatrix4x4 proj; + proj.perspective(m_fov, m_aspect, m_near, m_far); + return proj; +} + +void Camera::rotate(float dx, float dy) +{ + m_yaw -= dx * 0.01f; + m_pitch += dy * 0.01f; + m_pitch = std::clamp(m_pitch, -1.5f, 1.5f); +} + +void Camera::pan(float dx, float dy) +{ + QVector3D forward = (m_target - position()).normalized(); + QVector3D up(0, 0, 1); + QVector3D right = QVector3D::crossProduct(forward, up).normalized(); + QVector3D camUp = QVector3D::crossProduct(right, forward).normalized(); + + float scale = m_distance * 0.002f; + m_target += right * (-dx * scale) + camUp * (dy * scale); +} + +void Camera::zoom(float delta) +{ + m_distance *= (1.0f - delta * 0.1f); + m_distance = std::clamp(m_distance, 0.1f, 10000.0f); +} + +void Camera::focusOnBounds(const QVector3D &bmin, const QVector3D &bmax) +{ + m_target = (bmin + bmax) * 0.5f; + float extent = (bmax - bmin).length(); + if (extent < 0.001f) + extent = 10.0f; + m_distance = extent * 1.5f; + m_near = m_distance * 0.001f; + m_far = m_distance * 100.0f; +} + +void Camera::setAspectRatio(float aspect) +{ + m_aspect = aspect; +} diff --git a/camera.h b/camera.h new file mode 100644 index 0000000..336f276 --- /dev/null +++ b/camera.h @@ -0,0 +1,33 @@ +#ifndef CAMERA_H +#define CAMERA_H + +#include +#include + +class Camera +{ +public: + Camera(); + + void rotate(float dx, float dy); + void pan(float dx, float dy); + void zoom(float delta); + void focusOnBounds(const QVector3D &bmin, const QVector3D &bmax); + void setAspectRatio(float aspect); + + QMatrix4x4 viewMatrix() const; + QMatrix4x4 projectionMatrix() const; + QVector3D position() const; + +private: + QVector3D m_target; + float m_distance = 10.0f; + float m_yaw = 0.0f; + float m_pitch = 0.6f; + float m_fov = 45.0f; + float m_aspect = 1.0f; + float m_near = 0.01f; + float m_far = 5000.0f; +}; + +#endif diff --git a/gpumesh.cpp b/gpumesh.cpp new file mode 100644 index 0000000..63f8d87 --- /dev/null +++ b/gpumesh.cpp @@ -0,0 +1,93 @@ +#include "gpumesh.h" +#include + +GpuMesh::~GpuMesh() = default; + +GpuMesh::GpuMesh(GpuMesh &&other) noexcept + : m_vao(other.m_vao), m_vbo(other.m_vbo), m_ebo(other.m_ebo), m_indexCount(other.m_indexCount) +{ + other.m_vao = other.m_vbo = other.m_ebo = 0; + other.m_indexCount = 0; +} + +GpuMesh &GpuMesh::operator=(GpuMesh &&other) noexcept +{ + if (this != &other) + { + m_vao = other.m_vao; + m_vbo = other.m_vbo; + m_ebo = other.m_ebo; + m_indexCount = other.m_indexCount; + other.m_vao = other.m_vbo = other.m_ebo = 0; + other.m_indexCount = 0; + } + return *this; +} + +void GpuMesh::upload(QOpenGLFunctions_3_3_Core *gl, + const QVector &vertices, + const QVector &indices) +{ + m_indexCount = indices.size(); + + gl->glGenVertexArrays(1, &m_vao); + gl->glGenBuffers(1, &m_vbo); + gl->glGenBuffers(1, &m_ebo); + + gl->glBindVertexArray(m_vao); + + gl->glBindBuffer(GL_ARRAY_BUFFER, m_vbo); + gl->glBufferData(GL_ARRAY_BUFFER, + vertices.size() * static_cast(sizeof(Vertex)), + vertices.constData(), GL_STATIC_DRAW); + + gl->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo); + gl->glBufferData(GL_ELEMENT_ARRAY_BUFFER, + indices.size() * static_cast(sizeof(uint32_t)), + indices.constData(), GL_STATIC_DRAW); + + // location 0: position (vec3) + gl->glEnableVertexAttribArray(0); + gl->glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), + reinterpret_cast(offsetof(Vertex, pos))); + + // location 1: normal (vec3) + gl->glEnableVertexAttribArray(1); + gl->glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), + reinterpret_cast(offsetof(Vertex, normal))); + + // location 2: uv (vec2) + gl->glEnableVertexAttribArray(2); + gl->glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), + reinterpret_cast(offsetof(Vertex, uv))); + + gl->glBindVertexArray(0); +} + +void GpuMesh::draw(QOpenGLFunctions_3_3_Core *gl) const +{ + if (!m_vao || m_indexCount == 0) + return; + gl->glBindVertexArray(m_vao); + gl->glDrawElements(GL_TRIANGLES, m_indexCount, GL_UNSIGNED_INT, nullptr); + gl->glBindVertexArray(0); +} + +void GpuMesh::drawWireframe(QOpenGLFunctions_3_3_Core *gl) const +{ + if (!m_vao || m_indexCount == 0) + return; + gl->glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gl->glBindVertexArray(m_vao); + gl->glDrawElements(GL_TRIANGLES, m_indexCount, GL_UNSIGNED_INT, nullptr); + gl->glBindVertexArray(0); + gl->glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); +} + +void GpuMesh::destroy(QOpenGLFunctions_3_3_Core *gl) +{ + if (m_vao) { gl->glDeleteVertexArrays(1, &m_vao); m_vao = 0; } + if (m_vbo) { gl->glDeleteBuffers(1, &m_vbo); m_vbo = 0; } + if (m_ebo) { gl->glDeleteBuffers(1, &m_ebo); m_ebo = 0; } + m_indexCount = 0; +} diff --git a/gpumesh.h b/gpumesh.h new file mode 100644 index 0000000..1f86691 --- /dev/null +++ b/gpumesh.h @@ -0,0 +1,43 @@ +#ifndef GPUMESH_H +#define GPUMESH_H + +#include +#include +#include +#include + +struct Vertex { + float pos[3]; + float normal[3]; + float uv[2]; +}; + +class GpuMesh +{ +public: + GpuMesh() = default; + ~GpuMesh(); + + GpuMesh(const GpuMesh &) = delete; + GpuMesh &operator=(const GpuMesh &) = delete; + GpuMesh(GpuMesh &&other) noexcept; + GpuMesh &operator=(GpuMesh &&other) noexcept; + + void upload(QOpenGLFunctions_3_3_Core *gl, + const QVector &vertices, + const QVector &indices); + + void draw(QOpenGLFunctions_3_3_Core *gl) const; + void drawWireframe(QOpenGLFunctions_3_3_Core *gl) const; + void destroy(QOpenGLFunctions_3_3_Core *gl); + + bool isValid() const { return m_vao != 0; } + +private: + GLuint m_vao = 0; + GLuint m_vbo = 0; + GLuint m_ebo = 0; + int m_indexCount = 0; +}; + +#endif diff --git a/gputexture.cpp b/gputexture.cpp new file mode 100644 index 0000000..d03c735 --- /dev/null +++ b/gputexture.cpp @@ -0,0 +1,166 @@ +#include "gputexture.h" +#include +#include +#include +#include + +bool GpuTexture::loadFromFile(QOpenGLFunctions_3_3_Core *gl, const QString &path) +{ + if (m_texture) { + gl->glDeleteTextures(1, &m_texture); + m_texture = 0; + } + + QString suffix = QFileInfo(path).suffix().toLower(); + + if (suffix == "tga") + return loadTGA(gl, path); + + // Fallback: let QImage try + QImage img(path); + if (img.isNull()) { + qWarning() << "GpuTexture: cannot load" << path; + return false; + } + img = img.convertToFormat(QImage::Format_RGBA8888).flipped(Qt::Vertical); + return uploadRGBA(gl, img.constBits(), img.width(), img.height(), true); +} + +bool GpuTexture::loadTGA(QOpenGLFunctions_3_3_Core *gl, const QString &path) +{ + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) + return false; + + QByteArray raw = f.readAll(); + f.close(); + + if (raw.size() < 18) + return false; + + const auto *hdr = reinterpret_cast(raw.constData()); + int idLen = hdr[0]; + int cmapType = hdr[1]; + int imgType = hdr[2]; + int width = hdr[12] | (hdr[13] << 8); + int height = hdr[14] | (hdr[15] << 8); + int bpp = hdr[16]; + int descriptor = hdr[17]; + + Q_UNUSED(cmapType); + + if (imgType != 2 && imgType != 10) { + qWarning() << "GpuTexture: unsupported TGA type" << imgType; + return false; + } + + if (bpp != 24 && bpp != 32) { + qWarning() << "GpuTexture: unsupported TGA bpp" << bpp; + return false; + } + + int channels = bpp / 8; + int pixelDataOffset = 18 + idLen; + int expectedSize = pixelDataOffset + width * height * channels; + + bool topOrigin = (descriptor & 0x20) != 0; + bool hasAlpha = (bpp == 32); + + QByteArray rgba(width * height * 4, '\0'); + auto *dst = reinterpret_cast(rgba.data()); + const auto *src = reinterpret_cast(raw.constData()) + pixelDataOffset; + + if (imgType == 2) { + // Uncompressed + if (raw.size() < expectedSize) + return false; + + for (int i = 0; i < width * height; ++i) { + dst[i * 4 + 0] = src[i * channels + 2]; // R (TGA stores BGR) + dst[i * 4 + 1] = src[i * channels + 1]; // G + dst[i * 4 + 2] = src[i * channels + 0]; // B + dst[i * 4 + 3] = hasAlpha ? src[i * channels + 3] : 255; + } + } else { + // RLE compressed (type 10) + int pixelCount = width * height; + int pixel = 0; + int srcOff = pixelDataOffset; + + while (pixel < pixelCount && srcOff < raw.size()) { + unsigned char packet = static_cast(raw[srcOff++]); + int count = (packet & 0x7F) + 1; + + if (packet & 0x80) { + // RLE packet + if (srcOff + channels > raw.size()) break; + unsigned char b = raw[srcOff++]; + unsigned char g = raw[srcOff++]; + unsigned char r = raw[srcOff++]; + unsigned char a = hasAlpha ? raw[srcOff++] : 255; + + for (int j = 0; j < count && pixel < pixelCount; ++j, ++pixel) { + dst[pixel * 4 + 0] = r; + dst[pixel * 4 + 1] = g; + dst[pixel * 4 + 2] = b; + dst[pixel * 4 + 3] = a; + } + } else { + // Raw packet + for (int j = 0; j < count && pixel < pixelCount; ++j, ++pixel) { + if (srcOff + channels > raw.size()) break; + dst[pixel * 4 + 0] = raw[srcOff + 2]; // R + dst[pixel * 4 + 1] = raw[srcOff + 1]; // G + dst[pixel * 4 + 2] = raw[srcOff + 0]; // B + dst[pixel * 4 + 3] = hasAlpha ? raw[srcOff + 3] : 255; + srcOff += channels; + } + } + } + } + + if (!topOrigin) { + // Flip vertically (TGA default is bottom-origin) + int rowBytes = width * 4; + QByteArray row(rowBytes, '\0'); + for (int y = 0; y < height / 2; ++y) { + int topOff = y * rowBytes; + int botOff = (height - 1 - y) * rowBytes; + memcpy(row.data(), dst + topOff, rowBytes); + memcpy(dst + topOff, dst + botOff, rowBytes); + memcpy(dst + botOff, row.data(), rowBytes); + } + } + + return uploadRGBA(gl, dst, width, height, hasAlpha); +} + +bool GpuTexture::uploadRGBA(QOpenGLFunctions_3_3_Core *gl, + const unsigned char *data, int w, int h, bool hasAlpha) +{ + gl->glGenTextures(1, &m_texture); + gl->glBindTexture(GL_TEXTURE_2D, m_texture); + gl->glTexImage2D(GL_TEXTURE_2D, 0, hasAlpha ? GL_RGBA8 : GL_RGB8, + w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + gl->glGenerateMipmap(GL_TEXTURE_2D); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + gl->glBindTexture(GL_TEXTURE_2D, 0); + return true; +} + +void GpuTexture::bind(QOpenGLFunctions_3_3_Core *gl, int unit) const +{ + gl->glActiveTexture(GL_TEXTURE0 + unit); + gl->glBindTexture(GL_TEXTURE_2D, m_texture); +} + +void GpuTexture::destroy(QOpenGLFunctions_3_3_Core *gl) +{ + if (m_texture) { + gl->glDeleteTextures(1, &m_texture); + m_texture = 0; + } +} diff --git a/gputexture.h b/gputexture.h new file mode 100644 index 0000000..454fe6e --- /dev/null +++ b/gputexture.h @@ -0,0 +1,27 @@ +#ifndef GPUTEXTURE_H +#define GPUTEXTURE_H + +#include +#include + +class GpuTexture +{ +public: + GpuTexture() = default; + ~GpuTexture() = default; + + bool loadFromFile(QOpenGLFunctions_3_3_Core *gl, const QString &path); + void bind(QOpenGLFunctions_3_3_Core *gl, int unit = 0) const; + void destroy(QOpenGLFunctions_3_3_Core *gl); + + bool isValid() const { return m_texture != 0; } + +private: + GLuint m_texture = 0; + + bool loadTGA(QOpenGLFunctions_3_3_Core *gl, const QString &path); + bool uploadRGBA(QOpenGLFunctions_3_3_Core *gl, const unsigned char *data, + int w, int h, bool hasAlpha); +}; + +#endif diff --git a/main.cpp b/main.cpp index c5c3c1b..172e93c 100644 --- a/main.cpp +++ b/main.cpp @@ -1,7 +1,16 @@ #include "mainwindow.h" #include +#include + int main(int argc, char *argv[]) { + QSurfaceFormat fmt; + fmt.setVersion(3, 3); + fmt.setProfile(QSurfaceFormat::CoreProfile); + fmt.setDepthBufferSize(24); + fmt.setSamples(4); + QSurfaceFormat::setDefaultFormat(fmt); + QApplication a(argc, argv); QCoreApplication::setOrganizationName("Clean Models Community"); QCoreApplication::setApplicationName("Clean Models::EE QT"); diff --git a/mainwindow.cpp b/mainwindow.cpp index ffb0ef3..d61a3f9 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,5 +1,6 @@ #include "fsmodel.h" #include "mainwindow.h" +#include "modelviewport.h" #include "ui_mainwindow.h" #include #include @@ -13,15 +14,15 @@ #include #include #include +#include #include #include +#include #include #include #include #include -using namespace std; - MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) @@ -30,9 +31,9 @@ MainWindow::MainWindow(QWidget *parent) : ui->debugTextBrowser->insertHtml(tr("Welcome to Clean Models:EE QT!
")); #ifdef Q_OS_WIN - m_sBinaryName = "cleanmodels-cli.exe"; + m_sBinaryName = "cleanmodels.exe"; #else - m_sBinaryName = "cleanmodels-cli"; + m_sBinaryName = "cleanmodels"; #endif bool cliFound = true; @@ -48,7 +49,7 @@ MainWindow::MainWindow(QWidget *parent) : if (cliFound) { m_sBinaryPath = cliInPath; - QString foundMsg = "Clean Models Command Line Interface found at " % m_sBinaryPath; + QString foundMsg = "Clean Models CLI found at " % m_sBinaryPath; ui->debugTextBrowser->insertHtml(tr(foundMsg.toStdString().c_str())); auto sb = ui->debugTextBrowser->verticalScrollBar(); sb->setValue(sb->maximum()); @@ -56,7 +57,7 @@ MainWindow::MainWindow(QWidget *parent) : else { QString errorMsg = "Could not find the " % m_sBinaryName % " executable in the current directory or in your path!"; - QMessageBox::critical(nullptr, "No cleanmodels-cli", tr(errorMsg.toStdString().c_str())); + QMessageBox::critical(nullptr, "No cleanmodels CLI", tr(errorMsg.toStdString().c_str())); ui->debugTextBrowser->insertHtml(tr(errorMsg.toStdString().c_str())); auto sb = ui->debugTextBrowser->verticalScrollBar(); sb->setValue(sb->maximum()); @@ -64,15 +65,6 @@ MainWindow::MainWindow(QWidget *parent) : m_pCleanProcess = new QProcess(this); m_bCleanRunning = false; - m_sLastDirsPath = QCoreApplication::applicationDirPath() % "/last_dirs.pl"; - bool fileExists = QFileInfo::exists(m_sLastDirsPath) && QFileInfo(m_sLastDirsPath).isFile(); - if (!fileExists) - { - QFile fromResource(":/last_dirs.pl"); - fromResource.copy(m_sLastDirsPath); - QFile out(m_sLastDirsPath); - out.setPermissions(QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther | QFileDevice::WriteOwner | QFileDevice::WriteGroup); - } auto* sStatusLabel = new QLabel( QString( tr("Status:") ) ); m_pCleanStatus = new QLabel( QString( tr("Idle") ) ); @@ -134,14 +126,44 @@ MainWindow::MainWindow(QWidget *parent) : connect(m_dirWatcherTimer, &QTimer::timeout, this, QOverload<>::of(&MainWindow::handleDirWatcherTimer)); m_bUpdateFilesAfterClean = false; - readInLastDirs(m_sLastDirsPath); - QObject::connect(m_pCleanProcess, static_cast(&QProcess::finished), this, &MainWindow::onCleanFinished); - QObject::connect(ui->actionHelp, SIGNAL(triggered()), this, SLOT(onHelpTriggered())); - QObject::connect(ui->actionAbout, SIGNAL(triggered()), this, SLOT(onAboutTriggered())); - QObject::connect(ui->actionSavePreset, SIGNAL(triggered()), this, SLOT(onSaveConfigTriggered())); - QObject::connect(ui->actionLoadPreset, SIGNAL(triggered()), this, SLOT(onLoadConfigTriggered())); - QObject::connect(ui->actionQuit, SIGNAL(triggered()), this, SLOT(onQuitTriggered())); - QObject::connect(&m_fsWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(onDirectoryContentsChanged())); + // 3D viewport: replace the debugTextBrowser in the splitter with + // a horizontal splitter containing [debugTextBrowser | viewport] + m_viewport = new ModelViewport(this); + m_viewport->setCliBinaryPath(m_sBinaryPath); + m_viewport->setMinimumSize(200, 150); + + auto *hSplitter = new QSplitter(Qt::Horizontal, this); + QWidget *debugParent = ui->debugTextBrowser->parentWidget(); + QSplitter *parentSplitter = qobject_cast(debugParent); + if (parentSplitter) + { + int idx = parentSplitter->indexOf(ui->debugTextBrowser); + hSplitter->addWidget(ui->debugTextBrowser); + hSplitter->addWidget(m_viewport); + hSplitter->setSizes({400, 400}); + parentSplitter->insertWidget(idx, hSplitter); + } + else + { + hSplitter->addWidget(ui->debugTextBrowser); + hSplitter->addWidget(m_viewport); + hSplitter->setSizes({400, 400}); + } + + connect(m_viewport, &ModelViewport::previewError, this, [this](const QString &msg) { + appendDebugHtml("

Preview: " + msg.toHtmlEscaped() + "


"); + }); + + loadSettings(); + + connect(m_pCleanProcess, &QProcess::finished, this, &MainWindow::onCleanFinished); + connect(ui->actionHelp, &QAction::triggered, this, &MainWindow::onHelpTriggered); + connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::onAboutTriggered); + connect(ui->actionSavePreset, &QAction::triggered, this, &MainWindow::onSaveConfigTriggered); + connect(ui->actionLoadPreset, &QAction::triggered, this, &MainWindow::onLoadConfigTriggered); + connect(ui->actionQuit, &QAction::triggered, this, &MainWindow::onQuitTriggered); + connect(&m_fsWatcher, &QFileSystemWatcher::directoryChanged, this, &MainWindow::onDirectoryContentsChanged); + readSettings(); } @@ -152,7 +174,6 @@ MainWindow::~MainWindow() delete m_pCleanProcess; } -// Window Position/Geometry void MainWindow::readSettings() { QScreen *screen = QGuiApplication::primaryScreen(); @@ -182,461 +203,176 @@ void MainWindow::writeSettings() settings.setValue("geometry", saveGeometry()); } -void MainWindow::closeEvent(QCloseEvent*) -{ - writeSettings(); +void MainWindow::loadSettings() +{ + QSettings s(QCoreApplication::organizationName(), QCoreApplication::applicationName()); + s.beginGroup("options"); + + QString inDir = s.value("indir").toString(); + if (!inDir.isEmpty()) + { + onUpdateInDir(inDir); + QDir absDir; + m_pFileSystemModel->setRootPath(absDir.absoluteFilePath(inDir)); + } + + QString outDir = s.value("outdir").toString(); + if (!outDir.isEmpty()) + { + m_sOutDir = outDir; + ui->outDirectory->setText(m_sOutDir); + QDir absDir; + m_pFileSystemModel->setRootPath(absDir.absoluteFilePath(m_sOutDir)); + } + + ui->filePattern->setText(s.value("pattern", "*.mdl").toString()); + ui->logFileName->setText(s.value("logfile").toString()); + ui->summaryLogFileName->setText(s.value("summary_log").toString()); + ui->modelClassCombo->setCurrentIndex(s.value("classification", 0).toInt()); + ui->snapCombo->setCurrentIndex(s.value("snap", 0).toInt()); + ui->snapTVertsCombo->setCurrentIndex(s.value("tvert_snap", 0).toInt()); + ui->smoothingGroupsCombo->setCurrentIndex(s.value("smoothing_groups", 0).toInt()); + ui->repairAABBCombo->setCurrentIndex(s.value("fix_overhangs", 0).toInt()); + ui->dynamicWaterCombo->setCurrentIndex(s.value("dynamic_water", 0).toInt()); + ui->waterRotateTextureCombo->setCurrentIndex(s.value("rotate_water", 0).toInt()); + ui->retileWaterCombo->setCurrentIndex(s.value("tile_water", 0).toInt()); + ui->raiseLowerCombo->setCurrentIndex(s.value("tile_raise", 0).toInt()); + ui->raiseLowerAmountSpin->setValue(s.value("tile_raise_amount", 0.0).toDouble()); + ui->sliceForTileFadeCombo->setCurrentIndex(s.value("slice", 0).toInt()); + ui->renderTrimeshCombo->setCurrentIndex(s.value("render", 0).toInt()); + ui->renderShadowsCombo->setCurrentIndex(s.value("shadow", 0).toInt()); + ui->repivotCombo->setCurrentIndex(s.value("repivot", 0).toInt()); + ui->pivotsBelowZeroZCombo->setCurrentIndex(s.value("pivots_below_z0", 0).toInt()); + ui->moveBadPivotsCombo->setCurrentIndex(s.value("move_bad_pivots", 0).toInt()); + ui->foliageCombo->setCurrentIndex(s.value("foliage", 0).toInt()); + ui->groundRotateTextureCombo->setCurrentIndex(s.value("rotate_ground", 0).toInt()); + ui->tileEdgeChamfersCombo->setCurrentIndex(s.value("chamfer", 0).toInt()); + ui->retileGroundPlanesCombo->setCurrentIndex(s.value("tile_ground", 0).toInt()); + ui->cullInvisibleCheck->setChecked(s.value("invisible_mesh_cull", false).toBool()); + ui->changeWokMatCheck->setChecked(s.value("map_aabb_material", false).toBool()); + ui->changeWokMatGroupBox->setEnabled(s.value("map_aabb_material", false).toBool()); + ui->allowSplittingCheck->setChecked(s.value("allow_split", false).toBool()); + ui->waterFixupsCheck->setChecked(s.value("do_water", false).toBool()); + ui->waterFrame->setEnabled(s.value("do_water", false).toBool()); + ui->waterBitmapKeys->setText(s.value("water_key").toString()); + ui->groundBitmapKeys->setText(s.value("ground_key").toString()); + ui->splotchBitmapKeys->setText(s.value("splotch_key").toString()); + ui->foliageBitmapKeys->setText(s.value("foliage_key").toString()); + ui->subObjectSpin->setValue(s.value("min_size", 0).toInt()); + ui->meshMergeCheck->setChecked(s.value("merge_by_bitmap", false).toBool()); + ui->placeableWithTransparencyCheck->setChecked(s.value("placeable_with_transparency", false).toBool()); + ui->animateSplotchesCheck->setChecked(s.value("animate_splotches", false).toBool()); + ui->forceWhiteCheck->setChecked(s.value("force_white", false).toBool()); + ui->transparentBitmapKeys->setText(s.value("transparency_key").toString()); + ui->waveHeightSpin->setValue(s.value("wave_height", 0.0).toDouble()); + ui->changeWokMatFromSpin->setValue(s.value("map_aabb_from", 0).toInt()); + ui->changeWokMatToSpin->setValue(s.value("map_aabb_to", 0).toInt()); + ui->rescaleXSpin->setValue(s.value("rescale_x", 1.0).toDouble()); + ui->rescaleYSpin->setValue(s.value("rescale_y", 1.0).toDouble()); + ui->rescaleZSpin->setValue(s.value("rescale_z", 1.0).toDouble()); + + s.endGroup(); +} + +void MainWindow::saveSettings() +{ + QSettings s(QCoreApplication::organizationName(), QCoreApplication::applicationName()); + s.beginGroup("options"); + + s.setValue("indir", m_sInDir); + s.setValue("outdir", m_sOutDir); + s.setValue("pattern", ui->filePattern->text()); + s.setValue("logfile", ui->logFileName->text()); + s.setValue("summary_log", ui->summaryLogFileName->text()); + s.setValue("classification", ui->modelClassCombo->currentIndex()); + s.setValue("snap", ui->snapCombo->currentIndex()); + s.setValue("tvert_snap", ui->snapTVertsCombo->currentIndex()); + s.setValue("smoothing_groups", ui->smoothingGroupsCombo->currentIndex()); + s.setValue("fix_overhangs", ui->repairAABBCombo->currentIndex()); + s.setValue("dynamic_water", ui->dynamicWaterCombo->currentIndex()); + s.setValue("rotate_water", ui->waterRotateTextureCombo->currentIndex()); + s.setValue("tile_water", ui->retileWaterCombo->currentIndex()); + s.setValue("tile_raise", ui->raiseLowerCombo->currentIndex()); + s.setValue("tile_raise_amount", ui->raiseLowerAmountSpin->value()); + s.setValue("slice", ui->sliceForTileFadeCombo->currentIndex()); + s.setValue("render", ui->renderTrimeshCombo->currentIndex()); + s.setValue("shadow", ui->renderShadowsCombo->currentIndex()); + s.setValue("repivot", ui->repivotCombo->currentIndex()); + s.setValue("pivots_below_z0", ui->pivotsBelowZeroZCombo->currentIndex()); + s.setValue("move_bad_pivots", ui->moveBadPivotsCombo->currentIndex()); + s.setValue("foliage", ui->foliageCombo->currentIndex()); + s.setValue("rotate_ground", ui->groundRotateTextureCombo->currentIndex()); + s.setValue("chamfer", ui->tileEdgeChamfersCombo->currentIndex()); + s.setValue("tile_ground", ui->retileGroundPlanesCombo->currentIndex()); + s.setValue("invisible_mesh_cull", ui->cullInvisibleCheck->isChecked()); + s.setValue("map_aabb_material", ui->changeWokMatCheck->isChecked()); + s.setValue("allow_split", ui->allowSplittingCheck->isChecked()); + s.setValue("do_water", ui->waterFixupsCheck->isChecked()); + s.setValue("water_key", ui->waterBitmapKeys->text()); + s.setValue("ground_key", ui->groundBitmapKeys->text()); + s.setValue("splotch_key", ui->splotchBitmapKeys->text()); + s.setValue("foliage_key", ui->foliageBitmapKeys->text()); + s.setValue("min_size", ui->subObjectSpin->value()); + s.setValue("merge_by_bitmap", ui->meshMergeCheck->isChecked()); + s.setValue("placeable_with_transparency", ui->placeableWithTransparencyCheck->isChecked()); + s.setValue("animate_splotches", ui->animateSplotchesCheck->isChecked()); + s.setValue("force_white", ui->forceWhiteCheck->isChecked()); + s.setValue("transparency_key", ui->transparentBitmapKeys->text()); + s.setValue("wave_height", ui->waveHeightSpin->value()); + s.setValue("map_aabb_from", ui->changeWokMatFromSpin->value()); + s.setValue("map_aabb_to", ui->changeWokMatToSpin->value()); + s.setValue("rescale_x", ui->rescaleXSpin->value()); + s.setValue("rescale_y", ui->rescaleYSpin->value()); + s.setValue("rescale_z", ui->rescaleZSpin->value()); + + s.endGroup(); } -// Main last_dirs parsing and writing functions -void MainWindow::readInLastDirs(const QString& fileLoc) -{ - QFile inputFile(fileLoc); - if (inputFile.open(QIODevice::ReadOnly)) - { - QTextStream in(&inputFile); - while (!in.atEnd()) - { - QString line = in.readLine(); - QString str = R"(^:-asserta\((.*)\((.*)[,]?(\w+|\[.*\])?\)\)\.$)"; - - QRegularExpression re(str, QRegularExpression::InvertedGreedinessOption | QRegularExpression::MultilineOption); - QRegularExpressionMatchIterator i = re.globalMatch(line); - while (i.hasNext()) - { - QRegularExpressionMatch match = i.next(); - QString captured = match.captured(1); - QString capturedTwo = match.captured(2).isEmpty() ? match.captured(3) : match.captured(2); - if (captured.isEmpty() || capturedTwo.isEmpty()) - break; - if (capturedTwo.startsWith("'")) - capturedTwo.remove(0,1); - if (capturedTwo.endsWith("'")) - capturedTwo.chop(1); - if (captured == "g_indir") - { - onUpdateInDir(capturedTwo); - QDir absDir; - m_pFileSystemModel->setRootPath(absDir.absoluteFilePath(capturedTwo)); - } - else if (captured == "g_outdir") - { - m_sOutDir = capturedTwo; - ui->outDirectory->setText(m_sOutDir); - QDir absDir; - m_pFileSystemModel->setRootPath(absDir.absoluteFilePath(m_sOutDir)); - } - else if (captured == "g_pattern") - { - ui->filePattern->setText(capturedTwo); - } - else if (captured == "g_logfile") - { - ui->logFileName->setText(capturedTwo); - } - else if (captured == "g_small_log") - { - ui->summaryLogFileName->setText(capturedTwo); - } - else if (captured == "g_user_option") - { - QString value = match.captured(3); - if (capturedTwo == "classification") - { - if (value == "character") - ui->modelClassCombo->setCurrentIndex(1); - else if (value == "door") - ui->modelClassCombo->setCurrentIndex(2); - else if (value == "effect") - ui->modelClassCombo->setCurrentIndex(3); - else if (value == "item") - ui->modelClassCombo->setCurrentIndex(4); - else if (value == "tile") - ui->modelClassCombo->setCurrentIndex(5); - else - ui->modelClassCombo->setCurrentIndex(0); - } - else if (capturedTwo == "snap") - { - if (value == "binary") - ui->snapCombo->setCurrentIndex(1); - else if (value == "decimal") - ui->snapCombo->setCurrentIndex(2); - else if (value == "fine") - ui->snapCombo->setCurrentIndex(3); - else - ui->snapCombo->setCurrentIndex(0); - } - else if (capturedTwo == "tvert_snap") - { - if (value == "256") - ui->snapTVertsCombo->setCurrentIndex(1); - else if (value == "512") - ui->snapTVertsCombo->setCurrentIndex(2); - else if (value == "1024") - ui->snapTVertsCombo->setCurrentIndex(3); - else - ui->snapTVertsCombo->setCurrentIndex(0); - } - else if (capturedTwo == "use_Smoothed") - { - if (value == "ignore") - ui->smoothingGroupsCombo->setCurrentIndex(1); - else if (value == "protect") - ui->smoothingGroupsCombo->setCurrentIndex(2); - else - ui->smoothingGroupsCombo->setCurrentIndex(0); - } - else if (capturedTwo == "fix_overhangs") - { - if (value == "yes") - ui->repairAABBCombo->setCurrentIndex(1); - else if (value == "interior_only") - ui->repairAABBCombo->setCurrentIndex(2); - else - ui->repairAABBCombo->setCurrentIndex(0); - } - else if (capturedTwo == "dynamic_water") - { - if (value == "no") - ui->dynamicWaterCombo->setCurrentIndex(1); - else if (value == "wavy") - ui->dynamicWaterCombo->setCurrentIndex(2); - else - ui->dynamicWaterCombo->setCurrentIndex(0); - } - else if (capturedTwo == "rotate_water") - { - if (value == "1") - ui->waterRotateTextureCombo->setCurrentIndex(1); - else if (value == "0") - ui->waterRotateTextureCombo->setCurrentIndex(2); - else - ui->waterRotateTextureCombo->setCurrentIndex(0); - } - else if (capturedTwo == "tile_water") - { - if (value == "1") - ui->retileWaterCombo->setCurrentIndex(1); - else if (value == "2") - ui->retileWaterCombo->setCurrentIndex(2); - else if (value == "3") - ui->retileWaterCombo->setCurrentIndex(3); - else - ui->retileWaterCombo->setCurrentIndex(0); - } - else if (capturedTwo == "tile_raise") - { - if (value == "raise") - ui->raiseLowerCombo->setCurrentIndex(1); - else if (value == "lower") - ui->raiseLowerCombo->setCurrentIndex(2); - else - ui->raiseLowerCombo->setCurrentIndex(0); - } - else if (capturedTwo == "slice") - { - if (value == "no") - ui->sliceForTileFadeCombo->setCurrentIndex(1); - else if (value == "undo") - ui->sliceForTileFadeCombo->setCurrentIndex(2); - else - ui->sliceForTileFadeCombo->setCurrentIndex(0); - } - else if (capturedTwo == "tile_raise_amount") - { - ui->raiseLowerAmountSpin->setValue(value.toDouble()); - } - else if (capturedTwo == "render") - { - if (value == "all") - ui->renderTrimeshCombo->setCurrentIndex(1); - else if (value == "none") - ui->renderTrimeshCombo->setCurrentIndex(2); - else - ui->renderTrimeshCombo->setCurrentIndex(0); - } - else if (capturedTwo == "shadow") - { - if (value == "all") - ui->renderShadowsCombo->setCurrentIndex(1); - else if (value == "none") - ui->renderShadowsCombo->setCurrentIndex(2); - else - ui->renderShadowsCombo->setCurrentIndex(0); - } - else if (capturedTwo == "repivot") - { - if (value == "all") - ui->repivotCombo->setCurrentIndex(1); - else if (value == "none") - ui->repivotCombo->setCurrentIndex(2); - else - ui->repivotCombo->setCurrentIndex(0); - } - else if (capturedTwo == "pivots_below_z=0") - { - if (value == "allow") - ui->pivotsBelowZeroZCombo->setCurrentIndex(1); - else if (value == "slice") - ui->pivotsBelowZeroZCombo->setCurrentIndex(2); - else - ui->pivotsBelowZeroZCombo->setCurrentIndex(0); - } - else if (capturedTwo == "move_bad_pivots") - { - if (value == "top") - ui->moveBadPivotsCombo->setCurrentIndex(1); - else if (value == "middle") - ui->moveBadPivotsCombo->setCurrentIndex(2); - else if (value == "bottom") - ui->moveBadPivotsCombo->setCurrentIndex(3); - else - ui->moveBadPivotsCombo->setCurrentIndex(0); - } - else if (capturedTwo == "foliage") - { - if (value == "tilefade") - ui->foliageCombo->setCurrentIndex(1); - else if (value == "animate") - ui->foliageCombo->setCurrentIndex(2); - else if (value == "de-animate") - ui->foliageCombo->setCurrentIndex(3); - else if (value == "ignore") - ui->foliageCombo->setCurrentIndex(4); - else - ui->foliageCombo->setCurrentIndex(0); - } - else if (capturedTwo == "rotate_ground") - { - if (value == "1") - ui->groundRotateTextureCombo->setCurrentIndex(1); - else if (value == "0") - ui->groundRotateTextureCombo->setCurrentIndex(2); - else - ui->groundRotateTextureCombo->setCurrentIndex(0); - } - else if (capturedTwo == "chamfer") - { - if (value == "add") - ui->tileEdgeChamfersCombo->setCurrentIndex(1); - else if (value == "delete") - ui->tileEdgeChamfersCombo->setCurrentIndex(2); - else - ui->tileEdgeChamfersCombo->setCurrentIndex(0); - } - else if (capturedTwo == "tile_ground") - { - if (value == "1") - ui->retileGroundPlanesCombo->setCurrentIndex(1); - else if (value == "2") - ui->retileGroundPlanesCombo->setCurrentIndex(2); - else if (value == "3") - ui->retileGroundPlanesCombo->setCurrentIndex(3); - else - ui->retileGroundPlanesCombo->setCurrentIndex(0); - } - else if (capturedTwo == "invisible_mesh_cull") - { - ui->cullInvisibleCheck->setChecked(value == "yes"); - } - else if (capturedTwo == "map_aabb_material") - { - ui->changeWokMatCheck->setChecked(value == "yes"); - ui->changeWokMatGroupBox->setEnabled(value == "yes"); - } - else if (capturedTwo == "allow_split") - { - ui->allowSplittingCheck->setChecked(value == "yes"); - } - else if (capturedTwo == "do_water") - { - ui->waterFixupsCheck->setChecked(value == "yes"); - ui->waterFrame->setEnabled(value == "yes"); - } - else if (capturedTwo == "water_key") - { - ui->waterBitmapKeys->setText(value); - } - else if (capturedTwo == "ground_key") - { - ui->groundBitmapKeys->setText(value); - } - else if (capturedTwo == "splotch_key") - { - ui->splotchBitmapKeys->setText(value); - } - else if (capturedTwo == "foliage_key") - { - ui->foliageBitmapKeys->setText(value); - } - else if (capturedTwo == "min_Size") - { - ui->subObjectSpin->setValue(value.toInt()); - } - else if (capturedTwo == "merge_by_bitmap") - { - ui->meshMergeCheck->setChecked(value == "yes"); - } - else if (capturedTwo == "placeable_with_transparency") - { - ui->placeableWithTransparencyCheck->setChecked(value == "yes"); - ui->transBitmapKeyFrame->setEnabled(value == "yes"); - } - else if (capturedTwo == "splotch") - { - ui->animateSplotchesCheck->setChecked(value == "animate"); - ui->splotchBitmapKeysLabel->setEnabled(value == "animate"); - ui->splotchBitmapKeys->setEnabled(value == "animate"); - } - else if (capturedTwo == "force_white") - { - ui->forceWhiteCheck->setChecked(value == "yes"); - } - else if (capturedTwo == "split_Priority") - { - ui->forceWhiteCheck->setChecked(value == "concave"); - } - else if (capturedTwo == "transparency_key") - { - ui->transparentBitmapKeys->setText(value); - } - else if (capturedTwo == "wave_height") - { - ui->waveHeightSpin->setValue(value.toDouble()); - } - else if (capturedTwo == "map_aabb_from") - { - ui->changeWokMatFromSpin->setValue(value.toInt()); - } - else if (capturedTwo == "map_aabb_to") - { - ui->changeWokMatToSpin->setValue(value.toInt()); - } - else if (capturedTwo == "rescaleXYZ") - { - double X = 1.0; - double Y = 1.0; - double Z = 1.0; - if (value != "no") - { - auto scales = value.mid(1,value.length() - 2).split(","); - X = scales[0].toDouble(nullptr); - Y = scales[1].toDouble(nullptr); - Z = scales[2].toDouble(nullptr); - } - ui->rescaleXSpin->setValue(X); - ui->rescaleYSpin->setValue(Y); - ui->rescaleZSpin->setValue(Z); - } - } - } - } - inputFile.close(); - } -} - -void MainWindow::replaceUserOption(const QString& key, const QString& value, bool coreVal) +void MainWindow::closeEvent(QCloseEvent*) { - QString str, rpl; - if (!coreVal) - { - str = "^:-asserta\\(g_user_option\\(" % key % R"(,(.*)\)\)\.$)"; - rpl = ":-asserta(g_user_option(" % key % "," % value % "))."; - } - else - { - str = "^:-asserta\\(" % key % R"((.*)\)\)\.$)"; - rpl = ":-asserta(" % key % "('" % value % "'))."; - } - QFile file(m_sLastDirsPath); - if(!file.open(QIODevice::Text | QIODevice::ReadWrite)) - { - QMessageBox::critical(nullptr, "exception", tr("Could not open last_dirs for saving!")); - return; - } - QString dataText = file.readAll(); - file.close(); - - QRegularExpression re(str, QRegularExpression::InvertedGreedinessOption | QRegularExpression::MultilineOption); - dataText.replace(re, rpl); - - if(file.open(QFile::WriteOnly | QFile::Truncate)) - { - QTextStream out(&file); - out << dataText; - } - file.close(); + writeSettings(); + saveSettings(); } -// SLOTS/SIGNALS void MainWindow::onLoadConfigTriggered() { QFileDialog fileDialog; fileDialog.setAcceptMode(QFileDialog::AcceptMode::AcceptOpen); QStringList nameFilters; - nameFilters.append("Clean Models Config (*.cm)"); + nameFilters.append("Clean Models Config (*.ini)"); + nameFilters.append("Legacy Config (*.cm *.pl)"); fileDialog.setNameFilters(nameFilters); fileDialog.setDirectory(QDir::currentPath()); if (fileDialog.exec()) { QString fileName = fileDialog.selectedFiles()[0]; - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly)) - { - QMessageBox::information(this, tr("Unable to open file"), file.errorString()); - return; - } - else - file.close(); - if (QFile::exists(m_sLastDirsPath)) - { - QFile::remove(m_sLastDirsPath); - } - if (!file.copy(m_sLastDirsPath)) - { - QMessageBox::information(this, tr("Unable to load file"), file.errorString()); - return; - } - else - readInLastDirs(m_sLastDirsPath); + QSettings imported(fileName, QSettings::IniFormat); + QSettings current(QCoreApplication::organizationName(), QCoreApplication::applicationName()); + for (const auto &key : imported.allKeys()) + current.setValue(key, imported.value(key)); + loadSettings(); } } void MainWindow::onSaveConfigTriggered() { + saveSettings(); + QFileDialog fileDialog; fileDialog.setAcceptMode(QFileDialog::AcceptMode::AcceptSave); fileDialog.setFileMode(QFileDialog::AnyFile); - fileDialog.setDefaultSuffix("cm"); + fileDialog.setDefaultSuffix("ini"); QStringList nameFilters; - nameFilters.append("Clean Models Config (*.cm)"); + nameFilters.append("Clean Models Config (*.ini)"); fileDialog.setNameFilters(nameFilters); fileDialog.setDirectory(QDir::currentPath()); if (fileDialog.exec()) { QString fileName = fileDialog.selectedFiles()[0]; - QFile file(fileName); - if (!file.open(QIODevice::WriteOnly)) - { - QMessageBox::information(this, tr("Unable to open file"), file.errorString()); - return; - } - if (QFile::exists(fileName)) - { - QFile::remove(fileName); - } - QFile fromResource(m_sLastDirsPath); - if (fromResource.copy(fileName)) - { - QFile out(fileName); - out.setPermissions(QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther | QFileDevice::WriteOwner | QFileDevice::WriteGroup); - file.close(); - } - else - { - QMessageBox::information(this, tr("Unable to save file"),fromResource.errorString()); - return; - } + QSettings current(QCoreApplication::organizationName(), QCoreApplication::applicationName()); + QSettings exported(fileName, QSettings::IniFormat); + for (const auto &key : current.allKeys()) + exported.setValue(key, current.value(key)); } } @@ -644,7 +380,8 @@ void MainWindow::onAboutTriggered() { QMessageBox::about(this, tr("About Clean Models:EE QT"), tr("A front end to Clean Models, a utility to tidy up 3d models\n" - "for usage in Neverwinter Nights: Enhanced Edition.")); + "for usage in Neverwinter Nights: Enhanced Edition.\n\n" + "Powered by cleanmodels (Go CLI).")); } void MainWindow::onHelpTriggered() @@ -738,7 +475,8 @@ void MainWindow::updateFileListing() { QFile inputFile(ui->inDirectory->text() % "/" % filePath); QTextStream stream(&inputFile); - inputFile.open(QIODevice::ReadOnly); + if (!inputFile.open(QIODevice::ReadOnly)) + continue; if (!inputFile.isOpen()) continue; auto line = stream.readLine().trimmed().toStdString(); @@ -770,7 +508,6 @@ void MainWindow::onUpdateInDir(const QString& newInDir) m_bFilesHaveChanged = false; m_fsWatcher.addPath(newInDir); ui->inDirectory->setText(newInDir); - replaceUserOption("g_indir", newInDir, true); m_sInDir = newInDir; updateFileListing(); m_dirWatcherTimer->start(); @@ -846,7 +583,6 @@ void MainWindow::on_outdirButton_released() outDirectory = dialog.selectedFiles(); ui->outDirectory->setText(outDirectory.at(0)); m_sOutDir = ui->outDirectory->text(); - replaceUserOption("g_outdir", m_sOutDir, true); } } @@ -863,43 +599,19 @@ void MainWindow::on_outDirectory_editingFinished() const QFileInfo outputDir(m_sOutDir); QDir dir(QDir::currentPath()); QString f = dir.absoluteFilePath(m_sOutDir); - replaceUserOption("g_outdir", m_sOutDir, true); ui->outDirectory->setStatusTip(tr("Output folder resolved as ") % f); } -void MainWindow::on_filePattern_textChanged(const QString &pattern) +void MainWindow::on_filePattern_textChanged(const QString &) { m_dirWatcherTimer->stop(); m_bFilesHaveChanged = false; - replaceUserOption("g_pattern", pattern, true); updateFileListing(); m_dirWatcherTimer->start(); } void MainWindow::on_modelClassCombo_currentIndexChanged(int index) { - switch(index) - { - case 1: - replaceUserOption("classification","character"); - break; - case 2: - replaceUserOption("classification","door"); - break; - case 3: - replaceUserOption("classification","effect"); - break; - case 4: - replaceUserOption("classification","item"); - break; - case 5: - replaceUserOption("classification","tile"); - break; - case 0: - default: - replaceUserOption("classification","automatic"); - break; - } if (!index || index == 5) { if (ui->mainTabs->count() == 1) @@ -916,264 +628,55 @@ void MainWindow::on_modelClassCombo_currentIndexChanged(int index) ui->transparentBitmapKeys->setEnabled(true); } -void MainWindow::on_snapCombo_currentIndexChanged(int index) -{ - switch(index) - { - case 1: - replaceUserOption("snap","binary"); - break; - case 2: - replaceUserOption("snap","decimal"); - break; - case 3: - replaceUserOption("snap","fine"); - break; - case 0: - default: - replaceUserOption("snap","none"); - break; - } -} - -void MainWindow::on_snapTVertsCombo_currentIndexChanged(int index) -{ - switch(index) - { - case 1: - replaceUserOption("tvert_snap","256"); - break; - case 2: - replaceUserOption("tvert_snap","512"); - break; - case 3: - replaceUserOption("tvert_snap","1024"); - break; - case 0: - default: - replaceUserOption("tvert_snap","no"); - break; - } -} - -void MainWindow::on_renderShadowsCombo_currentIndexChanged(int index) -{ - switch(index) - { - case 1: - replaceUserOption("shadow","all"); - break; - case 2: - replaceUserOption("shadow","none"); - break; - case 0: - default: - replaceUserOption("shadow","default"); - break; - } -} +void MainWindow::on_snapCombo_currentIndexChanged(int) {} +void MainWindow::on_snapTVertsCombo_currentIndexChanged(int) {} +void MainWindow::on_renderShadowsCombo_currentIndexChanged(int) {} void MainWindow::on_repivotCombo_currentIndexChanged(int index) { - switch(index) - { - case 1: - replaceUserOption("repivot","all"); - break; - case 2: - replaceUserOption("repivot","none"); - break; - case 0: - default: - replaceUserOption("repivot","if_needed"); - break; - } ui->repivotBox->setEnabled(index <= 1); } void MainWindow::on_allowSplittingCheck_toggled(bool checked) { - replaceUserOption("allow_split", checked ? "yes" : "no"); ui->allowSplittingFrame->setEnabled(checked); } -void MainWindow::on_subObjectSpin_editingFinished() -{ - auto val = ui->subObjectSpin->cleanText(); - replaceUserOption("min_Size", val); -} - -void MainWindow::on_smoothingGroupsCombo_currentIndexChanged(int index) -{ - switch(index) - { - case 1: - replaceUserOption("use_Smoothed","ignore"); - break; - case 2: - replaceUserOption("use_Smoothed","protect"); - break; - case 0: - default: - replaceUserOption("use_Smoothed","use"); - break; - } -} - -void MainWindow::on_splitFirstCombo_currentIndexChanged(int index) -{ - replaceUserOption("split_Priority", index ? "concave" : "convex"); -} - -void MainWindow::on_pivotsBelowZeroZCombo_currentIndexChanged(int index) -{ - switch(index) - { - case 1: - replaceUserOption("'pivots_below_z=0'","allow"); - break; - case 2: - replaceUserOption("'pivots_below_z=0'","slice"); - break; - case 0: - default: - replaceUserOption("'pivots_below_z=0'","disallow"); - break; - } -} - -void MainWindow::on_moveBadPivotsCombo_currentIndexChanged(int index) -{ - switch(index) - { - case 1: - replaceUserOption("move_bad_pivots","top"); - break; - case 2: - replaceUserOption("move_bad_pivots","middle"); - break; - case 3: - replaceUserOption("move_bad_pivots","bottom"); - break; - case 0: - default: - replaceUserOption("move_bad_pivots","no"); - break; - } -} +void MainWindow::on_subObjectSpin_editingFinished() {} +void MainWindow::on_smoothingGroupsCombo_currentIndexChanged(int) {} +void MainWindow::on_splitFirstCombo_currentIndexChanged(int) {} -void MainWindow::on_forceWhiteCheck_toggled(bool checked) -{ - replaceUserOption("force_white", checked ? "yes" : "no"); -} +void MainWindow::on_pivotsBelowZeroZCombo_currentIndexChanged(int) {} +void MainWindow::on_moveBadPivotsCombo_currentIndexChanged(int) {} +void MainWindow::on_forceWhiteCheck_toggled(bool) {} -void MainWindow::on_repairAABBCombo_currentIndexChanged(int index) -{ - switch(index) - { - case 1: - replaceUserOption("fix_overhangs","no"); - break; - case 2: - replaceUserOption("fix_overhangs","interior_only"); - break; - case 0: - default: - replaceUserOption("fix_overhangs","yes"); - break; - } -} +void MainWindow::on_repairAABBCombo_currentIndexChanged(int) {} void MainWindow::on_changeWokMatCheck_toggled(bool checked) { - replaceUserOption("map_aabb_material", checked ? "yes" : "no"); ui->changeWokMatGroupBox->setEnabled(checked); } void MainWindow::on_raiseLowerCombo_currentIndexChanged(int index) { - switch(index) - { - case 1: - replaceUserOption("tile_raise","raise"); - break; - case 2: - replaceUserOption("tile_raise","lower"); - break; - case 0: - default: - replaceUserOption("tile_raise","no"); - break; - } ui->raiseLowerAmountSpin->setEnabled(index >= 1); } -void MainWindow::on_raiseLowerAmountSpin_editingFinished() -{ - auto val = ui->raiseLowerAmountSpin->cleanText(); - replaceUserOption("tile_raise_amount", val); -} +void MainWindow::on_raiseLowerAmountSpin_editingFinished() {} void MainWindow::on_sliceForTileFadeCombo_currentIndexChanged(int index) { - switch(index) - { - case 1: - replaceUserOption("slice","no"); - break; - case 2: - replaceUserOption("slice","undo"); - break; - case 0: - default: - replaceUserOption("slice","yes"); - break; - } ui->sliceHeightFrame->setEnabled(index == 0); - } void MainWindow::on_foliageCombo_currentIndexChanged(int index) { - switch(index) - { - case 1: - replaceUserOption("foliage","tilefade"); - break; - case 2: - replaceUserOption("foliage","animate"); - break; - case 3: - replaceUserOption("foliage","de-animate"); - break; - case 4: - replaceUserOption("foliage","ignore"); - break; - case 0: - default: - replaceUserOption("foliage","no_change"); - break; - } ui->foliageBitmapKeys->setEnabled(index != 4); ui->foliageBitmapKeysLabel->setEnabled(index != 4); - } -void MainWindow::on_groundRotateTextureCombo_currentIndexChanged(int index) +void MainWindow::on_groundRotateTextureCombo_currentIndexChanged(int) { - switch(index) - { - case 1: - replaceUserOption("rotate_ground","1"); - break; - case 2: - replaceUserOption("rotate_ground","0"); - break; - case 0: - default: - replaceUserOption("rotate_ground","no_change"); - break; - } bool showGroundTextEdit = ui->groundRotateTextureCombo->currentIndex() || ui->retileGroundPlanesCombo->currentIndex() || ui->tileEdgeChamfersCombo->currentIndex(); @@ -1181,21 +684,8 @@ void MainWindow::on_groundRotateTextureCombo_currentIndexChanged(int index) ui->groundBitmapKeysLabel->setEnabled(showGroundTextEdit); } -void MainWindow::on_tileEdgeChamfersCombo_currentIndexChanged(int index) +void MainWindow::on_tileEdgeChamfersCombo_currentIndexChanged(int) { - switch(index) - { - case 1: - replaceUserOption("chamfer","add"); - break; - case 2: - replaceUserOption("chamfer","delete"); - break; - case 0: - default: - replaceUserOption("chamfer","no_change"); - break; - } bool showGroundTextEdit = ui->groundRotateTextureCombo->currentIndex() || ui->retileGroundPlanesCombo->currentIndex() || ui->tileEdgeChamfersCombo->currentIndex(); @@ -1203,24 +693,8 @@ void MainWindow::on_tileEdgeChamfersCombo_currentIndexChanged(int index) ui->groundBitmapKeysLabel->setEnabled(showGroundTextEdit); } -void MainWindow::on_retileGroundPlanesCombo_currentIndexChanged(int index) +void MainWindow::on_retileGroundPlanesCombo_currentIndexChanged(int) { - switch(index) - { - case 1: - replaceUserOption("tile_ground","1"); - break; - case 2: - replaceUserOption("tile_ground","2"); - break; - case 3: - replaceUserOption("tile_ground","3"); - break; - case 0: - default: - replaceUserOption("tile_ground","no_change"); - break; - } bool showGroundTextEdit = ui->groundRotateTextureCombo->currentIndex() || ui->retileGroundPlanesCombo->currentIndex() || ui->tileEdgeChamfersCombo->currentIndex(); @@ -1228,176 +702,52 @@ void MainWindow::on_retileGroundPlanesCombo_currentIndexChanged(int index) ui->groundBitmapKeysLabel->setEnabled(showGroundTextEdit); } -void MainWindow::on_meshMergeCheck_toggled(bool checked) -{ - replaceUserOption("merge_by_bitmap", checked ? "yes" : "no"); -} +void MainWindow::on_meshMergeCheck_toggled(bool) {} void MainWindow::on_placeableWithTransparencyCheck_toggled(bool checked) { - if (checked) - { - replaceUserOption("placeable_with_transparency","yes"); - if (ui->modelClassCombo->currentIndex() <= 1) - ui->transBitmapKeyFrame->setEnabled(true); - } + if (checked && ui->modelClassCombo->currentIndex() <= 1) + ui->transBitmapKeyFrame->setEnabled(true); else - { - replaceUserOption("placeable_with_transparency","no"); ui->transBitmapKeyFrame->setEnabled(false); - } } void MainWindow::on_animateSplotchesCheck_toggled(bool checked) { - if (checked) - { - replaceUserOption("splotch","animate"); - ui->splotchBitmapKeysLabel->setEnabled(true); - ui->splotchBitmapKeys->setEnabled(true); - } - else - { - replaceUserOption("splotch","ignore"); - ui->splotchBitmapKeysLabel->setEnabled(false); - ui->splotchBitmapKeys->setEnabled(false); - } -} - -void MainWindow::on_transparentBitmapKeys_editingFinished() -{ - replaceUserOption("transparency_key",ui->transparentBitmapKeys->text().toStdString().c_str()); -} - -void MainWindow::on_cullInvisibleCheck_toggled(bool checked) -{ - replaceUserOption("invisible_mesh_cull", checked ? "yes" : "no"); -} - -void MainWindow::on_renderTrimeshCombo_currentIndexChanged(int index) -{ - switch(index) - { - case 1: - replaceUserOption("render","all"); - break; - case 2: - replaceUserOption("render","none"); - break; - case 0: - default: - replaceUserOption("render","default"); - break; - } + ui->splotchBitmapKeysLabel->setEnabled(checked); + ui->splotchBitmapKeys->setEnabled(checked); } -void MainWindow::on_changeWokMatFromSpin_editingFinished() -{ - auto val = ui->changeWokMatFromSpin->cleanText(); - replaceUserOption("map_aabb_from", val); -} - -void MainWindow::on_changeWokMatToSpin_editingFinished() -{ - auto val = ui->changeWokMatToSpin->cleanText(); - replaceUserOption("map_aabb_to", val); -} +void MainWindow::on_transparentBitmapKeys_editingFinished() {} +void MainWindow::on_cullInvisibleCheck_toggled(bool) {} +void MainWindow::on_renderTrimeshCombo_currentIndexChanged(int) {} +void MainWindow::on_changeWokMatFromSpin_editingFinished() {} +void MainWindow::on_changeWokMatToSpin_editingFinished() {} void MainWindow::on_waterFixupsCheck_toggled(bool checked) { ui->waterFrame->setEnabled(checked); } -void MainWindow::on_waterBitmapKeys_editingFinished() -{ - replaceUserOption("water_key", ui->waterBitmapKeys->text()); -} - -void MainWindow::on_foliageBitmapKeys_editingFinished() -{ - replaceUserOption("foliage_key", ui->foliageBitmapKeys->text()); -} - -void MainWindow::on_splotchBitmapKeys_editingFinished() -{ - replaceUserOption("splotch_key", ui->splotchBitmapKeys->text()); -} - -void MainWindow::on_groundBitmapKeys_editingFinished() -{ - replaceUserOption("ground_key", ui->groundBitmapKeys->text()); -} +void MainWindow::on_waterBitmapKeys_editingFinished() {} +void MainWindow::on_foliageBitmapKeys_editingFinished() {} +void MainWindow::on_splotchBitmapKeys_editingFinished() {} +void MainWindow::on_groundBitmapKeys_editingFinished() {} void MainWindow::on_dynamicWaterCombo_currentIndexChanged(int index) { - switch(index) - { - case 1: - replaceUserOption("dynamic_water","no"); - break; - case 2: - replaceUserOption("dynamic_water","wavy"); - break; - case 0: - default: - replaceUserOption("dynamic_water","yes"); - break; - } ui->waveHeightFrame->setEnabled(index == 2); ui->retileWaterCombo->setEnabled(index != 2); ui->retileWaterLabel->setEnabled(index != 2); } -void MainWindow::on_waveHeightSpin_editingFinished() -{ - auto val = ui->waveHeightSpin->cleanText(); - replaceUserOption("wave_height", val); - -} - -void MainWindow::on_waterRotateTextureCombo_currentIndexChanged(int index) -{ - switch(index) - { - case 1: - replaceUserOption("rotate_water","1"); - break; - case 2: - replaceUserOption("rotate_water","0"); - break; - case 0: - default: - replaceUserOption("rotate_water","no_change"); - break; - } -} - -void MainWindow::on_retileWaterCombo_currentIndexChanged(int index) -{ - switch(index) - { - case 1: - replaceUserOption("tile_water","1"); - break; - case 2: - replaceUserOption("tile_water","2"); - break; - case 3: - replaceUserOption("tile_water","3"); - break; - case 0: - default: - replaceUserOption("tile_water","no_change"); - break; - } -} +void MainWindow::on_waveHeightSpin_editingFinished() {} +void MainWindow::on_waterRotateTextureCombo_currentIndexChanged(int) {} +void MainWindow::on_retileWaterCombo_currentIndexChanged(int) {} void MainWindow::on_filesTable_customContextMenuRequested(const QPoint &pos) { - // Handle global position QPoint globalPos = ui->filesTable->mapToGlobal(pos); - - // Create menu and insert some actions QMenu myMenu; myMenu.addAction(tr("Copy path to clipboard"), this, SLOT(copyToClipboard())); myMenu.exec(globalPos); @@ -1405,24 +755,21 @@ void MainWindow::on_filesTable_customContextMenuRequested(const QPoint &pos) void MainWindow::on_filesTable_doubleClicked(const QModelIndex &index) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) QString cellText = index.siblingAtColumn(0).data().toString(); ui->debugTextBrowser->moveCursor(QTextCursor::Start); if (!ui->debugTextBrowser->find(cellText)) ui->debugTextBrowser->moveCursor(QTextCursor::End); -#endif + + if (m_viewport && !cellText.isEmpty() && !m_sInDir.isEmpty()) + { + QString fullPath = m_sInDir + "/" + cellText; + m_viewport->previewFile(fullPath); + } } void MainWindow::setRescaleOption() { - QString rescaleXYZ = "no"; - if (ui->rescaleXSpin->value() != 1 || ui->rescaleYSpin->value() != 1 || ui->rescaleZSpin->value() != 1) - { - rescaleXYZ = "[" % QString::number(ui->rescaleXSpin->value(),'g',4) % "," % - QString::number(ui->rescaleYSpin->value(),'g',4) % "," % - QString::number(ui->rescaleZSpin->value(),'g',4) % "]"; - } - replaceUserOption("rescaleXYZ",rescaleXYZ); + // Settings are saved at close via saveSettings() } void MainWindow::on_rescaleLockBtn_clicked(bool checked) diff --git a/mainwindow.h b/mainwindow.h index 9c7910f..a2b4f1b 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -12,6 +12,7 @@ #include class FileSystemModel; +class ModelViewport; namespace Ui { class MainWindow; @@ -107,7 +108,7 @@ private slots: QString m_sCurrentModel; QString m_sInDir; QString m_sOutDir; - QString m_sLastDirsPath; + QString m_sSettingsPath; QIcon m_iconReadingMDL; QIcon m_iconDecompilingMDL; QIcon m_iconCleaningMDL; @@ -123,6 +124,7 @@ private slots: QElapsedTimer m_cleanTimer; QFileSystemWatcher m_fsWatcher; QTimer *m_dirWatcherTimer; + QByteArray m_stdoutBuffer; bool m_bFilesHaveChanged; bool m_bUpdateFilesAfterClean; bool m_bCleanRunning; @@ -131,13 +133,17 @@ private slots: void onUpdateInDir(const QString& newInDir); void setRescaleOption(); - void replaceUserOption(const QString& str, const QString& rpl, bool coreValue = false); - void readInLastDirs(const QString& fileLoc); void readSettings(); void writeSettings(); + void saveSettings(); + void loadSettings(); void doClean(); + QStringList buildCliArgs(); int findModelRow(const QString& mdlFile); + void appendDebugHtml(const QString& html); + + ModelViewport *m_viewport = nullptr; }; #endif // MAINWINDOW_H diff --git a/mainwindow_clean.cpp b/mainwindow_clean.cpp index e53b869..ba8feee 100644 --- a/mainwindow_clean.cpp +++ b/mainwindow_clean.cpp @@ -1,146 +1,181 @@ #include "mainwindow.h" #include "ui_mainwindow.h" #include +#include +#include +#include #include #include #include -using namespace std; - - void MainWindow::onCaptureCleanModelsOutput() { - if (m_pCleanProcess) + if (!m_pCleanProcess) + return; + + m_stdoutBuffer.append(m_pCleanProcess->readAllStandardOutput()); + + while (true) { - QString actionVerbPast = tr("Cleaned"); - QString actionVerbPresent = tr("Cleaning"); - QIcon actionIcon = m_iconCleaningMDL; - if (ui->decompileCheck->isChecked()) + int nlPos = m_stdoutBuffer.indexOf('\n'); + if (nlPos < 0) + break; + + QByteArray line = m_stdoutBuffer.left(nlPos).trimmed(); + m_stdoutBuffer.remove(0, nlPos + 1); + + if (line.isEmpty()) + continue; + + QJsonParseError jsonErr; + QJsonDocument doc = QJsonDocument::fromJson(line, &jsonErr); + if (jsonErr.error != QJsonParseError::NoError || !doc.isObject()) { - actionVerbPast = "Decompiled"; - actionVerbPresent = "Decompiling"; - actionIcon = m_iconDecompilingMDL; + appendDebugHtml("" % QString::fromUtf8(line) % "
"); + continue; } - auto outPut = QString::fromStdString(m_pCleanProcess->readAllStandardOutput().toStdString()); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 2)) - QStringList lines = outPut.split( "\n", Qt::SkipEmptyParts ); -#else - QStringList lines = outPut.split( "\n", QString::SkipEmptyParts ); -#endif - foreach( QString line, lines ) + + QJsonObject evt = doc.object(); + QString type = evt["type"].toString(); + QString file = evt["file"].toString(); + + QString actionVerbPast = ui->decompileCheck->isChecked() ? "Decompiled" : "Cleaned"; + QString actionVerbPresent = ui->decompileCheck->isChecked() ? "Decompiling" : "Cleaning"; + QIcon actionIcon = ui->decompileCheck->isChecked() ? m_iconDecompilingMDL : m_iconCleaningMDL; + + if (type == "start") { - if (line == ".") - continue; - QRegExp rx_dot(R"(^((.)\2+)+$)"); - auto pos = rx_dot.indexIn(line); - if (pos > -1) - continue; - QString sStatus; - QString outputHtml; - QRegExp rx_reading("Attempting to read (.*)"); - pos = rx_reading.indexIn(line); - if (pos > -1) - { - m_cleanTimer.start(); - m_sCurrentModel = rx_reading.cap(1).trimmed(); - sStatus = tr("Reading ") % m_sCurrentModel; - m_pCleanStatus->setText(sStatus); - m_pStatusProgress->setVisible(true); - outputHtml = "

" % line % "


"; - ui->debugTextBrowser->insertHtml(outputHtml); - auto sb = ui->debugTextBrowser->verticalScrollBar(); - sb->setValue(sb->maximum()); - auto *twiReadingMDL = new QTableWidgetItem(); - twiReadingMDL->setIcon(m_iconReadingMDL); - twiReadingMDL->setToolTip("Reading"); - twiReadingMDL->setText(tr("Reading")); - ui->filesTable->setItem(findModelRow(m_sCurrentModel), 2, twiReadingMDL); - ui->filesTable->scrollToItem(twiReadingMDL); - continue; - } - QRegExp rx_mdl("MDL\\s(.*)\\sloaded."); - pos = rx_mdl.indexIn(line); - if (pos > -1) + m_cleanTimer.start(); + m_sCurrentModel = file; + m_pCleanStatus->setText(tr("Processing ") % file); + m_pStatusProgress->setVisible(true); + + int idx = evt["index"].toInt(); + int total = evt["total"].toInt(); + if (total > 0) { - sStatus = tr(actionVerbPresent.toStdString().c_str()) % " " % m_sCurrentModel; - m_pCleanStatus->setText(sStatus); - m_pStatusProgress->setVisible(true); - outputHtml = "

" % line % "


"; - ui->debugTextBrowser->insertHtml(outputHtml); - auto sb = ui->debugTextBrowser->verticalScrollBar(); - sb->setValue(sb->maximum()); - auto *twiCleaningMDL = new QTableWidgetItem(); - twiCleaningMDL->setText(tr(actionVerbPresent.toStdString().c_str())); - twiCleaningMDL->setIcon(actionIcon); - twiCleaningMDL->setToolTip(tr(actionVerbPresent.toStdString().c_str())); - ui->filesTable->setItem(findModelRow(m_sCurrentModel), 2, twiCleaningMDL); - continue; + m_pStatusProgress->setRange(0, total); + m_pStatusProgress->setValue(idx); } - QRegExp rx_bin("Binary file (.*) detected, attempting import."); - pos = rx_bin.indexIn(line); - if (pos > -1) + + appendDebugHtml("

Processing " % file % "


"); + + int row = findModelRow(file); + if (row >= 0) { - sStatus = tr("Decompiling ") % rx_bin.cap(1); - m_pStatusProgress->setVisible(true); - m_pCleanStatus->setText(sStatus); + auto *item = new QTableWidgetItem(); + item->setIcon(m_iconReadingMDL); + item->setToolTip(actionVerbPresent); + item->setText(actionVerbPresent); + ui->filesTable->setItem(row, 2, item); + ui->filesTable->scrollToItem(item); } - QRegExp rx_fixes(R"(Fixes made = (\d+))"); - pos = rx_fixes.indexIn(line); - if (pos > -1) + } + else if (type == "done") + { + m_nMdlsCleaned++; + ui->mdlsCleanedLabel->setText("Files " % actionVerbPast % ": " % QString::number(m_nMdlsCleaned)); + + int fixes = evt["fixes"].toInt(); + QString elapsedStr = QTime(0,0).addMSecs(m_cleanTimer.elapsed()).toString("mm:ss.zzz"); + + appendDebugHtml("

" % file % " " % actionVerbPast.toLower() % " (" % QString::number(fixes) % " fixes)


"); + + int row = findModelRow(file); + if (row >= 0) { - auto *fixesItem = new QTableWidgetItem(rx_fixes.cap(1)); + auto *statusItem = new QTableWidgetItem(); + statusItem->setText(actionVerbPast); + statusItem->setIcon(m_iconCleanSuccess); + statusItem->setToolTip(actionVerbPast); + ui->filesTable->setItem(row, 2, statusItem); + + auto *fixesItem = new QTableWidgetItem(QString::number(fixes)); fixesItem->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); - ui->filesTable->setItem(findModelRow(m_sCurrentModel), 3, fixesItem); - } - QRegExp rx_written(R"((.*) written.)"); - pos = rx_written.indexIn(line); - if (pos > -1) - { - m_nMdlsCleaned++; - ui->mdlsCleanedLabel->setText("Files " % actionVerbPast % ": " % QString::number(m_nMdlsCleaned)); - outputHtml = "

" % line % "


"; - ui->debugTextBrowser->insertHtml(outputHtml); - auto sb = ui->debugTextBrowser->verticalScrollBar(); - sb->setValue(sb->maximum()); - auto *twiCleanSuccess = new QTableWidgetItem(); - twiCleanSuccess->setText(tr(actionVerbPast.toStdString().c_str())); - twiCleanSuccess->setIcon(m_iconCleanSuccess); - twiCleanSuccess->setToolTip(tr(actionVerbPast.toStdString().c_str())); - auto *twiCleanTimer = new QTableWidgetItem(); - twiCleanTimer->setText(QTime(0,0).addMSecs(m_cleanTimer.elapsed()).toString("mm:ss.zzz")); - twiCleanTimer->setTextAlignment(Qt::AlignCenter); - ui->filesTable->setItem(findModelRow(m_sCurrentModel), 2, twiCleanSuccess); - ui->filesTable->setItem(findModelRow(m_sCurrentModel), 4, twiCleanTimer); - continue; - } - QRegExp rx_error(R"(\*\*\* Cannot(.*)|\*\* Load failed(.*))"); - pos = rx_error.indexIn(line); - if (pos > -1) - { - m_nMdlsFailed++; - ui->mdlsFailedLabel->setText("Failures: " % QString::number(m_nMdlsFailed)); - outputHtml = "

" % line % "


"; - auto *twiCleanError = new QTableWidgetItem(); - twiCleanError->setText(tr("Failed")); - twiCleanError->setIcon(m_iconCleanError); - twiCleanError->setToolTip(tr("Failed")); - auto *twiCleanTimer = new QTableWidgetItem(); - twiCleanTimer->setText(QTime(0,0).addMSecs(m_cleanTimer.elapsed()).toString("mm:ss.zzz")); - twiCleanTimer->setTextAlignment(Qt::AlignCenter); - ui->filesTable->setItem(findModelRow(m_sCurrentModel), 2, twiCleanError); - ui->filesTable->setItem(findModelRow(m_sCurrentModel), 4, twiCleanTimer); + ui->filesTable->setItem(row, 3, fixesItem); + + auto *timerItem = new QTableWidgetItem(elapsedStr); + timerItem->setTextAlignment(Qt::AlignCenter); + ui->filesTable->setItem(row, 4, timerItem); } - else + } + else if (type == "error") + { + m_nMdlsFailed++; + ui->mdlsFailedLabel->setText("Failures: " % QString::number(m_nMdlsFailed)); + + QString msg = evt["message"].toString(); + QString elapsedStr = QTime(0,0).addMSecs(m_cleanTimer.elapsed()).toString("mm:ss.zzz"); + + appendDebugHtml("

" % file % ": " % msg.toHtmlEscaped() % "


"); + + int row = findModelRow(file); + if (row >= 0) { - outputHtml = "" % line % "
"; + auto *item = new QTableWidgetItem(); + item->setText(tr("Failed")); + item->setIcon(m_iconCleanError); + item->setToolTip(msg); + ui->filesTable->setItem(row, 2, item); + + auto *timerItem = new QTableWidgetItem(elapsedStr); + timerItem->setTextAlignment(Qt::AlignCenter); + ui->filesTable->setItem(row, 4, timerItem); } - ui->debugTextBrowser->insertHtml(outputHtml); - auto sb = ui->debugTextBrowser->verticalScrollBar(); - sb->setValue(sb->maximum()); + } + else if (type == "summary") + { + QString msg = evt["message"].toString(); + appendDebugHtml("

" % msg.toHtmlEscaped() % "


"); } } } +QStringList MainWindow::buildCliArgs() +{ + QStringList args; + args << "--json-lines"; + + if (ui->decompileCheck->isChecked()) + { + args << "--decompile-only"; + } + else + { + args << "--check"; + + if (ui->rescaleXSpin->value() != 1 || ui->rescaleYSpin->value() != 1 || ui->rescaleZSpin->value() != 1) + { + double avg = (ui->rescaleXSpin->value() + ui->rescaleYSpin->value() + ui->rescaleZSpin->value()) / 3.0; + args << "--scale" << QString::number(avg, 'g', 6); + } + + if (ui->repairAABBCombo->currentIndex() == 1 || ui->repairAABBCombo->currentIndex() == 2) + args << "--fix-aabb"; + + if (ui->repivotCombo->currentIndex() == 1) + args << "--fix-pivots"; + + if (ui->sliceForTileFadeCombo->currentIndex() == 0) + { + args << "--fix-tilefade"; + } + + args << "--strip-degenerate"; + args << "--fix-animations"; + args << "--reparent-children"; + args << "--wrap-root"; + args << "--split-multiedge"; + } + + args << m_sInDir; + + if (!m_sOutDir.isEmpty() && m_sOutDir != m_sInDir) + args << m_sOutDir; + + return args; +} + void MainWindow::doClean() { if (m_bCleanRunning) @@ -152,22 +187,27 @@ void MainWindow::doClean() twiCleanAborted->setText(tr("Aborted")); twiCleanAborted->setIcon(m_iconAbortButton); twiCleanAborted->setToolTip(tr("Aborted")); - ui->filesTable->setItem(findModelRow(m_sCurrentModel), 2, twiCleanAborted); + int row = findModelRow(m_sCurrentModel); + if (row >= 0) + ui->filesTable->setItem(row, 2, twiCleanAborted); return; } - connect(m_pCleanProcess, SIGNAL(readyReadStandardOutput()), this, SLOT(onCaptureCleanModelsOutput())); + + connect(m_pCleanProcess, &QProcess::readyReadStandardOutput, + this, &MainWindow::onCaptureCleanModelsOutput); + + m_stdoutBuffer.clear(); ui->debugTextBrowser->clear(); ui->debugTextBrowser->insertHtml(tr("Running cleanmodels
")); - QStringList args; - m_pCleanProcess->setCurrentWriteChannel(QProcess::StandardOutput); + + QStringList args = buildCliArgs(); + + appendDebugHtml("$ " % m_sBinaryPath % " " % args.join(" ") % "
"); + m_pCleanProcess->setWorkingDirectory(QDir::currentPath()); - QString filePattern = ui->filePattern->text(); - if (ui->decompileCheck->isChecked()) - args<<"-d"; - else - args<<"last_dirs.pl"; - m_pCleanProcess->start(m_sBinaryPath,args,QIODevice::ReadWrite); + m_pCleanProcess->start(m_sBinaryPath, args, QIODevice::ReadOnly); ui->cleanButton->setDisabled(true); + if (m_pCleanProcess->waitForStarted()) { ui->decompileCheck->setEnabled(false); @@ -179,11 +219,13 @@ void MainWindow::doClean() ui->cleanButton->setDisabled(false); ui->cleanButton->setText(tr("Abort")); ui->cleanButton->setIcon(m_iconAbortButton); - m_pCleanProcess->moveToThread(QCoreApplication::instance()->thread()); + m_pStatusProgress->setRange(0, 0); + m_pStatusProgress->setVisible(true); } else { - QString errorMsg = "

Failed to run clean! Does the " % m_sBinaryName % " executable exist in the working directory or your PATH?


" % m_sBinaryPath; + QString errorMsg = "

Failed to run cleanmodels! Does the " % + m_sBinaryName % " executable exist in the working directory or your PATH?


" % m_sBinaryPath; ui->debugTextBrowser->insertHtml(tr(errorMsg.toStdString().c_str())); auto sb = ui->debugTextBrowser->verticalScrollBar(); sb->setValue(sb->maximum()); @@ -193,8 +235,11 @@ void MainWindow::doClean() void MainWindow::onCleanFinished(int, QProcess::ExitStatus) { + onCaptureCleanModelsOutput(); + ui->debugTextBrowser->append(m_pCleanProcess->readAllStandardError()); m_bCleanRunning = false; + if (!ui->decompileCheck->isChecked()) ui->cleanButton->setText(tr("Clean")); else @@ -203,7 +248,8 @@ void MainWindow::onCleanFinished(int, QProcess::ExitStatus) ui->cleanButton->setIcon(m_iconCleanButton); m_pCleanStatus->setText(tr("Idle")); m_pStatusProgress->setVisible(false); - if(m_bUpdateFilesAfterClean) + + if (m_bUpdateFilesAfterClean) { MainWindow::updateFileListing(); m_bUpdateFilesAfterClean = false; @@ -213,14 +259,17 @@ void MainWindow::onCleanFinished(int, QProcess::ExitStatus) int MainWindow::findModelRow(const QString& mdlFile) { int rows = ui->filesTable->rowCount(); - int foundRow = 0; // fall back to first row if we can't find it for some reason for (int i = 0; i < rows; ++i) { if (ui->filesTable->item(i, 0)->text() == mdlFile) - { - foundRow = i; - break; - } + return i; } - return foundRow; + return -1; +} + +void MainWindow::appendDebugHtml(const QString& html) +{ + ui->debugTextBrowser->insertHtml(html); + auto sb = ui->debugTextBrowser->verticalScrollBar(); + sb->setValue(sb->maximum()); } diff --git a/mdlscene.cpp b/mdlscene.cpp new file mode 100644 index 0000000..96e5dec --- /dev/null +++ b/mdlscene.cpp @@ -0,0 +1,261 @@ +#include "mdlscene.h" +#include +#include +#include +#include + +bool MdlNode::hasMesh() const +{ + return !verts.isEmpty() && !faces.isEmpty() && + (nodeType == "trimesh" || nodeType == "skin" || + nodeType == "danglymesh" || nodeType == "animmesh" || + nodeType == "aabb"); +} + +bool MdlScene::loadFromString(const QString &ascii) +{ + m_nodes.clear(); + QStringList lines = ascii.split('\n'); + int pos = 0; + while (pos < lines.size()) + { + QString line = lines[pos].trimmed(); + if (line.startsWith("node ")) + { + parseNodeBlock(lines, pos); + } + else if (line.startsWith("endmodelgeom")) + { + break; + } + else + { + pos++; + } + } + return !m_nodes.isEmpty(); +} + +static QVector3D parseVec3(const QStringList &tokens, int offset = 0) +{ + if (tokens.size() < offset + 3) + return {}; + return {tokens[offset].toFloat(), tokens[offset + 1].toFloat(), tokens[offset + 2].toFloat()}; +} + +void MdlScene::parseNodeBlock(const QStringList &lines, int &pos) +{ + MdlNode node; + QStringList header = lines[pos].trimmed().split(QRegularExpression("\\s+")); + if (header.size() >= 3) + { + node.nodeType = header[1].toLower(); + node.name = header[2]; + } + pos++; + + enum class ArrayMode { None, Verts, Faces, TVerts, Normals }; + ArrayMode mode = ArrayMode::None; + int remaining = 0; + + while (pos < lines.size()) + { + QString line = lines[pos].trimmed(); + pos++; + + if (line.isEmpty() || line.startsWith('#')) + continue; + + if (line == "endnode") + break; + + if (line.startsWith("node ")) + { + pos--; + parseNodeBlock(lines, pos); + continue; + } + + if (mode != ArrayMode::None && remaining > 0) + { + QStringList tokens = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + switch (mode) + { + case ArrayMode::Verts: + node.verts.append(parseVec3(tokens)); + break; + case ArrayMode::Normals: + node.normals.append(parseVec3(tokens)); + break; + case ArrayMode::TVerts: + node.tverts.append(parseVec3(tokens)); + break; + case ArrayMode::Faces: { + if (tokens.size() >= 8) + { + MdlFace f; + f.verts[0] = tokens[0].toInt(); + f.verts[1] = tokens[1].toInt(); + f.verts[2] = tokens[2].toInt(); + f.smoothGroup = tokens[3].toInt(); + f.uvs[0] = tokens[4].toInt(); + f.uvs[1] = tokens[5].toInt(); + f.uvs[2] = tokens[6].toInt(); + f.material = tokens[7].toInt(); + node.faces.append(f); + } + break; + } + default: + break; + } + remaining--; + if (remaining <= 0) + mode = ArrayMode::None; + continue; + } + + QStringList tokens = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + if (tokens.isEmpty()) + continue; + + QString key = tokens[0].toLower(); + + if (key == "parent" && tokens.size() >= 2) + { + node.parent = tokens[1]; + } + else if (key == "position" && tokens.size() >= 4) + { + node.position = parseVec3(tokens, 1); + } + else if (key == "orientation" && tokens.size() >= 5) + { + float x = tokens[1].toFloat(); + float y = tokens[2].toFloat(); + float z = tokens[3].toFloat(); + float w = tokens[4].toFloat(); + node.orientation = QQuaternion::fromAxisAndAngle(QVector3D(x, y, z), qRadiansToDegrees(w)); + } + else if (key == "scale" && tokens.size() >= 2) + { + node.scale = tokens[1].toFloat(); + } + else if (key == "ambient" && tokens.size() >= 4) + { + node.ambient = parseVec3(tokens, 1); + } + else if (key == "diffuse" && tokens.size() >= 4) + { + node.diffuse = parseVec3(tokens, 1); + } + else if (key == "specular" && tokens.size() >= 4) + { + node.specular = parseVec3(tokens, 1); + } + else if (key == "shininess" && tokens.size() >= 2) + { + node.shininess = tokens[1].toFloat(); + } + else if (key == "bitmap" && tokens.size() >= 2) + { + node.bitmap = tokens[1]; + } + else if (key == "render" && tokens.size() >= 2) + { + node.render = tokens[1].toInt(); + } + else if (key == "verts" && tokens.size() >= 2) + { + int count = tokens[1].toInt(); + if (count > 0) + { + mode = ArrayMode::Verts; + remaining = count; + node.verts.reserve(count); + } + } + else if (key == "faces" && tokens.size() >= 2) + { + int count = tokens[1].toInt(); + if (count > 0) + { + mode = ArrayMode::Faces; + remaining = count; + node.faces.reserve(count); + } + } + else if (key == "tverts" && tokens.size() >= 2) + { + int count = tokens[1].toInt(); + if (count > 0) + { + mode = ArrayMode::TVerts; + remaining = count; + node.tverts.reserve(count); + } + } + else if (key == "normals" && tokens.size() >= 2) + { + int count = tokens[1].toInt(); + if (count > 0) + { + mode = ArrayMode::Normals; + remaining = count; + node.normals.reserve(count); + } + } + } + + m_nodes.append(node); +} + +int MdlScene::rootIndex() const +{ + for (int i = 0; i < m_nodes.size(); ++i) + { + if (m_nodes[i].parent.toLower() == "null" || m_nodes[i].parent.isEmpty()) + return i; + } + return m_nodes.isEmpty() ? -1 : 0; +} + +QVector MdlScene::childrenOf(int idx) const +{ + QVector out; + if (idx < 0 || idx >= m_nodes.size()) + return out; + const QString &name = m_nodes[idx].name; + for (int i = 0; i < m_nodes.size(); ++i) + { + if (i != idx && m_nodes[i].parent == name) + out.append(i); + } + return out; +} + +void MdlScene::computeBounds(QVector3D &bmin, QVector3D &bmax) const +{ + bmin = QVector3D(FLT_MAX, FLT_MAX, FLT_MAX); + bmax = QVector3D(-FLT_MAX, -FLT_MAX, -FLT_MAX); + bool any = false; + for (const auto &node : m_nodes) + { + for (const auto &v : node.verts) + { + QVector3D world = v + node.position; + bmin.setX(std::min(bmin.x(), world.x())); + bmin.setY(std::min(bmin.y(), world.y())); + bmin.setZ(std::min(bmin.z(), world.z())); + bmax.setX(std::max(bmax.x(), world.x())); + bmax.setY(std::max(bmax.y(), world.y())); + bmax.setZ(std::max(bmax.z(), world.z())); + any = true; + } + } + if (!any) + { + bmin = QVector3D(-1, -1, -1); + bmax = QVector3D(1, 1, 1); + } +} diff --git a/mdlscene.h b/mdlscene.h new file mode 100644 index 0000000..40643dc --- /dev/null +++ b/mdlscene.h @@ -0,0 +1,58 @@ +#ifndef MDLSCENE_H +#define MDLSCENE_H + +#include +#include +#include +#include +#include + +struct MdlFace { + int32_t verts[3]; + int32_t uvs[3]; + int32_t smoothGroup; + int32_t material; +}; + +struct MdlNode { + QString name; + QString parent; + QString nodeType; + + QVector3D position; + QQuaternion orientation; + float scale = 1.0f; + + QVector3D ambient{0.2f, 0.2f, 0.2f}; + QVector3D diffuse{0.8f, 0.8f, 0.8f}; + QVector3D specular{0.0f, 0.0f, 0.0f}; + float shininess = 1.0f; + QString bitmap; + int render = 1; + + QVector verts; + QVector normals; + QVector tverts; + QVector faces; + + bool hasMesh() const; +}; + +class MdlScene +{ +public: + bool loadFromString(const QString &ascii); + + const QVector &nodes() const { return m_nodes; } + int rootIndex() const; + QVector childrenOf(int idx) const; + + void computeBounds(QVector3D &bmin, QVector3D &bmax) const; + +private: + QVector m_nodes; + + void parseNodeBlock(const QStringList &lines, int &pos); +}; + +#endif diff --git a/modelviewport.cpp b/modelviewport.cpp new file mode 100644 index 0000000..10983c7 --- /dev/null +++ b/modelviewport.cpp @@ -0,0 +1,233 @@ +#include "modelviewport.h" +#include +#include +#include + +ModelViewport::ModelViewport(QWidget *parent) + : QOpenGLWidget(parent) +{ + QSurfaceFormat fmt; + fmt.setVersion(3, 3); + fmt.setProfile(QSurfaceFormat::CoreProfile); + fmt.setDepthBufferSize(24); + fmt.setSamples(4); + setFormat(fmt); + + setMouseTracking(true); + setFocusPolicy(Qt::StrongFocus); +} + +ModelViewport::~ModelViewport() +{ + if (!m_initialized) + return; + makeCurrent(); + m_renderer.shutdown(this); + doneCurrent(); +} + +void ModelViewport::setCliBinaryPath(const QString &path) +{ + m_cliBinaryPath = path; +} + +void ModelViewport::initializeGL() +{ + if (!initializeOpenGLFunctions()) { + qWarning() << "VIEWPORT: Failed to initialize OpenGL 3.3 Core functions"; + return; + } + + const char *version = reinterpret_cast(glGetString(GL_VERSION)); + const char *renderer = reinterpret_cast(glGetString(GL_RENDERER)); + qDebug() << "VIEWPORT: OpenGL version:" << (version ? version : "null"); + qDebug() << "VIEWPORT: OpenGL renderer:" << (renderer ? renderer : "null"); + + m_renderer.initialize(this); + m_initialized = true; + + // resizeGL runs before initializeGL, so the viewport was never set + glViewport(0, 0, width(), height()); + m_camera.setAspectRatio(static_cast(width()) / static_cast(std::max(height(), 1))); + + qDebug() << "VIEWPORT: Initialized successfully, program:" << m_renderer.program(); +} + +void ModelViewport::resizeGL(int w, int h) +{ + if (h == 0) h = 1; + m_camera.setAspectRatio(static_cast(w) / static_cast(h)); + if (m_initialized) + glViewport(0, 0, w, h); +} + +void ModelViewport::paintGL() +{ + if (!m_initialized) { + qDebug() << "VIEWPORT: paintGL called but not initialized"; + return; + } + m_renderer.render(this, m_camera); + + GLenum err = glGetError(); + if (err != GL_NO_ERROR) + qDebug() << "VIEWPORT: GL error after render:" << Qt::hex << err; + + if (m_hasModel) { + static int paintCount = 0; + if (paintCount < 3) { + qDebug() << "VIEWPORT: paintGL with model, nodes:" << m_renderer.renderNodeCount() + << "fbo:" << defaultFramebufferObject() + << "size:" << size(); + paintCount++; + } + } +} + +static bool fileIsBinary(const QString &path) +{ + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) + return false; + QByteArray hdr = f.read(4); + f.close(); + return hdr.size() == 4 && hdr[0] == 0 && hdr[1] == 0 && hdr[2] == 0 && hdr[3] == 0; +} + +void ModelViewport::previewFile(const QString &mdlPath) +{ + if (!m_initialized) { + emit previewError("Viewport not initialized"); + return; + } + + QString ascii; + + if (fileIsBinary(mdlPath)) + { + if (m_cliBinaryPath.isEmpty()) { + emit previewError("CLI binary path not set"); + return; + } + + QProcess proc; + proc.setProgram(m_cliBinaryPath); + proc.setArguments({"--decompile-only", mdlPath}); + proc.start(QIODevice::ReadOnly); + + if (!proc.waitForFinished(10000)) { + emit previewError("CLI timed out decompiling " + mdlPath); + return; + } + + if (proc.exitCode() != 0) { + emit previewError("CLI error: " + proc.readAllStandardError()); + return; + } + + ascii = QString::fromUtf8(proc.readAllStandardOutput()); + } + else + { + QFile f(mdlPath); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + emit previewError("Cannot open " + mdlPath); + return; + } + ascii = QString::fromUtf8(f.readAll()); + f.close(); + } + + if (ascii.isEmpty()) { + emit previewError("Empty MDL content for " + mdlPath); + return; + } + + loadModel(ascii, QFileInfo(mdlPath).absolutePath()); +} + +void ModelViewport::loadModel(const QString &asciiMdl, const QString &textureDir) +{ + if (!m_initialized) + return; + + MdlScene scene; + if (!scene.loadFromString(asciiMdl)) { + emit previewError("Failed to parse MDL scene"); + return; + } + + qDebug() << "VIEWPORT: loadModel called, nodes:" << scene.nodes().size() + << "root:" << scene.rootIndex(); + + makeCurrent(); + m_renderer.prepareScene(this, scene, textureDir); + doneCurrent(); + + qDebug() << "VIEWPORT: prepareScene done, render nodes:" << m_renderer.renderNodeCount(); + + QVector3D bmin, bmax; + scene.computeBounds(bmin, bmax); + m_camera.focusOnBounds(bmin, bmax); + + qDebug() << "VIEWPORT: bounds min:" << bmin << "max:" << bmax + << "camera pos:" << m_camera.position(); + + m_hasModel = true; + update(); + emit modelLoaded("model"); +} + +void ModelViewport::clearModel() +{ + if (!m_initialized) + return; + makeCurrent(); + m_renderer.prepareScene(this, MdlScene()); + doneCurrent(); + m_hasModel = false; + update(); +} + +void ModelViewport::mousePressEvent(QMouseEvent *event) +{ + m_lastMousePos = event->position(); + m_pressedButtons = event->buttons(); + event->accept(); +} + +void ModelViewport::mouseMoveEvent(QMouseEvent *event) +{ + QPointF delta = event->position() - m_lastMousePos; + m_lastMousePos = event->position(); + + if (m_pressedButtons & Qt::MiddleButton) { + if (event->modifiers() & Qt::ShiftModifier) + m_camera.pan(static_cast(delta.x()), static_cast(delta.y())); + else + m_camera.rotate(static_cast(delta.x()), static_cast(delta.y())); + update(); + } else if (m_pressedButtons & Qt::RightButton) { + m_camera.pan(static_cast(delta.x()), static_cast(delta.y())); + update(); + } else if (m_pressedButtons & Qt::LeftButton) { + m_camera.rotate(static_cast(delta.x()), static_cast(delta.y())); + update(); + } + + event->accept(); +} + +void ModelViewport::mouseReleaseEvent(QMouseEvent *event) +{ + m_pressedButtons = event->buttons(); + event->accept(); +} + +void ModelViewport::wheelEvent(QWheelEvent *event) +{ + float delta = static_cast(event->angleDelta().y()) / 120.0f; + m_camera.zoom(delta); + update(); + event->accept(); +} diff --git a/modelviewport.h b/modelviewport.h new file mode 100644 index 0000000..9748a39 --- /dev/null +++ b/modelviewport.h @@ -0,0 +1,51 @@ +#ifndef MODELVIEWPORT_H +#define MODELVIEWPORT_H + +#include "camera.h" +#include "renderer.h" +#include +#include +#include +#include +#include +#include + +class ModelViewport : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core +{ + Q_OBJECT + +public: + explicit ModelViewport(QWidget *parent = nullptr); + ~ModelViewport() override; + + void setCliBinaryPath(const QString &path); + void previewFile(const QString &mdlPath); + void loadModel(const QString &asciiMdl, const QString &textureDir = QString()); + void clearModel(); + +signals: + void modelLoaded(const QString &name); + void previewError(const QString &msg); + +protected: + void initializeGL() override; + void resizeGL(int w, int h) override; + void paintGL() override; + + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + +private: + Renderer m_renderer; + Camera m_camera; + QString m_cliBinaryPath; + bool m_initialized = false; + bool m_hasModel = false; + + QPointF m_lastMousePos; + Qt::MouseButtons m_pressedButtons; +}; + +#endif diff --git a/renderer.cpp b/renderer.cpp new file mode 100644 index 0000000..9abb587 --- /dev/null +++ b/renderer.cpp @@ -0,0 +1,382 @@ +#include "renderer.h" +#include +#include +#include + +static const char *kVertexShader = R"glsl( +#version 330 core +layout(location=0) in vec3 aPos; +layout(location=1) in vec3 aNormal; +layout(location=2) in vec2 aUV; + +uniform mat4 uModel, uView, uProjection; +uniform mat3 uNormalMatrix; + +out vec3 vNormal, vWorldPos; +out vec2 vUV; + +void main() { + vec4 wp = uModel * vec4(aPos, 1.0); + vWorldPos = wp.xyz; + vNormal = normalize(uNormalMatrix * aNormal); + vUV = aUV; + gl_Position = uProjection * uView * wp; +} +)glsl"; + +static const char *kFragmentShader = R"glsl( +#version 330 core +in vec3 vNormal, vWorldPos; +in vec2 vUV; + +uniform vec3 uDiffuse, uAmbient, uSpecular; +uniform float uShininess; +uniform vec3 uLightDir, uLightColor, uViewPos; +uniform sampler2D uTexture; + +out vec4 FragColor; + +void main() { + vec3 normal = normalize(vNormal); + if (!gl_FrontFacing) normal = -normal; + + // Ambient + vec3 ambient = uAmbient * uLightColor * 0.3; + + // Diffuse (Lambert) + float diff = max(dot(normal, -uLightDir), 0.0); + vec3 diffuse = diff * uDiffuse * uLightColor; + + // Specular (Blinn-Phong) + vec3 viewDir = normalize(uViewPos - vWorldPos); + vec3 halfDir = normalize(-uLightDir + viewDir); + float spec = pow(max(dot(normal, halfDir), 0.0), max(uShininess, 1.0)); + vec3 specular = spec * uSpecular * uLightColor; + + vec3 result = ambient + diffuse + specular; + + // Texture as color map, multiplied onto lit result + vec4 baseColor = texture(uTexture, vUV); + FragColor = vec4(result * baseColor.rgb, baseColor.a); +} +)glsl"; + +GLuint Renderer::compileShader(QOpenGLFunctions_3_3_Core *gl, GLenum type, const char *src) +{ + GLuint shader = gl->glCreateShader(type); + gl->glShaderSource(shader, 1, &src, nullptr); + gl->glCompileShader(shader); + + GLint ok = 0; + gl->glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); + if (!ok) + { + char log[1024]; + gl->glGetShaderInfoLog(shader, sizeof(log), nullptr, log); + qWarning() << "Shader compile error:" << log; + gl->glDeleteShader(shader); + return 0; + } + return shader; +} + +GLuint Renderer::linkProgram(QOpenGLFunctions_3_3_Core *gl, GLuint vert, GLuint frag) +{ + GLuint prog = gl->glCreateProgram(); + gl->glAttachShader(prog, vert); + gl->glAttachShader(prog, frag); + gl->glLinkProgram(prog); + + GLint ok = 0; + gl->glGetProgramiv(prog, GL_LINK_STATUS, &ok); + if (!ok) + { + char log[1024]; + gl->glGetProgramInfoLog(prog, sizeof(log), nullptr, log); + qWarning() << "Program link error:" << log; + gl->glDeleteProgram(prog); + return 0; + } + gl->glDeleteShader(vert); + gl->glDeleteShader(frag); + return prog; +} + +void Renderer::initialize(QOpenGLFunctions_3_3_Core *gl) +{ + GLuint vs = compileShader(gl, GL_VERTEX_SHADER, kVertexShader); + GLuint fs = compileShader(gl, GL_FRAGMENT_SHADER, kFragmentShader); + if (vs && fs) + m_program = linkProgram(gl, vs, fs); + + if (m_program) + { + m_locModel = gl->glGetUniformLocation(m_program, "uModel"); + m_locView = gl->glGetUniformLocation(m_program, "uView"); + m_locProjection = gl->glGetUniformLocation(m_program, "uProjection"); + m_locNormalMatrix = gl->glGetUniformLocation(m_program, "uNormalMatrix"); + m_locDiffuse = gl->glGetUniformLocation(m_program, "uDiffuse"); + m_locAmbient = gl->glGetUniformLocation(m_program, "uAmbient"); + m_locSpecular = gl->glGetUniformLocation(m_program, "uSpecular"); + m_locShininess = gl->glGetUniformLocation(m_program, "uShininess"); + m_locLightDir = gl->glGetUniformLocation(m_program, "uLightDir"); + m_locLightColor = gl->glGetUniformLocation(m_program, "uLightColor"); + m_locViewPos = gl->glGetUniformLocation(m_program, "uViewPos"); + m_locTexture = gl->glGetUniformLocation(m_program, "uTexture"); + } + + // 1x1 white fallback so the sampler is always valid + unsigned char white[] = {255, 255, 255, 255}; + gl->glGenTextures(1, &m_whiteTex); + gl->glBindTexture(GL_TEXTURE_2D, m_whiteTex); + gl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, + GL_RGBA, GL_UNSIGNED_BYTE, white); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl->glBindTexture(GL_TEXTURE_2D, 0); +} + +void Renderer::shutdown(QOpenGLFunctions_3_3_Core *gl) +{ + for (auto &rn : m_renderNodes) { + if (rn.mesh) rn.mesh->destroy(gl); + if (rn.texture) rn.texture->destroy(gl); + } + m_renderNodes.clear(); + + if (m_whiteTex) { + gl->glDeleteTextures(1, &m_whiteTex); + m_whiteTex = 0; + } + + if (m_program) + { + gl->glDeleteProgram(m_program); + m_program = 0; + } +} + +void Renderer::prepareScene(QOpenGLFunctions_3_3_Core *gl, const MdlScene &scene, + const QString &textureDir) +{ + for (auto &rn : m_renderNodes) { + if (rn.mesh) rn.mesh->destroy(gl); + if (rn.texture) rn.texture->destroy(gl); + } + m_renderNodes.clear(); + m_textureDir = textureDir; + + int root = scene.rootIndex(); + if (root < 0) + return; + + QMatrix4x4 identity; + buildRenderNodes(gl, scene, root, identity); +} + +void Renderer::buildRenderNodes(QOpenGLFunctions_3_3_Core *gl, const MdlScene &scene, + int nodeIdx, const QMatrix4x4 &parentWorld) +{ + const MdlNode &node = scene.nodes()[nodeIdx]; + + QMatrix4x4 local; + local.translate(node.position); + local.rotate(node.orientation); + if (node.scale != 1.0f) + local.scale(node.scale); + + QMatrix4x4 world = parentWorld * local; + + if (node.hasMesh() && node.render) + uploadNodeMesh(gl, node, world); + + for (int childIdx : scene.childrenOf(nodeIdx)) + buildRenderNodes(gl, scene, childIdx, world); +} + +void Renderer::uploadNodeMesh(QOpenGLFunctions_3_3_Core *gl, const MdlNode &node, + const QMatrix4x4 &worldTransform) +{ + QVector vertices; + QVector indices; + + bool hasNormals = (node.normals.size() == node.verts.size()); + + // Compute smooth vertex normals by averaging face normals per vertex + QVector smoothNormals; + if (!hasNormals && !node.verts.isEmpty()) { + smoothNormals.resize(node.verts.size(), QVector3D(0, 0, 0)); + for (const auto &face : node.faces) { + if (face.verts[0] >= node.verts.size() || + face.verts[1] >= node.verts.size() || + face.verts[2] >= node.verts.size()) + continue; + + QVector3D e1 = node.verts[face.verts[1]] - node.verts[face.verts[0]]; + QVector3D e2 = node.verts[face.verts[2]] - node.verts[face.verts[0]]; + QVector3D fn = QVector3D::crossProduct(e1, e2); + // Weight by face area (unnormalized cross product magnitude) + smoothNormals[face.verts[0]] += fn; + smoothNormals[face.verts[1]] += fn; + smoothNormals[face.verts[2]] += fn; + } + for (auto &n : smoothNormals) + n.normalize(); + } + + for (const auto &face : node.faces) + { + for (int i = 0; i < 3; ++i) + { + Vertex v{}; + int vi = face.verts[i]; + if (vi >= 0 && vi < node.verts.size()) + { + const auto &p = node.verts[vi]; + v.pos[0] = p.x(); v.pos[1] = p.y(); v.pos[2] = p.z(); + } + + if (hasNormals && vi >= 0 && vi < node.normals.size()) + { + const auto &n = node.normals[vi]; + v.normal[0] = n.x(); v.normal[1] = n.y(); v.normal[2] = n.z(); + } + else if (!smoothNormals.isEmpty() && vi >= 0 && vi < smoothNormals.size()) + { + const auto &n = smoothNormals[vi]; + v.normal[0] = n.x(); v.normal[1] = n.y(); v.normal[2] = n.z(); + } + else + { + v.normal[0] = 0; v.normal[1] = 0; v.normal[2] = 1; + } + + int ui = face.uvs[i]; + if (ui >= 0 && ui < node.tverts.size()) + { + const auto &t = node.tverts[ui]; + v.uv[0] = t.x(); v.uv[1] = t.y(); + } + + indices.append(static_cast(vertices.size())); + vertices.append(v); + } + } + + if (vertices.isEmpty()) + return; + + RenderNode rn; + rn.worldTransform = worldTransform; + rn.ambient = node.ambient; + rn.diffuse = node.diffuse; + rn.specular = node.specular; + rn.shininess = node.shininess; + rn.mesh = std::make_unique(); + rn.mesh->upload(gl, vertices, indices); + + if (!node.bitmap.isEmpty() && node.bitmap.toLower() != "null") { + QString texPath = resolveTexturePath(node.bitmap); + if (!texPath.isEmpty()) { + auto tex = std::make_unique(); + if (tex->loadFromFile(gl, texPath)) + rn.texture = std::move(tex); + else + qWarning() << "Failed to load texture:" << texPath; + } + } + + m_renderNodes.push_back(std::move(rn)); +} + +QString Renderer::resolveTexturePath(const QString &bitmap) const +{ + if (m_textureDir.isEmpty()) + return {}; + + QDir dir(m_textureDir); + static const QStringList extensions = {"tga", "dds", "png", "bmp", "jpg", "jpeg"}; + + for (const auto &ext : extensions) { + QString candidate = dir.filePath(bitmap + "." + ext); + if (QFileInfo::exists(candidate)) + return candidate; + } + + // Also check case-insensitive by scanning directory + QStringList entries = dir.entryList(QDir::Files); + QString lowerBitmap = bitmap.toLower(); + for (const auto &entry : entries) { + QString baseName = QFileInfo(entry).completeBaseName().toLower(); + if (baseName == lowerBitmap) { + QString suffix = QFileInfo(entry).suffix().toLower(); + if (extensions.contains(suffix)) + return dir.filePath(entry); + } + } + + return {}; +} + +void Renderer::render(QOpenGLFunctions_3_3_Core *gl, const Camera &camera) +{ + gl->glClearColor(0.18f, 0.20f, 0.25f, 1.0f); + gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + if (!m_program || m_renderNodes.empty()) { + static bool logged = false; + if (!logged) { + qDebug() << "RENDERER: render early return, program:" << m_program + << "nodes:" << m_renderNodes.size(); + logged = true; + } + return; + } + + gl->glEnable(GL_DEPTH_TEST); + gl->glDepthFunc(GL_LESS); + gl->glEnable(GL_CULL_FACE); + gl->glCullFace(GL_BACK); + gl->glEnable(GL_BLEND); + gl->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + gl->glEnable(GL_MULTISAMPLE); + + gl->glUseProgram(m_program); + + QMatrix4x4 view = camera.viewMatrix(); + QMatrix4x4 proj = camera.projectionMatrix(); + QVector3D camPos = camera.position(); + + gl->glUniformMatrix4fv(m_locView, 1, GL_FALSE, view.constData()); + gl->glUniformMatrix4fv(m_locProjection, 1, GL_FALSE, proj.constData()); + gl->glUniform3f(m_locViewPos, camPos.x(), camPos.y(), camPos.z()); + + QVector3D lightDir = QVector3D(-0.5f, -1.0f, -0.5f).normalized(); + gl->glUniform3f(m_locLightDir, lightDir.x(), lightDir.y(), lightDir.z()); + gl->glUniform3f(m_locLightColor, 1.0f, 1.0f, 1.0f); + + for (const auto &rn : m_renderNodes) + { + gl->glUniformMatrix4fv(m_locModel, 1, GL_FALSE, rn.worldTransform.constData()); + + QMatrix3x3 normalMat = rn.worldTransform.normalMatrix(); + gl->glUniformMatrix3fv(m_locNormalMatrix, 1, GL_FALSE, normalMat.constData()); + + gl->glUniform3f(m_locDiffuse, rn.diffuse.x(), rn.diffuse.y(), rn.diffuse.z()); + gl->glUniform3f(m_locAmbient, rn.ambient.x(), rn.ambient.y(), rn.ambient.z()); + gl->glUniform3f(m_locSpecular, rn.specular.x(), rn.specular.y(), rn.specular.z()); + gl->glUniform1f(m_locShininess, rn.shininess); + + if (rn.texture && rn.texture->isValid()) + rn.texture->bind(gl, 0); + else { + gl->glActiveTexture(GL_TEXTURE0); + gl->glBindTexture(GL_TEXTURE_2D, m_whiteTex); + } + gl->glUniform1i(m_locTexture, 0); + + rn.mesh->draw(gl); + } + + gl->glUseProgram(0); +} diff --git a/renderer.h b/renderer.h new file mode 100644 index 0000000..cb5a5ad --- /dev/null +++ b/renderer.h @@ -0,0 +1,69 @@ +#ifndef RENDERER_H +#define RENDERER_H + +#include "camera.h" +#include "gpumesh.h" +#include "gputexture.h" +#include "mdlscene.h" +#include +#include +#include +#include +#include + +struct RenderNode { + std::unique_ptr mesh; + std::unique_ptr texture; + QMatrix4x4 worldTransform; + QVector3D ambient; + QVector3D diffuse; + QVector3D specular; + float shininess = 1.0f; +}; + +class Renderer +{ +public: + Renderer() = default; + + void initialize(QOpenGLFunctions_3_3_Core *gl); + void shutdown(QOpenGLFunctions_3_3_Core *gl); + + void prepareScene(QOpenGLFunctions_3_3_Core *gl, const MdlScene &scene, + const QString &textureDir = QString()); + void render(QOpenGLFunctions_3_3_Core *gl, const Camera &camera); + + GLuint program() const { return m_program; } + int renderNodeCount() const { return static_cast(m_renderNodes.size()); } + +private: + GLuint m_program = 0; + GLint m_locModel = -1; + GLint m_locView = -1; + GLint m_locProjection = -1; + GLint m_locNormalMatrix = -1; + GLint m_locDiffuse = -1; + GLint m_locAmbient = -1; + GLint m_locSpecular = -1; + GLint m_locShininess = -1; + GLint m_locLightDir = -1; + GLint m_locLightColor = -1; + GLint m_locViewPos = -1; + GLint m_locTexture = -1; + + GLuint m_whiteTex = 0; + std::vector m_renderNodes; + QString m_textureDir; + + GLuint compileShader(QOpenGLFunctions_3_3_Core *gl, GLenum type, const char *src); + GLuint linkProgram(QOpenGLFunctions_3_3_Core *gl, GLuint vert, GLuint frag); + + void buildRenderNodes(QOpenGLFunctions_3_3_Core *gl, const MdlScene &scene, + int nodeIdx, const QMatrix4x4 &parentWorld); + void uploadNodeMesh(QOpenGLFunctions_3_3_Core *gl, const MdlNode &node, + const QMatrix4x4 &worldTransform); + + QString resolveTexturePath(const QString &bitmap) const; +}; + +#endif From 3f6fa3e90b5a2026792ad058aae0dbf0b7eb6e13 Mon Sep 17 00:00:00 2001 From: James Gre Date: Tue, 14 Apr 2026 00:32:30 -0400 Subject: [PATCH 02/28] feat: add compile mode and tilefade undo to align with CLI - Add Compile radio button to mode selector (Clean / Decompile / Compile) - Compile mode passes --compile flag, disables repair/check options - Add "Undo tilefade splits" checkbox with --tilefade-undo flag - Update status labels: "Compiled: N" / "Compiling" for compile mode - Add CliFlag::Compile and CliFlag::TilefadeUndo to constants.h - Add Setting::TilefadeUndo for persistence - Update buildCliArgs() with compile and tilefade-undo branches - Update onCleanFinished() button text for compile mode --- AUDIT-CRITIQUE.md | 173 + CLAUDE.md | 33 + CMakeLists.txt | 1 + DESIGN-BRIEF.md | 144 + EXTRACT-FINDINGS.md | 19 + build/.qt/QtDeploySupport.cmake | 71 + build/.qt/QtDeployTargets.cmake | 2 + build/CMakeCache.txt | 1359 ++ build/CMakeFiles/4.3.1/CMakeCXXCompiler.cmake | 102 + .../4.3.1/CMakeDetermineCompilerABI_CXX.bin | Bin 0 -> 33560 bytes build/CMakeFiles/4.3.1/CMakeSystem.cmake | 15 + .../CompilerIdCXX/CMakeCXXCompilerId.cpp | 949 + build/CMakeFiles/4.3.1/CompilerIdCXX/a.out | Bin 0 -> 33736 bytes .../4.3.1/CompilerIdCXX/apple-sdk.cpp | 1 + build/CMakeFiles/CMakeConfigureLog.yaml | 8637 ++++++++ .../CMakeDirectoryInformation.cmake | 16 + build/CMakeFiles/CMakeRuleHashes.txt | 4 + build/CMakeFiles/InstallScripts.json | 7 + build/CMakeFiles/TargetDirectories.txt | 5 + .../cleanmodels-qt.dir/DependInfo.cmake | 35 + .../CMakeFiles/cleanmodels-qt.dir/build.make | 309 + .../cleanmodels-qt.dir/camera.cpp.o.d | 964 + .../EWIEGA46WW/qrc_icons.cpp.o.d | 2 + .../mocs_compilation.cpp.o.d | 1137 + .../cleanmodels-qt.dir/cmake_clean.cmake | 39 + .../compiler_depend.internal | 13157 +++++++++++ .../cleanmodels-qt.dir/compiler_depend.make | 18398 ++++++++++++++++ .../cleanmodels-qt.dir/compiler_depend.ts | 2 + .../CMakeFiles/cleanmodels-qt.dir/depend.make | 2 + .../CMakeFiles/cleanmodels-qt.dir/flags.make | 12 + .../cleanmodels-qt.dir/fsmodel.cpp.o.d | 1022 + .../cleanmodels-qt.dir/gpumesh.cpp.o.d | 990 + .../cleanmodels-qt.dir/gputexture.cpp.o.d | 1045 + build/CMakeFiles/cleanmodels-qt.dir/link.txt | 1 + .../cleanmodels-qt.dir/main.cpp.o.d | 1076 + .../cleanmodels-qt.dir/mainwindow.cpp.o.d | 1241 ++ .../mainwindow_clean.cpp.o.d | 1144 + .../cleanmodels-qt.dir/mdlscene.cpp.o.d | 978 + .../cleanmodels-qt.dir/modelviewport.cpp.o.d | 1112 + .../cleanmodels-qt.dir/progress.make | 16 + .../cleanmodels-qt.dir/renderer.cpp.o.d | 1052 + .../AutoRcc_icons_EWIEGA46WW_Info.json | 22 + .../AutoRcc_icons_EWIEGA46WW_Lock.lock | 0 .../AutoRcc_icons_EWIEGA46WW_Used.txt | 1 + .../AutogenInfo.json | 575 + .../AutogenUsed.txt | 2 + .../DependInfo.cmake | 23 + .../cleanmodels-qt_autogen.dir/ParseCache.txt | 1982 ++ .../cleanmodels-qt_autogen.dir/build.make | 97 + .../cmake_clean.cmake | 11 + .../compiler_depend.internal | 1382 ++ .../compiler_depend.make | 4135 ++++ .../compiler_depend.ts | 2 + .../cleanmodels-qt_autogen.dir/progress.make | 2 + .../DependInfo.cmake | 22 + .../build.make | 86 + .../cmake_clean.cmake | 5 + .../compiler_depend.make | 2 + .../compiler_depend.ts | 2 + .../progress.make | 1 + build/CMakeFiles/cmake.check_cache | 1 + build/CMakeFiles/progress.marks | 1 + build/cleanmodels-qt | Bin 0 -> 1239024 bytes .../EWIEGA46WW/moc_mainwindow.cpp.d | 981 + .../EWIEGA46WW/moc_modelviewport.cpp.d | 977 + build/cleanmodels-qt_autogen/deps | 1378 ++ .../mocs_compilation.cpp | 3 + build/cleanmodels-qt_autogen/timestamp | 0 build/cmake_install.cmake | 61 + camera.cpp | 8 +- camera.h | 17 + constants.h | 272 + gpumesh.h | 2 + icons.qrc | 17 - icons/lock-rescale.png | Bin 650 -> 0 bytes icons/unlock-rescale.png | Bin 565 -> 0 bytes main.cpp | 7 +- mainwindow.cpp | 1712 +- mainwindow.h | 223 +- mainwindow.ui | 2795 +-- mainwindow_clean.cpp | 421 +- modelviewport.cpp | 138 +- modelviewport.h | 14 + renderer.cpp | 209 +- renderer.h | 24 +- 85 files changed, 69264 insertions(+), 3621 deletions(-) create mode 100644 AUDIT-CRITIQUE.md create mode 100644 CLAUDE.md create mode 100644 DESIGN-BRIEF.md create mode 100644 EXTRACT-FINDINGS.md create mode 100644 build/.qt/QtDeploySupport.cmake create mode 100644 build/.qt/QtDeployTargets.cmake create mode 100644 build/CMakeCache.txt create mode 100644 build/CMakeFiles/4.3.1/CMakeCXXCompiler.cmake create mode 100755 build/CMakeFiles/4.3.1/CMakeDetermineCompilerABI_CXX.bin create mode 100644 build/CMakeFiles/4.3.1/CMakeSystem.cmake create mode 100644 build/CMakeFiles/4.3.1/CompilerIdCXX/CMakeCXXCompilerId.cpp create mode 100755 build/CMakeFiles/4.3.1/CompilerIdCXX/a.out create mode 100644 build/CMakeFiles/4.3.1/CompilerIdCXX/apple-sdk.cpp create mode 100644 build/CMakeFiles/CMakeConfigureLog.yaml create mode 100644 build/CMakeFiles/CMakeDirectoryInformation.cmake create mode 100644 build/CMakeFiles/CMakeRuleHashes.txt create mode 100644 build/CMakeFiles/InstallScripts.json create mode 100644 build/CMakeFiles/TargetDirectories.txt create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/DependInfo.cmake create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/build.make create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/camera.cpp.o.d create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/cleanmodels-qt_autogen/EWIEGA46WW/qrc_icons.cpp.o.d create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/cleanmodels-qt_autogen/mocs_compilation.cpp.o.d create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/cmake_clean.cmake create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/compiler_depend.internal create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/compiler_depend.make create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/compiler_depend.ts create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/depend.make create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/flags.make create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/fsmodel.cpp.o.d create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/gpumesh.cpp.o.d create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/gputexture.cpp.o.d create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/link.txt create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/main.cpp.o.d create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/mainwindow.cpp.o.d create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/mainwindow_clean.cpp.o.d create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/mdlscene.cpp.o.d create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/modelviewport.cpp.o.d create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/progress.make create mode 100644 build/CMakeFiles/cleanmodels-qt.dir/renderer.cpp.o.d create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/AutoRcc_icons_EWIEGA46WW_Info.json create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/AutoRcc_icons_EWIEGA46WW_Lock.lock create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/AutoRcc_icons_EWIEGA46WW_Used.txt create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/AutogenInfo.json create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/AutogenUsed.txt create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/DependInfo.cmake create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/ParseCache.txt create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/build.make create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/cmake_clean.cmake create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/compiler_depend.internal create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/compiler_depend.make create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/compiler_depend.ts create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen.dir/progress.make create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen_timestamp_deps.dir/DependInfo.cmake create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen_timestamp_deps.dir/build.make create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen_timestamp_deps.dir/cmake_clean.cmake create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen_timestamp_deps.dir/compiler_depend.make create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen_timestamp_deps.dir/compiler_depend.ts create mode 100644 build/CMakeFiles/cleanmodels-qt_autogen_timestamp_deps.dir/progress.make create mode 100644 build/CMakeFiles/cmake.check_cache create mode 100644 build/CMakeFiles/progress.marks create mode 100755 build/cleanmodels-qt create mode 100644 build/cleanmodels-qt_autogen/EWIEGA46WW/moc_mainwindow.cpp.d create mode 100644 build/cleanmodels-qt_autogen/EWIEGA46WW/moc_modelviewport.cpp.d create mode 100644 build/cleanmodels-qt_autogen/deps create mode 100644 build/cleanmodels-qt_autogen/mocs_compilation.cpp create mode 100644 build/cleanmodels-qt_autogen/timestamp create mode 100644 build/cmake_install.cmake create mode 100644 constants.h delete mode 100644 icons/lock-rescale.png delete mode 100644 icons/unlock-rescale.png diff --git a/AUDIT-CRITIQUE.md b/AUDIT-CRITIQUE.md new file mode 100644 index 0000000..4aa6516 --- /dev/null +++ b/AUDIT-CRITIQUE.md @@ -0,0 +1,173 @@ +# cleanmodels-qt — Design Audit & Critique + +## Anti-Patterns Verdict + +**Pass.** This does not look AI-generated. It looks like a competent developer built a functional tool and stopped before the "polish" phase. No gradient text, no glassmorphism, no hero metrics, no card grids. The aesthetic is honest — it's a Qt utility app that uses native widgets. The problem isn't that it's trying too hard; it's that it isn't trying at all in places where it should. + +--- + +## Overall Impression + +The tool is functionally complete and architecturally sound. The 3D viewport, file table, debug log, and option panels all work. But the layout feels like a **settings dialog that grew into a main window** — every option has equal visual weight, there's no clear narrative flow from "pick a directory" to "review results," and the most important element (the 3D viewport where users verify their work) is crammed into half of the bottom quarter of the screen. + +**Single biggest opportunity**: Restructure the layout so the viewport and results are the heroes, and the options recede into a sidebar or panel. + +--- + +## What's Working + +1. **Progressive disclosure via collapsible groups** — The Advanced, Tile, and Pivot sections default to collapsed. This is the right instinct — most users should never need to open them. The "All Fixes (recommended)" master checkbox is a genuinely good UX pattern. + +2. **File table with status feedback** — Icons for ASCII/binary, per-file status/fixes/time, and the colored debug log provide a clear audit trail. The tooltip on the fixes column showing individual fixes is thoughtful. + +3. **Reference model overlay** — Being able to visually compare against a reference model in the 3D viewport is a standout feature for this domain. + +--- + +## Priority Issues + +### 1. The viewport is buried — the most critical element gets the least space + +**What**: The 3D viewport is the user's only way to verify fixes visually. It's "critical to the workflow." But it's allocated to roughly 1/4 of the window — the bottom-right quadrant of a bottom half-panel, hidden behind a horizontal splitter. + +**Why it matters**: Users process a directory of models, then want to spot-check results. They have to squint at a tiny viewport or manually drag splitters every session. The options panel (which they configure once and forget) takes more space than the thing they stare at continuously. + +**Fix**: Consider a left sidebar layout: narrow options panel (250-300px) on the left, file table + viewport stacked on the right taking 70%+ of the window. Or a tabbed approach where options are a collapsible panel and the main area is table + viewport. The viewport should be at *least* 50% of the visible area when working. + +**Command**: `/i-arrange` + +### 2. Flat visual hierarchy — everything screams at the same volume + +**What**: The In/Out path fields, mode radio buttons, fix checkboxes, advanced options, tile options, pivot options, clean button, file table, debug log, and viewport all sit in a single vertical stack with uniform 4px spacing. There's no visual grouping, no breathing room, no sense of priority. + +**Why it matters**: A new user opening this for the first time can't tell at a glance: "What do I do first? What's the primary action? What can I ignore?" The eye has nowhere to land. The Clean button — the single most important action — is the same visual weight as the "Pattern:" label. + +**Fix**: +- Give the Clean button significantly more prominence — larger, colored, or in a fixed toolbar position +- Separate the "configure" phase (top: paths + options) from the "results" phase (bottom: table + viewport + log) with a clear visual divider +- Use typography weight/size to create hierarchy: section headers should be visually distinct from field labels + +**Command**: `/i-arrange`, `/i-typeset` + +### 3. The debug log is an HTML soup with no structure + +**What**: `appendDebugHtml` inserts raw HTML strings with inline `color:` styles into a `QTextBrowser`. The log output is a wall of colored text with ``, `