From 5dd213695da2eb22219ac5143aa8b5cfe7c89559 Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Sun, 12 Aug 2018 20:44:21 +0000 Subject: [PATCH 1/3] New upstream version 0.9.3 --- README.md | 59 ++++++++++---- app/package.sh | 53 +++++++++++-- exe/package.sh | 16 +++- qt/fstl.pro | 5 ++ qt/style.qss | 17 +--- src/app.cpp | 2 +- src/app.h | 2 +- src/canvas.cpp | 70 ++++++++++++---- src/canvas.h | 13 ++- src/loader.cpp | 211 +++++++++++++++++++++++++++++++++++++++---------- src/loader.h | 17 +++- src/mesh.cpp | 13 +++ src/mesh.h | 2 + src/window.cpp | 200 ++++++++++++++++++++++++++++++++++++++++++---- src/window.h | 32 +++++++- 15 files changed, 588 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index a9a845f..b4ab04b 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,58 @@ -`fstl` is a viewer for [.stl files](http://en.wikipedia.org/wiki/STL_\(file_format\)). +# fstl -Here's a screenshot: -![Eiffel tower](http://mattkeeter.com/projects/fstl/eiffel.png) -(credit to [Pranav Panchal](https://grabcad.com/pranav.panchal)) +`fstl` is the fastest [.stl file](http://en.wikipedia.org/wiki/STL_\(file_format\)) viewer. It is designed to quickly load and render very high-polygon models; showing 2 million triangles at 60+ FPS on a mid-range laptop. For more details, see the [project page](http://mattkeeter.com/projects/fstl). -Issues and pull requests are welcome; +Issues and minor pull requests are welcome; the project is under 1K lines of code and should be fairly approachable. --------------------------------------------------------------------------------- +## Screenshot +![Eiffel tower](http://mattkeeter.com/projects/fstl/eiffel.png) +(credit to [Pranav Panchal](https://grabcad.com/pranav.panchal)) + +## Building + +The only dependency for `fstl` is [Qt](https://www.qt.io). + +### macOS -License -------- -(c) 2014 Matt Keeter +Install Qt from their website or [Homebrew](brew.sh), +making sure `qmake` is on your shell's path. -This work may be reproduced, modified, distributed, performed, and displayed for any purpose. Copyright is retained and must be preserved. The work is provided as is; no warranty is provided, and users accept all liability. +Then, run through the following set of commands in a shell: +``` +git clone https://github.com/mkeeter/fstl +cd fstl +mkdir build +cd build +qmake ../qt/fstl.pro +make -j8 +./fstl.app/Contents/MacOS/fstl +``` + +To package a standalone app, go to the app directory and run `package.sh` + +``` +cd ../app +./package.sh +``` + +This should produce two new files in the root directory: +- `fstl.app` is a standalone application that can be copied to `/Applications` +- `fstl.dmg` is a disk image that can be given to a friend -------------------------------------------------------------------------------- -*p.s.* In my day job, I'm an engineer at [Formlabs](http://formlabs.com). -If you think `fstl` is cool, you should check out our -[jobs page](http://formlabs.com/jobs) -- we're hiring software developers -and engineers of all stripes to work on interesting, hard problems in -consumer-level 3D printing. +# License + +Copyright (c) 2014-2017 Matthew Keeter + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/app/package.sh b/app/package.sh index d51c470..bdb4999 100755 --- a/app/package.sh +++ b/app/package.sh @@ -1,14 +1,53 @@ #!/bin/sh +set -x -e + cd ../build -macdeployqt fstl.app +make clean +rm -rf fstl.app +make -j8 + +APP=fstl + +# Pull out framework paths info with otool +MACDEPLOYQT=`otool -L $APP.app/Contents/MacOS/fstl | sed -n -e "s:\(.*\)lib/QtCore.*:\1/bin/macdeployqt:gp"` + +$MACDEPLOYQT $APP.app + +# Delete unused Qt plugins cd fstl.app/Contents/PlugIns rm -rf accessible audio imageformats mediaservice playlistformats position printsupport qml1tooling sensorgestures sensors -cd ../Frameworks -rm -rf QtDeclarative.framework QtMultimedia.framework QtMultimediaWidgets.framework QtNetwork.framework QtPositioning.framework QtQml.framework QtQuick.framework QtScript.framework QtSensors.framework QtSql.framework QtXmlPatterns.framework + +fix_qt () { + echo "Fixing Qt for $1" + for LIB in $( otool -L $1 | sed -n -e "s:\(.*Qt.*.framework[^ ]*\).*:\1:gp" ) + do + RENAMED=`echo $LIB | sed -n -e "s:.*\(Qt.*\)\.framework.*:@executable_path/../Frameworks/\1.framework/Versions/5/\1:gp"` + install_name_tool -change $LIB $RENAMED $1 + done +} + +# Remap platform links +cd platforms +fix_qt libqcocoa.dylib + +# Delete unused Qt frameworks +cd ../../Frameworks +rm -rf QtDeclarative.framework QtMultimedia.framework QtMultimediaWidgets.framework QtNetwork.framework QtPositioning.framework QtQml.framework QtQuick.framework QtScript.framework QtSensors.framework QtSql.framework QtXmlPatterns.framework Qt3DCore.framework Qt3DRender.framework QtLocation.framework QtSerialBus.framework QtSerialPort.framework + +# Clean up remaining Qt frameworks +for LIB in $( ls|sed -n -e "s:\(Qt.*\)\.framework:\1:gp" ) +do + fix_qt $LIB.framework/Versions/Current/$LIB +done + cd ../Resources rm empty.lproj -cd ../../.. -cp -r fstl.app .. -cd .. -zip -r fstl_mac.zip fstl.app README.md +# Create a disk image +cd ../../.. +mkdir $APP +cp ../README.md ./$APP/README.txt +cp -R $APP.app ./$APP +hdiutil create $APP.dmg -volname "$APP" -srcfolder $APP +rm -rf $APP +mv $APP.dmg .. diff --git a/exe/package.sh b/exe/package.sh index ef81b62..dfc4115 100644 --- a/exe/package.sh +++ b/exe/package.sh @@ -1,3 +1,13 @@ -cd .. -cp build/release/fstl.exe . -/c/Program\ Files/7-Zip/7z.exe a fstl_win.zip fstl.exe README.md +#!/bin/sh +set -e -x + +cd ../build +make clean +rm -rf fstl.exe +make -j8 + +cp ../README.md README.txt +cp release/fstl.exe . +/c/Program\ Files/7-Zip/7z.exe a fstl.zip fstl.exe README.txt +cp fstl.zip .. +rm README.txt fstl.exe diff --git a/qt/fstl.pro b/qt/fstl.pro index 7a0f449..5d8613d 100644 --- a/qt/fstl.pro +++ b/qt/fstl.pro @@ -41,6 +41,11 @@ win32 { RC_FILE = ../exe/fstl.rc } +linux { + target.path = /usr/bin + INSTALLS += target +} + static { CONFIG += static } diff --git a/qt/style.qss b/qt/style.qss index 0297148..3f0ca9b 100644 --- a/qt/style.qss +++ b/qt/style.qss @@ -1,19 +1,4 @@ QWidget { - background-color: #fdf6e3; + background-color: null; color: #839496; } - -QPushButton { - background-color: #eee8d5; - border-top-width: 5px; - border-bottom-width: 5px; - border-left-width: 30px; - border-right-width: 30px; - border-style: flat; - margin: 0px; - color: #839496; -} - -QPushButton:pressed { - background-color: #ddd7c4; -} diff --git a/src/app.cpp b/src/app.cpp index b6eb3af..d182048 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -4,7 +4,7 @@ #include "app.h" #include "window.h" -App::App(int argc, char *argv[]) : +App::App(int& argc, char *argv[]) : QApplication(argc, argv), window(new Window()) { window->show(); diff --git a/src/app.h b/src/app.h index 7a7672e..2e06c24 100644 --- a/src/app.h +++ b/src/app.h @@ -9,7 +9,7 @@ class App : public QApplication { Q_OBJECT public: - explicit App(int argc, char *argv[]); + explicit App(int& argc, char *argv[]); protected: bool event(QEvent* e); private: diff --git a/src/canvas.cpp b/src/canvas.cpp index dd645a5..b42a2be 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -10,9 +10,14 @@ Canvas::Canvas(const QGLFormat& format, QWidget *parent) : QGLWidget(format, parent), mesh(NULL), - scale(1), zoom(1), tilt(90), yaw(0), status(" ") + scale(1), zoom(1), tilt(90), yaw(0), + perspective(0.25), anim(this, "perspective"), status(" ") { - // Nothing to do here + QFile styleFile(":/qt/style.qss"); + styleFile.open( QFile::ReadOnly ); + setStyleSheet(styleFile.readAll()); + + anim.setDuration(100); } Canvas::~Canvas() @@ -20,21 +25,39 @@ Canvas::~Canvas() delete mesh; } -void Canvas::load_mesh(Mesh* m) +void Canvas::view_anim(float v) +{ + anim.setStartValue(perspective); + anim.setEndValue(v); + anim.start(); +} + +void Canvas::view_orthographic() +{ + view_anim(0); +} + +void Canvas::view_perspective() +{ + view_anim(0.25); +} + +void Canvas::load_mesh(Mesh* m, bool is_reload) { mesh = new GLMesh(m); - center = QVector3D(m->xmin() + m->xmax(), - m->ymin() + m->ymax(), - m->zmin() + m->zmax()) / 2; - scale = 2 / sqrt( - pow(m->xmax() - m->xmin(), 2) + - pow(m->ymax() - m->ymin(), 2) + - pow(m->zmax() - m->zmin(), 2)); - - // Reset other camera parameters - zoom = 1; - yaw = 0; - tilt = 90; + + if (!is_reload) + { + QVector3D lower(m->xmin(), m->ymin(), m->zmin()); + QVector3D upper(m->xmax(), m->ymax(), m->zmax()); + center = (lower + upper) / 2; + scale = 2 / (upper - lower).length(); + + // Reset other camera parameters + zoom = 1; + yaw = 0; + tilt = 90; + } update(); @@ -47,6 +70,12 @@ void Canvas::set_status(const QString &s) update(); } +void Canvas::set_perspective(float p) +{ + perspective = p; + update(); +} + void Canvas::clear_status() { status = ""; @@ -115,7 +144,7 @@ QMatrix4x4 Canvas::transform_matrix() const QMatrix4x4 m; m.rotate(tilt, QVector3D(1, 0, 0)); m.rotate(yaw, QVector3D(0, 0, 1)); - m.scale(scale); + m.scale(-scale, scale, -scale); m.translate(-center); return m; } @@ -132,6 +161,7 @@ QMatrix4x4 Canvas::view_matrix() const m.scale(-1, width() / float(height()), 0.5); } m.scale(zoom, zoom, 1); + m(3, 2) = perspective; return m; } @@ -159,10 +189,11 @@ void Canvas::mouseMoveEvent(QMouseEvent* event) auto p = event->pos(); auto d = p - mouse_pos; + if (event->buttons() & Qt::LeftButton) { yaw = fmod(yaw - d.x(), 360); - tilt = fmax(0, fmin(180, tilt - d.y())); + tilt = fmod(tilt - d.y(), 360); update(); } else if (event->buttons() & Qt::RightButton) @@ -203,3 +234,8 @@ void Canvas::wheelEvent(QWheelEvent *event) center += b - a; update(); } + +void Canvas::resizeGL(int width, int height) +{ + glViewport(0, 0, width, height); +} diff --git a/src/canvas.h b/src/canvas.h index 33313cf..4dae298 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -2,6 +2,7 @@ #define CANVAS_H #include +#include #include #include #include @@ -22,10 +23,13 @@ public: void paintEvent(QPaintEvent* event); ~Canvas(); + void view_orthographic(); + void view_perspective(); + public slots: void set_status(const QString& s); void clear_status(); - void load_mesh(Mesh* m); + void load_mesh(Mesh* m, bool is_reload); protected: @@ -33,6 +37,9 @@ protected: void mouseReleaseEvent(QMouseEvent* event); void mouseMoveEvent(QMouseEvent* event); void wheelEvent(QWheelEvent* event); + void resizeGL(int width, int height); + void set_perspective(float p); + void view_anim(float v); private: @@ -53,6 +60,10 @@ private: float tilt; float yaw; + float perspective; + Q_PROPERTY(float perspective WRITE set_perspective); + QPropertyAnimation anim; + QPoint mouse_pos; QString status; }; diff --git a/src/loader.cpp b/src/loader.cpp index 0d6b165..93f5525 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -1,7 +1,9 @@ +#include + #include "loader.h" -Loader::Loader(QObject* parent, const QString& filename) - : QThread(parent), filename(filename) +Loader::Loader(QObject* parent, const QString& filename, bool is_reload) + : QThread(parent), filename(filename), is_reload(is_reload) { // Nothing to do here } @@ -11,12 +13,19 @@ void Loader::run() Mesh* mesh = load_stl(); if (mesh) { - emit got_mesh(mesh); - emit loaded_file(filename); + if (mesh->empty()) + { + emit error_empty_mesh(); + delete mesh; + } + else + { + emit got_mesh(mesh, is_reload); + emit loaded_file(filename); + } } } - //////////////////////////////////////////////////////////////////////////////// struct Vec3 @@ -37,25 +46,116 @@ struct Vec3 typedef std::pair Vec3i; +void parallel_sort(Vec3i* begin, Vec3i* end, int threads) +{ + if (threads < 2 || end - begin < 2) + { + std::sort(begin, end); + } + else + { + const auto mid = begin + (end - begin) / 2; + if (threads == 2) + { + auto future = std::async(parallel_sort, begin, mid, threads / 2); + std::sort(mid, end); + future.wait(); + } + else + { + auto a = std::async(std::launch::async, parallel_sort, begin, mid, threads / 2); + auto b = std::async(std::launch::async, parallel_sort, mid, end, threads / 2); + a.wait(); + b.wait(); + } + std::inplace_merge(begin, mid, end); + } +} + +Mesh* mesh_from_verts(uint32_t tri_count, QVector& verts) +{ + // Save indicies as the second element in the array + // (so that we can reconstruct triangle order after sorting) + for (size_t i=0; i < tri_count*3; ++i) + { + verts[i].second = i; + } + + // Sort the set of vertices (to deduplicate) + parallel_sort(verts.begin(), verts.end(), 8); + + // This vector will store triangles as sets of 3 indices + std::vector indices(tri_count*3); + + // Go through the sorted vertex list, deduplicating and creating + // an indexed geometry representation for the triangles. + // Unique vertices are moved so that they occupy the first vertex_count + // positions in the verts array. + size_t vertex_count = 0; + for (auto v : verts) + { + if (!vertex_count || v.first != verts[vertex_count-1].first) + { + verts[vertex_count++] = v; + } + indices[v.second] = vertex_count - 1; + } + verts.resize(vertex_count); + + std::vector flat_verts; + flat_verts.reserve(vertex_count*3); + for (auto v : verts) + { + flat_verts.push_back(v.first.x); + flat_verts.push_back(v.first.y); + flat_verts.push_back(v.first.z); + } + + return new Mesh(flat_verts, indices); +} + //////////////////////////////////////////////////////////////////////////////// Mesh* Loader::load_stl() { QFile file(filename); - file.open(QIODevice::ReadOnly); - if (file.read(5) == "solid") + if (!file.open(QIODevice::ReadOnly)) { - emit error_ascii_stl(); + emit error_missing_file(); return NULL; } - // Skip the rest of the header material - file.read(75); + // First, try to read the stl as an ASCII file + if (file.read(6) == "solid ") + { + file.readLine(); // skip solid name + const auto line = file.readLine().trimmed(); + if (line.startsWith("facet") || + line.startsWith("endsolid")) + { + file.seek(0); + return read_stl_ascii(file); + } + confusing_stl = true; + } + else + { + confusing_stl = false; + } + + // Otherwise, skip the rest of the header material and read as binary + file.seek(0); + return read_stl_binary(file); +} + +Mesh* Loader::read_stl_binary(QFile& file) +{ QDataStream data(&file); data.setByteOrder(QDataStream::LittleEndian); data.setFloatingPointPrecision(QDataStream::SinglePrecision); // Load the triangle count from the .stl file + file.seek(80); uint32_t tri_count; data >> tri_count; @@ -70,60 +170,87 @@ Mesh* Loader::load_stl() QVector verts(tri_count*3); // Dummy array, because readRawData is faster than skipRawData - char buffer[sizeof(float)*3]; + uint8_t* buffer = (uint8_t*)malloc(tri_count * 50); + data.readRawData((char*)buffer, tri_count * 50); // Store vertices in the array, processing one triangle at a time. + auto b = buffer; for (auto v=verts.begin(); v != verts.end(); v += 3) { // Skip face's normal vector - data.readRawData(buffer, 3*sizeof(float)); + b += 3 * sizeof(float); // Load vertex data from .stl file into vertices - data >> v[0].first.x >> v[0].first.y >> v[0].first.z; - data >> v[1].first.x >> v[1].first.y >> v[1].first.z; - data >> v[2].first.x >> v[2].first.y >> v[2].first.z; + for (unsigned i=0; i < 3; ++i) + { + memcpy(&v[i].first, b, 3*sizeof(float)); + b += 3 * sizeof(float); + } // Skip face attribute - data.readRawData(buffer, sizeof(uint16_t)); + b += sizeof(uint16_t); } - // Save indicies as the second element in the array - // (so that we can reconstruct triangle order after sorting) - for (size_t i=0; i < tri_count*3; ++i) + if (confusing_stl) { - verts[i].second = i; + emit warning_confusing_stl(); } + free(buffer); - // Sort the set of vertices (to deduplicate) - std::sort(verts.begin(), verts.end()); + return mesh_from_verts(tri_count, verts); +} - // This vector will store triangles as sets of 3 indices - std::vector indices(tri_count*3); +Mesh* Loader::read_stl_ascii(QFile& file) +{ + file.readLine(); + uint32_t tri_count = 0; + QVector verts(tri_count*3); - // Go through the sorted vertex list, deduplicating and creating - // an indexed geometry representation for the triangles. - // Unique vertices are moved so that they occupy the first vertex_count - // positions in the verts array. - size_t vertex_count = 0; - for (auto v : verts) + bool okay = true; + while (!file.atEnd() && okay) { - if (!vertex_count || v.first != verts[vertex_count-1].first) + const auto line = file.readLine().simplified(); + if (line.startsWith("endsolid")) { - verts[vertex_count++] = v; + break; } - indices[v.second] = vertex_count - 1; + else if (!line.startsWith("facet normal") || + !file.readLine().simplified().startsWith("outer loop")) + { + okay = false; + break; + } + + for (int i=0; i < 3; ++i) + { + auto line = file.readLine().simplified().split(' '); + if (line[0] != "vertex") + { + okay = false; + break; + } + const float x = line[1].toFloat(&okay); + const float y = line[2].toFloat(&okay); + const float z = line[3].toFloat(&okay); + verts.push_back({{x, y, z}, 0}); + } + if (!file.readLine().trimmed().startsWith("endloop") || + !file.readLine().trimmed().startsWith("endfacet")) + { + okay = false; + break; + } + tri_count++; } - verts.resize(vertex_count); - std::vector flat_verts; - flat_verts.reserve(vertex_count*3); - for (auto v : verts) + if (okay) { - flat_verts.push_back(v.first.x); - flat_verts.push_back(v.first.y); - flat_verts.push_back(v.first.z); + return mesh_from_verts(tri_count, verts); + } + else + { + emit error_bad_stl(); + return NULL; } - - return new Mesh(flat_verts, indices); } diff --git a/src/loader.h b/src/loader.h index fb0c8d8..006f1d1 100644 --- a/src/loader.h +++ b/src/loader.h @@ -9,21 +9,32 @@ class Loader : public QThread { Q_OBJECT public: - explicit Loader(QObject* parent, const QString& filename); + explicit Loader(QObject* parent, const QString& filename, bool is_reload); void run(); protected: Mesh* load_stl(); + /* Reads an ASCII stl, starting from the start of the file*/ + Mesh* read_stl_ascii(QFile& file); + /* Reads a binary stl, assuming we're at the end of the header */ + Mesh* read_stl_binary(QFile& file); + signals: void loaded_file(QString filename); - void got_mesh(Mesh* m); + void got_mesh(Mesh* m, bool is_reload); - void error_ascii_stl(); void error_bad_stl(); + void error_empty_mesh(); + void warning_confusing_stl(); + void error_missing_file(); private: const QString filename; + bool is_reload; + + /* Used to warn on binary STLs that begin with the word 'solid'" */ + bool confusing_stl; }; diff --git a/src/mesh.cpp b/src/mesh.cpp index cfb4ca4..140a769 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -16,6 +16,10 @@ Mesh::Mesh(std::vector v, std::vector i) float Mesh::min(size_t start) const { + if (start >= vertices.size()) + { + return -1; + } float v = vertices[start]; for (size_t i=start; i < vertices.size(); i += 3) { @@ -26,6 +30,10 @@ float Mesh::min(size_t start) const float Mesh::max(size_t start) const { + if (start >= vertices.size()) + { + return 1; + } float v = vertices[start]; for (size_t i=start; i < vertices.size(); i += 3) { @@ -33,3 +41,8 @@ float Mesh::max(size_t start) const } return v; } + +bool Mesh::empty() const +{ + return vertices.size() == 0; +} diff --git a/src/mesh.h b/src/mesh.h index e8a02f0..1412781 100644 --- a/src/mesh.h +++ b/src/mesh.h @@ -21,6 +21,8 @@ public: float ymax() const { return max(1); } float zmax() const { return max(2); } + bool empty() const; + private: std::vector vertices; std::vector indices; diff --git a/src/window.cpp b/src/window.cpp index 3002ecc..0954b33 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -6,20 +6,26 @@ #include "canvas.h" #include "loader.h" +const QString Window::RECENT_FILE_KEY = "recentFiles"; + Window::Window(QWidget *parent) : QMainWindow(parent), open_action(new QAction("Open", this)), about_action(new QAction("About", this)), - quit_action(new QAction("Quit", this)) + quit_action(new QAction("Quit", this)), + perspective_action(new QAction("Perspective", this)), + orthogonal_action(new QAction("Orthographic", this)), + reload_action(new QAction("Reload", this)), + autoreload_action(new QAction("Autoreload", this)), + recent_files(new QMenu("Open recent", this)), + recent_files_group(new QActionGroup(this)), + recent_files_clear_action(new QAction("Clear recent files", this)), + watcher(new QFileSystemWatcher(this)) { setWindowTitle("fstl"); setAcceptDrops(true); - QFile styleFile(":/qt/style.qss"); - styleFile.open( QFile::ReadOnly ); - setStyleSheet(styleFile.readAll()); - QGLFormat format; format.setVersion(2, 1); format.setSampleBuffers(true); @@ -27,6 +33,9 @@ Window::Window(QWidget *parent) : canvas = new Canvas(format, this); setCentralWidget(canvas); + QObject::connect(watcher, &QFileSystemWatcher::fileChanged, + this, &Window::on_watched_change); + open_action->setShortcut(QKeySequence::Open); QObject::connect(open_action, &QAction::triggered, this, &Window::on_open); @@ -35,13 +44,50 @@ Window::Window(QWidget *parent) : QObject::connect(quit_action, &QAction::triggered, this, &Window::close); + autoreload_action->setCheckable(true); + autoreload_action->setChecked(true); + autoreload_action->setEnabled(false); + QObject::connect(autoreload_action, &QAction::triggered, + this, &Window::on_autoreload_triggered); + + reload_action->setShortcut(QKeySequence::Refresh); + reload_action->setEnabled(false); + QObject::connect(reload_action, &QAction::triggered, + this, &Window::on_reload); + QObject::connect(about_action, &QAction::triggered, this, &Window::on_about); + QObject::connect(recent_files_clear_action, &QAction::triggered, + this, &Window::on_clear_recent); + QObject::connect(recent_files_group, &QActionGroup::triggered, + this, &Window::on_load_recent); + + rebuild_recent_files(); + auto file_menu = menuBar()->addMenu("File"); file_menu->addAction(open_action); + file_menu->addMenu(recent_files); + file_menu->addSeparator(); + file_menu->addAction(reload_action); + file_menu->addAction(autoreload_action); file_menu->addAction(quit_action); + auto view_menu = menuBar()->addMenu("View"); + auto projection_menu = view_menu->addMenu("Projection"); + projection_menu->addAction(perspective_action); + projection_menu->addAction(orthogonal_action); + auto projections = new QActionGroup(projection_menu); + for (auto p : {perspective_action, orthogonal_action}) + { + projections->addAction(p); + p->setCheckable(true); + } + perspective_action->setChecked(true); + projections->setExclusive(true); + QObject::connect(projections, &QActionGroup::triggered, + this, &Window::on_projection); + auto help_menu = menuBar()->addMenu("Help"); help_menu->addAction(about_action); @@ -65,25 +111,39 @@ void Window::on_about() "

A fast viewer for .stl files.
" "https://github.com/mkeeter/fstl

" - "

© 2014 Matthew Keeter
" + "

© 2014-2017 Matthew Keeter
" "matt.j.keeter@gmail.com

"); } -void Window::on_ascii_stl() +void Window::on_bad_stl() { QMessageBox::critical(this, "Error", "Error:
" - "Cannot open ASCII .stl file
" - "Please convert to binary .stl and retry"); + "This .stl file is invalid or corrupted.
" + "Please export it from the original source, verify, and retry."); } -void Window::on_bad_stl() +void Window::on_empty_mesh() { QMessageBox::critical(this, "Error", "Error:
" - "This .stl file is invalid or corrupted.
" - "Please export it from the original source, verify, and retry."); + "This file is syntactically correct
but contains no triangles."); +} + +void Window::on_confusing_stl() +{ + QMessageBox::warning(this, "Warning", + "Warning:
" + "This .stl file begins with solid but appears to be a binary file.
" + "fstl loaded it, but other programs may be confused by this file."); +} + +void Window::on_missing_file() +{ + QMessageBox::critical(this, "Error", + "Error:
" + "The target file is missing.
"); } void Window::enable_open() @@ -96,22 +156,126 @@ void Window::disable_open() open_action->setEnabled(false); } -bool Window::load_stl(const QString& filename) +void Window::set_watched(const QString& filename) +{ + const auto files = watcher->files(); + if (files.size()) + { + watcher->removePaths(watcher->files()); + } + watcher->addPath(filename); + + QSettings settings; + auto recent = settings.value(RECENT_FILE_KEY).toStringList(); + const auto f = QFileInfo(filename).absoluteFilePath(); + recent.removeAll(f); + recent.prepend(f); + while (recent.size() > MAX_RECENT_FILES) + { + recent.pop_back(); + } + settings.setValue(RECENT_FILE_KEY, recent); + rebuild_recent_files(); +} + +void Window::on_projection(QAction* proj) +{ + if (proj == perspective_action) + { + canvas->view_perspective(); + } + else + { + canvas->view_orthographic(); + } +} + +void Window::on_watched_change(const QString& filename) +{ + if (autoreload_action->isChecked()) + { + load_stl(filename, true); + } +} + +void Window::on_autoreload_triggered(bool b) +{ + if (b) + { + on_reload(); + } +} + +void Window::on_clear_recent() +{ + QSettings settings; + settings.setValue(RECENT_FILE_KEY, QStringList()); + rebuild_recent_files(); +} + +void Window::on_load_recent(QAction* a) +{ + load_stl(a->data().toString()); +} + +void Window::rebuild_recent_files() +{ + QSettings settings; + QStringList files = settings.value(RECENT_FILE_KEY).toStringList(); + + const auto actions = recent_files_group->actions(); + for (auto a : actions) + { + recent_files_group->removeAction(a); + } + recent_files->clear(); + + for (auto f : files) + { + const auto a = new QAction(f, recent_files); + a->setData(f); + recent_files_group->addAction(a); + recent_files->addAction(a); + } + if (files.size() == 0) + { + auto a = new QAction("No recent files", recent_files); + recent_files->addAction(a); + a->setEnabled(false); + } + recent_files->addSeparator(); + recent_files->addAction(recent_files_clear_action); +} + +void Window::on_reload() +{ + auto fs = watcher->files(); + if (fs.size() == 1) + { + load_stl(fs[0], true); + } +} + +bool Window::load_stl(const QString& filename, bool is_reload) { if (!open_action->isEnabled()) return false; canvas->set_status("Loading " + filename); - Loader* loader = new Loader(this, filename); + Loader* loader = new Loader(this, filename, is_reload); connect(loader, &Loader::started, this, &Window::disable_open); connect(loader, &Loader::got_mesh, canvas, &Canvas::load_mesh); - connect(loader, &Loader::error_ascii_stl, - this, &Window::on_ascii_stl); connect(loader, &Loader::error_bad_stl, this, &Window::on_bad_stl); + connect(loader, &Loader::error_empty_mesh, + this, &Window::on_empty_mesh); + connect(loader, &Loader::warning_confusing_stl, + this, &Window::on_confusing_stl); + connect(loader, &Loader::error_missing_file, + this, &Window::on_missing_file); connect(loader, &Loader::finished, loader, &Loader::deleteLater); @@ -124,6 +288,10 @@ bool Window::load_stl(const QString& filename) { connect(loader, &Loader::loaded_file, this, &Window::setWindowTitle); + connect(loader, &Loader::loaded_file, + this, &Window::set_watched); + autoreload_action->setEnabled(true); + reload_action->setEnabled(true); } loader->start(); diff --git a/src/window.h b/src/window.h index cb92ebb..fd0a770 100644 --- a/src/window.h +++ b/src/window.h @@ -2,6 +2,8 @@ #define WINDOW_H #include +#include +#include class Canvas; @@ -10,7 +12,7 @@ class Window : public QMainWindow Q_OBJECT public: explicit Window(QWidget* parent=0); - bool load_stl(const QString& filename); + bool load_stl(const QString& filename, bool is_reload=false); protected: void dragEnterEvent(QDragEnterEvent* event); @@ -19,16 +21,42 @@ protected: public slots: void on_open(); void on_about(); - void on_ascii_stl(); void on_bad_stl(); + void on_empty_mesh(); + void on_missing_file(); + void on_confusing_stl(); void enable_open(); void disable_open(); + void set_watched(const QString& filename); + +private slots: + void on_projection(QAction* proj); + void on_watched_change(const QString& filename); + void on_reload(); + void on_autoreload_triggered(bool r); + void on_clear_recent(); + void on_load_recent(QAction* a); + private: + void rebuild_recent_files(); + QAction* const open_action; QAction* const about_action; QAction* const quit_action; + QAction* const perspective_action; + QAction* const orthogonal_action; + QAction* const reload_action; + QAction* const autoreload_action; + + QMenu* const recent_files; + QActionGroup* const recent_files_group; + QAction* const recent_files_clear_action; + const static int MAX_RECENT_FILES=8; + const static QString RECENT_FILE_KEY; + + QFileSystemWatcher* watcher; Canvas* canvas; }; -- 2.39.2 From 0da9fdf2b9623665a991990e57485a007645eba6 Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Mon, 4 Jan 2021 21:19:16 +0000 Subject: [PATCH 2/3] New upstream version 0.9.4 --- CMakeLists.txt | 147 ++++++++++++++++++++++++++++ README.md | 23 ++++- gl/gl.qrc | 1 + gl/mesh_wireframe.frag | 9 ++ src/app.cpp | 11 ++- src/app.h | 3 +- src/backdrop.cpp | 6 +- src/backdrop.h | 12 +-- src/canvas.cpp | 87 ++++++++++++----- src/canvas.h | 45 ++++----- src/glmesh.cpp | 8 +- src/glmesh.h | 11 ++- src/loader.cpp | 71 ++++++-------- src/mesh.cpp | 4 +- src/mesh.h | 2 +- src/vertex.h | 30 ++++++ src/window.cpp | 215 ++++++++++++++++++++++++++++++++++++++++- src/window.h | 22 ++++- 18 files changed, 584 insertions(+), 123 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 gl/mesh_wireframe.frag create mode 100644 src/vertex.h diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..66cc168 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,147 @@ +#### Fast .stl viewer ##### +# Original Project Author: Matt Keeter Copyright 2014 -2017 +# Author: Paul Tsouchlos Copyright 2017 + +cmake_minimum_required(VERSION 3.3) + +# Setting -std=c++11 +set(CMAKE_CXX_STANDARD 11) +# Setting standard to required, as requisted by DeveloperPaul123 on github +set(CXX_STANDARD_REQUIRED ON) + +# Set the version number +set (FSTL_VERSION_MAJOR "0") +set (FSTL_VERSION_MINOR "9") +set (FSTL_VERSION_PATCH "4") +set (PROJECT_VERSION "${FSTL_VERSION_MAJOR}.${FSTL_VERSION_MINOR}.${FSTL_VERSION_PATCH}") + +project(fstl) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOUIC ON) + +#set project sources +set(Project_Sources src/app.cpp +src/backdrop.cpp +src/canvas.cpp +src/glmesh.cpp +src/loader.cpp +src/main.cpp +src/mesh.cpp +src/window.cpp) + +#set project headers. +set(Project_Headers src/app.h +src/backdrop.h +src/canvas.h +src/glmesh.h +src/loader.h +src/mesh.h +src/window.h) + +#set project resources and icon resource +set(Project_Resources qt/qt.qrc gl/gl.qrc) +set(Icon_Resource exe/fstl.rc) + +#set Policy CMP0072 FindOpenGL behavior +set(OpenGL_GL_PREFERENCE GLVND) + +#find required packages. +find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets OpenGL) +find_package(OpenGL REQUIRED) +find_package(Threads REQUIRED) + +#add resources to RCC +qt5_add_resources(Project_Resources_RCC ${Project_Resources}) + +#tell CMake AUTOGEN to skip autogen on the generated qrc files +set_property(SOURCE ${Project_Resources_RCC} PROPERTY SKIP_AUTOGEN ON) + +#include opengl files. +include_directories(${QT_QTOPENGL_INCLUDE_DIR} ${OPENGL_INCLUDE_DIR} ) + +add_executable(fstl WIN32 ${Project_Sources} ${Project_Headers} ${Project_Resources_RCC} ${Icon_Resource}) +target_link_libraries(fstl Qt5::Widgets Qt5::Core Qt5::Gui Qt5::OpenGL ${OPENGL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) +if(WIN32) + set(Fstl_LINK_FLAGS ${CMAKE_CURRENT_SOURCE_DIR}/${Icon_Resource}) + set_target_properties(fstl PROPERTIES LINK_FLAGS ${Fstl_LINK_FLAGS}) +endif(WIN32) + +# Add version definitions to use within the code. +target_compile_definitions(fstl PRIVATE -DFSTL_VERSION="${PROJECT_VERSION}") + +#installer information that is platform independent +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Fast .stl file viewer.") +set(CPACK_PACKAGE_VERSION_MAJOR ${FSTL_VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${FSTL_VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${FSTL_VERSION_PATCH}) + +if(WIN32) + + set(QT_USE_QTMAIN true) + + if(MSVC) + set_source_files_properties(fstl PROPERTIES LINKER_LANGUAGE "CXX") + set_target_properties(fstl PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS") + + install(TARGETS fstl DESTINATION bin COMPONENT all) + + install(FILES + $ + $ + $ + $ + $ + $ + DESTINATION bin COMPONENT all) + + #install file in the platforms directory. + install (FILES + ${Qt5Core_DIR}/../../../plugins/platforms/qwindows.dll + DESTINATION bin/platforms COMPONENT all + ) + + #custom commands based on: https://gist.github.com/Rod-Persky/e6b93e9ee31f9516261b + add_custom_command(TARGET fstl POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + ) + endif(MSVC) + + # windows specific installer generation information + set(CPACK_GENERATOR NSIS) + set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL OFF) + set(CPACK_NSIS_MODIFY_PATH ON) + set(CPACK_NSIS_MUI_FINISHPAGE_RUN ${PROJECT_NAME}) + set(CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}") + set(CPACK_NSIS_INSTALLED_ICON_NAME "bin\\\\fstl.exe") + set(CPACK_NSIS_URL_INFO_ABOUT "https://github.com/mkeeter/fstl") + set(CPACK_NSIS_DISPLAY_NAME "fstl ${FSTL_VERSION}") + set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/exe/fstl.ico") + set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/exe/fstl.ico") + set(CPACK_NSIS_CREATE_ICONS_EXTRA + "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\fstl.lnk' '$INSTDIR\\\\bin\\\\fstl.exe'") + set(CPACK_COMPONENTS_ALL all) + if (CMAKE_CL_64) + set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64") + else (CMAKE_CL_64) + set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES") + endif (CMAKE_CL_64) +elseif(APPLE) + set(CPACK_GENERATOR "DragNDrop") + set(CPACK_DMG_FORMAT "UDBZ") + set(CPACK_DMG_VOLUME_NAME "${PROJECT_NAME}") + set(CPACK_SYSTEM_NAME "OSX") + set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}") + set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/app/fstl.icns") +else() + install(TARGETS fstl RUNTIME DESTINATION bin) + + set(CPACK_GENERATOR "DEB;RPM") + set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}") +endif(WIN32) + +include(CPack) diff --git a/README.md b/README.md index b4ab04b..a8a3d18 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ Issues and minor pull requests are welcome; the project is under 1K lines of code and should be fairly approachable. ## Screenshot -![Eiffel tower](http://mattkeeter.com/projects/fstl/eiffel.png) +![Eiffel tower](http://mattkeeter.com/projects/fstl/eiffel.png) (credit to [Pranav Panchal](https://grabcad.com/pranav.panchal)) ## Building -The only dependency for `fstl` is [Qt](https://www.qt.io). +The only dependency for `fstl` is [Qt 5](https://www.qt.io). ### macOS @@ -45,6 +45,25 @@ This should produce two new files in the root directory: - `fstl.app` is a standalone application that can be copied to `/Applications` - `fstl.dmg` is a disk image that can be given to a friend +### Linux + +Install Qt with your distro's package manager (required libraries are Core, Gui, +Widgets and OpenGL, e.g. `qt5-default` and `libqt5opengl5-dev` on Debian). + +You can build fstl with qmake (in some distros qmake-qt5) or with CMake: +``` +git clone https://github.com/mkeeter/fstl +cd fstl +mkdir build +cd build + +qmake ../qt/fstl.pro # For qmake build +cmake .. # For CMake build + +make -j8 +./fstl +``` + -------------------------------------------------------------------------------- # License diff --git a/gl/gl.qrc b/gl/gl.qrc index ef93a0e..776226c 100644 --- a/gl/gl.qrc +++ b/gl/gl.qrc @@ -2,6 +2,7 @@ mesh.frag mesh.vert + mesh_wireframe.frag quad.frag quad.vert sphere.stl diff --git a/gl/mesh_wireframe.frag b/gl/mesh_wireframe.frag new file mode 100644 index 0000000..13f001c --- /dev/null +++ b/gl/mesh_wireframe.frag @@ -0,0 +1,9 @@ +#version 120 + +uniform float zoom; + +varying vec3 ec_pos; + +void main() { + gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); +} diff --git a/src/app.cpp b/src/app.cpp index d182048..982d0d0 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -7,11 +7,20 @@ App::App(int& argc, char *argv[]) : QApplication(argc, argv), window(new Window()) { - window->show(); + QCoreApplication::setOrganizationName("mkeeter"); + QCoreApplication::setOrganizationDomain("https://github.com/mkeeter/fstl"); + QCoreApplication::setApplicationName("fstl"); + if (argc > 1) window->load_stl(argv[1]); else window->load_stl(":gl/sphere.stl"); + window->show(); +} + +App::~App() +{ + delete window; } bool App::event(QEvent* e) diff --git a/src/app.h b/src/app.h index 2e06c24..6afb4d7 100644 --- a/src/app.h +++ b/src/app.h @@ -10,8 +10,9 @@ class App : public QApplication Q_OBJECT public: explicit App(int& argc, char *argv[]); + ~App(); protected: - bool event(QEvent* e); + bool event(QEvent* e) override; private: Window* const window; diff --git a/src/backdrop.cpp b/src/backdrop.cpp index 13d0aa3..4ef1bba 100644 --- a/src/backdrop.cpp +++ b/src/backdrop.cpp @@ -2,10 +2,10 @@ Backdrop::Backdrop() { - initializeGLFunctions(); + initializeOpenGLFunctions(); - shader.addShaderFromSourceFile(QGLShader::Vertex, ":/gl/quad.vert"); - shader.addShaderFromSourceFile(QGLShader::Fragment, ":/gl/quad.frag"); + shader.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/gl/quad.vert"); + shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/quad.frag"); shader.link(); float vbuf[] = { diff --git a/src/backdrop.h b/src/backdrop.h index a2398c8..95ae48b 100644 --- a/src/backdrop.h +++ b/src/backdrop.h @@ -1,18 +1,18 @@ #ifndef BACKDROP_H #define BACKDROP_H -#include -#include -#include +#include +#include +#include -class Backdrop : protected QGLFunctions +class Backdrop : protected QOpenGLFunctions { public: Backdrop(); void draw(); private: - QGLShaderProgram shader; - QGLBuffer vertices; + QOpenGLShaderProgram shader; + QOpenGLBuffer vertices; }; #endif // BACKDROP_H diff --git a/src/canvas.cpp b/src/canvas.cpp index b42a2be..dd188ff 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -1,5 +1,4 @@ #include -#include #include @@ -8,11 +7,12 @@ #include "glmesh.h" #include "mesh.h" -Canvas::Canvas(const QGLFormat& format, QWidget *parent) - : QGLWidget(format, parent), mesh(NULL), +Canvas::Canvas(const QSurfaceFormat& format, QWidget *parent) + : QOpenGLWidget(parent), mesh(nullptr), scale(1), zoom(1), tilt(90), yaw(0), perspective(0.25), anim(this, "perspective"), status(" ") { + setFormat(format); QFile styleFile(":/qt/style.qss"); styleFile.open( QFile::ReadOnly ); setStyleSheet(styleFile.readAll()); @@ -22,7 +22,9 @@ Canvas::Canvas(const QGLFormat& format, QWidget *parent) Canvas::~Canvas() { - delete mesh; + makeCurrent(); + delete mesh; + doneCurrent(); } void Canvas::view_anim(float v) @@ -42,6 +44,16 @@ void Canvas::view_perspective() view_anim(0.25); } +void Canvas::draw_shaded() +{ + set_drawMode(0); +} + +void Canvas::draw_wireframe() +{ + set_drawMode(1); +} + void Canvas::load_mesh(Mesh* m, bool is_reload) { mesh = new GLMesh(m); @@ -76,6 +88,12 @@ void Canvas::set_perspective(float p) update(); } +void Canvas::set_drawMode(int mode) +{ + drawMode = mode; + update(); +} + void Canvas::clear_status() { status = ""; @@ -84,59 +102,76 @@ void Canvas::clear_status() void Canvas::initializeGL() { - initializeGLFunctions(); + initializeOpenGLFunctions(); - mesh_shader.addShaderFromSourceFile(QGLShader::Vertex, ":/gl/mesh.vert"); - mesh_shader.addShaderFromSourceFile(QGLShader::Fragment, ":/gl/mesh.frag"); + mesh_shader.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/gl/mesh.vert"); + mesh_shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/mesh.frag"); mesh_shader.link(); + mesh_wireframe_shader.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/gl/mesh.vert"); + mesh_wireframe_shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/mesh_wireframe.frag"); + mesh_wireframe_shader.link(); backdrop = new Backdrop(); } -void Canvas::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - glClearColor(0.0, 0.0, 0.0, 0.0); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - glEnable(GL_DEPTH_TEST); +void Canvas::paintGL() +{ + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glEnable(GL_DEPTH_TEST); - backdrop->draw(); - if (mesh) draw_mesh(); + backdrop->draw(); + if (mesh) draw_mesh(); - if (status.isNull()) return; + if (status.isNull()) return; - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - painter.drawText(10, height() - 10, status); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawText(10, height() - 10, status); } - void Canvas::draw_mesh() { - mesh_shader.bind(); + QOpenGLShaderProgram* selected_mesh_shader = NULL; + // Set gl draw mode + if(drawMode == 1) + { + selected_mesh_shader = &mesh_wireframe_shader; + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + } + else + { + selected_mesh_shader = &mesh_shader; + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + } + + selected_mesh_shader->bind(); // Load the transform and view matrices into the shader glUniformMatrix4fv( - mesh_shader.uniformLocation("transform_matrix"), + selected_mesh_shader->uniformLocation("transform_matrix"), 1, GL_FALSE, transform_matrix().data()); glUniformMatrix4fv( - mesh_shader.uniformLocation("view_matrix"), + selected_mesh_shader->uniformLocation("view_matrix"), 1, GL_FALSE, view_matrix().data()); // Compensate for z-flattening when zooming - glUniform1f(mesh_shader.uniformLocation("zoom"), 1/zoom); + glUniform1f(selected_mesh_shader->uniformLocation("zoom"), 1/zoom); // Find and enable the attribute location for vertex position - const GLuint vp = mesh_shader.attributeLocation("vertex_position"); + const GLuint vp = selected_mesh_shader->attributeLocation("vertex_position"); glEnableVertexAttribArray(vp); // Then draw the mesh with that vertex position mesh->draw(vp); + // Reset draw mode for the background and anything else that needs to be drawn + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + // Clean up state machine glDisableVertexAttribArray(vp); - mesh_shader.release(); + selected_mesh_shader->release(); } QMatrix4x4 Canvas::transform_matrix() const diff --git a/src/canvas.h b/src/canvas.h index 4dae298..7fa80c8 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -1,55 +1,55 @@ #ifndef CANVAS_H #define CANVAS_H -#include -#include -#include -#include -#include -#include +#include +#include +#include class GLMesh; class Mesh; class Backdrop; -class Canvas : public QGLWidget, protected QGLFunctions +class Canvas : public QOpenGLWidget, protected QOpenGLFunctions { Q_OBJECT public: - Canvas(const QGLFormat& format, QWidget* parent=0); - - void initializeGL(); - void paintEvent(QPaintEvent* event); + explicit Canvas(const QSurfaceFormat& format, QWidget* parent=0); ~Canvas(); void view_orthographic(); void view_perspective(); + void draw_shaded(); + void draw_wireframe(); public slots: void set_status(const QString& s); void clear_status(); void load_mesh(Mesh* m, bool is_reload); - protected: - void mousePressEvent(QMouseEvent* event); - void mouseReleaseEvent(QMouseEvent* event); - void mouseMoveEvent(QMouseEvent* event); - void wheelEvent(QWheelEvent* event); - void resizeGL(int width, int height); - void set_perspective(float p); + void paintGL() override; + void initializeGL() override; + void resizeGL(int width, int height) override; + + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + + void set_perspective(float p); + void set_drawMode(int mode); void view_anim(float v); - private: void draw_mesh(); QMatrix4x4 transform_matrix() const; QMatrix4x4 view_matrix() const; - QGLShaderProgram mesh_shader; - QGLShaderProgram quad_shader; + QOpenGLShaderProgram mesh_shader; + QOpenGLShaderProgram mesh_wireframe_shader; + QOpenGLShaderProgram quad_shader; GLMesh* mesh; Backdrop* backdrop; @@ -61,7 +61,8 @@ private: float yaw; float perspective; - Q_PROPERTY(float perspective WRITE set_perspective); + int drawMode; + Q_PROPERTY(float perspective MEMBER perspective WRITE set_perspective); QPropertyAnimation anim; QPoint mouse_pos; diff --git a/src/glmesh.cpp b/src/glmesh.cpp index 7053809..863f558 100644 --- a/src/glmesh.cpp +++ b/src/glmesh.cpp @@ -2,15 +2,15 @@ #include "mesh.h" GLMesh::GLMesh(const Mesh* const mesh) - : vertices(QGLBuffer::VertexBuffer), indices(QGLBuffer::IndexBuffer) + : vertices(QOpenGLBuffer::VertexBuffer), indices(QOpenGLBuffer::IndexBuffer) { - initializeGLFunctions(); + initializeOpenGLFunctions(); vertices.create(); indices.create(); - vertices.setUsagePattern(QGLBuffer::StaticDraw); - indices.setUsagePattern(QGLBuffer::StaticDraw); + vertices.setUsagePattern(QOpenGLBuffer::StaticDraw); + indices.setUsagePattern(QOpenGLBuffer::StaticDraw); vertices.bind(); vertices.allocate(mesh->vertices.data(), diff --git a/src/glmesh.h b/src/glmesh.h index 74a193f..5c47c2d 100644 --- a/src/glmesh.h +++ b/src/glmesh.h @@ -1,19 +1,20 @@ #ifndef GLMESH_H #define GLMESH_H -#include -#include +#include +#include +// forward declaration class Mesh; -class GLMesh : protected QGLFunctions +class GLMesh : protected QOpenGLFunctions { public: GLMesh(const Mesh* const mesh); void draw(GLuint vp); private: - QGLBuffer vertices; - QGLBuffer indices; + QOpenGLBuffer vertices; + QOpenGLBuffer indices; }; #endif // GLMESH_H diff --git a/src/loader.cpp b/src/loader.cpp index 93f5525..b5a2f04 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -1,6 +1,7 @@ #include #include "loader.h" +#include "vertex.h" Loader::Loader(QObject* parent, const QString& filename, bool is_reload) : QThread(parent), filename(filename), is_reload(is_reload) @@ -28,25 +29,7 @@ void Loader::run() //////////////////////////////////////////////////////////////////////////////// -struct Vec3 -{ - GLfloat x, y, z; - bool operator!=(const Vec3& rhs) const - { - return x != rhs.x || y != rhs.y || z != rhs.z; - } - bool operator<(const Vec3& rhs) const - { - if (x != rhs.x) return x < rhs.x; - else if (y != rhs.y) return y < rhs.y; - else if (z != rhs.z) return z < rhs.z; - else return false; - } -}; - -typedef std::pair Vec3i; - -void parallel_sort(Vec3i* begin, Vec3i* end, int threads) +void parallel_sort(Vertex* begin, Vertex* end, int threads) { if (threads < 2 || end - begin < 2) { @@ -72,17 +55,25 @@ void parallel_sort(Vec3i* begin, Vec3i* end, int threads) } } -Mesh* mesh_from_verts(uint32_t tri_count, QVector& verts) +Mesh* mesh_from_verts(uint32_t tri_count, QVector& verts) { // Save indicies as the second element in the array // (so that we can reconstruct triangle order after sorting) for (size_t i=0; i < tri_count*3; ++i) { - verts[i].second = i; + verts[i].i = i; + } + + // Check how many threads the hardware can safely support. This may return + // 0 if the property can't be read so we shoud check for that too. + auto threads = std::thread::hardware_concurrency(); + if (threads == 0) + { + threads = 8; } // Sort the set of vertices (to deduplicate) - parallel_sort(verts.begin(), verts.end(), 8); + parallel_sort(verts.begin(), verts.end(), threads); // This vector will store triangles as sets of 3 indices std::vector indices(tri_count*3); @@ -94,11 +85,11 @@ Mesh* mesh_from_verts(uint32_t tri_count, QVector& verts) size_t vertex_count = 0; for (auto v : verts) { - if (!vertex_count || v.first != verts[vertex_count-1].first) + if (!vertex_count || v != verts[vertex_count-1]) { verts[vertex_count++] = v; } - indices[v.second] = vertex_count - 1; + indices[v.i] = vertex_count - 1; } verts.resize(vertex_count); @@ -106,12 +97,12 @@ Mesh* mesh_from_verts(uint32_t tri_count, QVector& verts) flat_verts.reserve(vertex_count*3); for (auto v : verts) { - flat_verts.push_back(v.first.x); - flat_verts.push_back(v.first.y); - flat_verts.push_back(v.first.z); + flat_verts.push_back(v.x); + flat_verts.push_back(v.y); + flat_verts.push_back(v.z); } - return new Mesh(flat_verts, indices); + return new Mesh(std::move(flat_verts), std::move(indices)); } //////////////////////////////////////////////////////////////////////////////// @@ -126,7 +117,7 @@ Mesh* Loader::load_stl() } // First, try to read the stl as an ASCII file - if (file.read(6) == "solid ") + if (file.read(5) == "solid") { file.readLine(); // skip solid name const auto line = file.readLine().trimmed(); @@ -167,35 +158,31 @@ Mesh* Loader::read_stl_binary(QFile& file) } // Extract vertices into an array of xyz, unsigned pairs - QVector verts(tri_count*3); + QVector verts(tri_count*3); // Dummy array, because readRawData is faster than skipRawData - uint8_t* buffer = (uint8_t*)malloc(tri_count * 50); - data.readRawData((char*)buffer, tri_count * 50); + std::unique_ptr buffer(new uint8_t[tri_count * 50]); + data.readRawData((char*)buffer.get(), tri_count * 50); // Store vertices in the array, processing one triangle at a time. - auto b = buffer; + auto b = buffer.get() + 3 * sizeof(float); for (auto v=verts.begin(); v != verts.end(); v += 3) { - // Skip face's normal vector - b += 3 * sizeof(float); - // Load vertex data from .stl file into vertices for (unsigned i=0; i < 3; ++i) { - memcpy(&v[i].first, b, 3*sizeof(float)); + memcpy(&v[i], b, 3*sizeof(float)); b += 3 * sizeof(float); } - // Skip face attribute - b += sizeof(uint16_t); + // Skip face attribute and next face's normal vector + b += 3 * sizeof(float) + sizeof(uint16_t); } if (confusing_stl) { emit warning_confusing_stl(); } - free(buffer); return mesh_from_verts(tri_count, verts); } @@ -204,7 +191,7 @@ Mesh* Loader::read_stl_ascii(QFile& file) { file.readLine(); uint32_t tri_count = 0; - QVector verts(tri_count*3); + QVector verts(tri_count*3); bool okay = true; while (!file.atEnd() && okay) @@ -232,7 +219,7 @@ Mesh* Loader::read_stl_ascii(QFile& file) const float x = line[1].toFloat(&okay); const float y = line[2].toFloat(&okay); const float z = line[3].toFloat(&okay); - verts.push_back({{x, y, z}, 0}); + verts.push_back(Vertex(x, y, z)); } if (!file.readLine().trimmed().startsWith("endloop") || !file.readLine().trimmed().startsWith("endfacet")) diff --git a/src/mesh.cpp b/src/mesh.cpp index 140a769..b971029 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -8,8 +8,8 @@ //////////////////////////////////////////////////////////////////////////////// -Mesh::Mesh(std::vector v, std::vector i) - : vertices(v), indices(i) +Mesh::Mesh(std::vector&& v, std::vector&& i) + : vertices(std::move(v)), indices(std::move(i)) { // Nothing to do here } diff --git a/src/mesh.h b/src/mesh.h index 1412781..c4e888b 100644 --- a/src/mesh.h +++ b/src/mesh.h @@ -9,7 +9,7 @@ class Mesh { public: - Mesh(std::vector vertices, std::vector indices); + Mesh(std::vector&& vertices, std::vector&& indices); float min(size_t start) const; float max(size_t start) const; diff --git a/src/vertex.h b/src/vertex.h new file mode 100644 index 0000000..9738a75 --- /dev/null +++ b/src/vertex.h @@ -0,0 +1,30 @@ +#ifndef VEC3_H +#define VEC3_H + +#include + +/* + * Represents an optionally-indexed vertex in space + */ +struct Vertex +{ + Vertex() {} + Vertex(float x, float y, float z) : x(x), y(y), z(z) {} + + GLfloat x, y, z; + GLuint i=0; + + bool operator!=(const Vertex& rhs) const + { + return x != rhs.x || y != rhs.y || z != rhs.z; + } + bool operator<(const Vertex& rhs) const + { + if (x != rhs.x) return x < rhs.x; + else if (y != rhs.y) return y < rhs.y; + else if (z != rhs.z) return z < rhs.z; + else return false; + } +}; + +#endif diff --git a/src/window.cpp b/src/window.cpp index 0954b33..09d74ed 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -1,6 +1,4 @@ #include -#include -#include #include "window.h" #include "canvas.h" @@ -15,8 +13,11 @@ Window::Window(QWidget *parent) : quit_action(new QAction("Quit", this)), perspective_action(new QAction("Perspective", this)), orthogonal_action(new QAction("Orthographic", this)), + shaded_action(new QAction("Shaded", this)), + wireframe_action(new QAction("Wireframe", this)), reload_action(new QAction("Reload", this)), autoreload_action(new QAction("Autoreload", this)), + save_screenshot_action(new QAction("Save Screenshot", this)), recent_files(new QMenu("Open recent", this)), recent_files_group(new QActionGroup(this)), recent_files_clear_action(new QAction("Clear recent files", this)), @@ -26,10 +27,14 @@ Window::Window(QWidget *parent) : setWindowTitle("fstl"); setAcceptDrops(true); - QGLFormat format; + QSurfaceFormat format; + format.setDepthBufferSize(24); + format.setStencilBufferSize(8); format.setVersion(2, 1); - format.setSampleBuffers(true); + format.setProfile(QSurfaceFormat::CoreProfile); + QSurfaceFormat::setDefaultFormat(format); + canvas = new Canvas(format, this); setCentralWidget(canvas); @@ -63,6 +68,10 @@ Window::Window(QWidget *parent) : QObject::connect(recent_files_group, &QActionGroup::triggered, this, &Window::on_load_recent); + save_screenshot_action->setCheckable(false); + QObject::connect(save_screenshot_action, &QAction::triggered, + this, &Window::on_save_screenshot); + rebuild_recent_files(); auto file_menu = menuBar()->addMenu("File"); @@ -71,6 +80,7 @@ Window::Window(QWidget *parent) : file_menu->addSeparator(); file_menu->addAction(reload_action); file_menu->addAction(autoreload_action); + file_menu->addAction(save_screenshot_action); file_menu->addAction(quit_action); auto view_menu = menuBar()->addMenu("View"); @@ -88,6 +98,20 @@ Window::Window(QWidget *parent) : QObject::connect(projections, &QActionGroup::triggered, this, &Window::on_projection); + auto draw_menu = view_menu->addMenu("Draw Mode"); + draw_menu->addAction(shaded_action); + draw_menu->addAction(wireframe_action); + auto drawModes = new QActionGroup(draw_menu); + for (auto p : {shaded_action, wireframe_action}) + { + drawModes->addAction(p); + p->setCheckable(true); + } + shaded_action->setChecked(true); + drawModes->setExclusive(true); + QObject::connect(drawModes, &QActionGroup::triggered, + this, &Window::on_drawMode); + auto help_menu = menuBar()->addMenu("Help"); help_menu->addAction(about_action); @@ -98,7 +122,7 @@ void Window::on_open() { QString filename = QFileDialog::getOpenFileName( this, "Load .stl file", QString(), "*.stl"); - if (not filename.isNull()) + if (!filename.isNull()) { load_stl(filename); } @@ -190,6 +214,18 @@ void Window::on_projection(QAction* proj) } } +void Window::on_drawMode(QAction* mode) +{ + if (mode == shaded_action) + { + canvas->draw_shaded(); + } + else + { + canvas->draw_wireframe(); + } +} + void Window::on_watched_change(const QString& filename) { if (autoreload_action->isChecked()) @@ -218,6 +254,45 @@ void Window::on_load_recent(QAction* a) load_stl(a->data().toString()); } +void Window::on_loaded(const QString& filename) +{ + current_file = filename; +} + +void Window::on_save_screenshot() +{ + const auto image = canvas->grabFramebuffer(); + auto file_name = QFileDialog::getSaveFileName( + this, + tr("Save Screenshot Image"), + QStandardPaths::standardLocations(QStandardPaths::StandardLocation::PicturesLocation).first(), + "Images (*.png *.jpg)"); + + auto get_file_extension = [](const std::string& file_name) -> std::string + { + const auto location = std::find(file_name.rbegin(), file_name.rend(), '.'); + if (location == file_name.rend()) + { + return ""; + } + + const auto index = std::distance(file_name.rbegin(), location); + return file_name.substr(file_name.size() - index); + }; + + const auto extension = get_file_extension(file_name.toStdString()); + if(extension.empty() || (extension != "png" && extension != "jpg")) + { + file_name.append(".png"); + } + + const auto save_ok = image.save(file_name); + if(!save_ok) + { + QMessageBox::warning(this, tr("Error Saving Image"), tr("Unable to save screen shot image.")); + } +} + void Window::rebuild_recent_files() { QSettings settings; @@ -290,6 +365,8 @@ bool Window::load_stl(const QString& filename, bool is_reload) this, &Window::setWindowTitle); connect(loader, &Loader::loaded_file, this, &Window::set_watched); + connect(loader, &Loader::loaded_file, + this, &Window::on_loaded); autoreload_action->setEnabled(true); reload_action->setEnabled(true); } @@ -312,3 +389,131 @@ void Window::dropEvent(QDropEvent *event) { load_stl(event->mimeData()->urls().front().toLocalFile()); } + +void Window::sorted_insert(QStringList& list, const QCollator& collator, const QString& value) +{ + int start = 0; + int end = list.size() - 1; + int index = 0; + while (start <= end){ + int mid = (start+end)/2; + if (list[mid] == value) { + return; + } + int compare = collator.compare(value, list[mid]); + if (compare < 0) { + end = mid-1; + index = mid; + } else { + start = mid+1; + index = start; + } + } + + list.insert(index, value); +} + +void Window::build_folder_file_list() +{ + QString current_folder_path = QFileInfo(current_file).absoluteDir().absolutePath(); + if (!lookup_folder_files.isEmpty()) + { + if (current_folder_path == lookup_folder) { + return; + } + + lookup_folder_files.clear(); + } + lookup_folder = current_folder_path; + + QCollator collator; + collator.setNumericMode(true); + + QDirIterator dirIterator(lookup_folder, QStringList() << "*.stl", QDir::Files | QDir::Readable | QDir::Hidden); + while (dirIterator.hasNext()) { + dirIterator.next(); + + QString name = dirIterator.fileName(); + sorted_insert(lookup_folder_files, collator, name); + } +} + +QPair Window::get_file_neighbors() +{ + if (current_file.isEmpty()) { + return QPair(QString::null, QString::null); + } + + build_folder_file_list(); + + QFileInfo fileInfo(current_file); + + QString current_dir = fileInfo.absoluteDir().absolutePath(); + QString current_name = fileInfo.fileName(); + + QString prev = QString::null; + QString next = QString::null; + + QListIterator fileIterator(lookup_folder_files); + while (fileIterator.hasNext()) { + QString name = fileIterator.next(); + + if (name == current_name) { + if (fileIterator.hasNext()) { + next = current_dir + QDir::separator() + fileIterator.next(); + } + break; + } + + prev = name; + } + + if (!prev.isEmpty()) { + prev.prepend(QDir::separator()); + prev.prepend(current_dir); + } + + return QPair(prev, next); +} + +bool Window::load_prev(void) +{ + QPair neighbors = get_file_neighbors(); + if (neighbors.first.isEmpty()) { + return false; + } + + return load_stl(neighbors.first); +} + +bool Window::load_next(void) +{ + QPair neighbors = get_file_neighbors(); + if (neighbors.second.isEmpty()) { + return false; + } + + return load_stl(neighbors.second); +} + +void Window::keyPressEvent(QKeyEvent* event) +{ + if (!open_action->isEnabled()) + { + QMainWindow::keyPressEvent(event); + return; + } + + if (event->key() == Qt::Key_Left) + { + load_prev(); + return; + } + else if (event->key() == Qt::Key_Right) + { + load_next(); + return; + } + + QMainWindow::keyPressEvent(event); +} diff --git a/src/window.h b/src/window.h index fd0a770..8619b4d 100644 --- a/src/window.h +++ b/src/window.h @@ -4,6 +4,7 @@ #include #include #include +#include class Canvas; @@ -13,10 +14,13 @@ class Window : public QMainWindow public: explicit Window(QWidget* parent=0); bool load_stl(const QString& filename, bool is_reload=false); + bool load_prev(void); + bool load_next(void); protected: - void dragEnterEvent(QDragEnterEvent* event); - void dropEvent(QDropEvent* event); + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; public slots: void on_open(); @@ -33,28 +37,40 @@ public slots: private slots: void on_projection(QAction* proj); + void on_drawMode(QAction* mode); void on_watched_change(const QString& filename); void on_reload(); void on_autoreload_triggered(bool r); void on_clear_recent(); void on_load_recent(QAction* a); - + void on_loaded(const QString& filename); + void on_save_screenshot(); + private: void rebuild_recent_files(); + void sorted_insert(QStringList& list, const QCollator& collator, const QString& value); + void build_folder_file_list(); + QPair get_file_neighbors(); QAction* const open_action; QAction* const about_action; QAction* const quit_action; QAction* const perspective_action; QAction* const orthogonal_action; + QAction* const shaded_action; + QAction* const wireframe_action; QAction* const reload_action; QAction* const autoreload_action; + QAction* const save_screenshot_action; QMenu* const recent_files; QActionGroup* const recent_files_group; QAction* const recent_files_clear_action; const static int MAX_RECENT_FILES=8; const static QString RECENT_FILE_KEY; + QString current_file; + QString lookup_folder; + QStringList lookup_folder_files; QFileSystemWatcher* watcher; -- 2.39.2 From dd4a156f8787b310568d82161bddd478b0938499 Mon Sep 17 00:00:00 2001 From: Jakob Haufe Date: Wed, 29 Jun 2022 16:02:12 +0200 Subject: [PATCH 3/3] New upstream version 0.10.0 --- CMakeLists.txt | 139 +++++++++++++++++--------------- README.md | 70 +++++++++++++--- app/package.sh | 2 +- gl/colored_lines.frag | 7 ++ gl/colored_lines.vert | 14 ++++ gl/gl.qrc | 3 + gl/mesh_surfaceangle.frag | 17 ++++ qt/fstl.pro | 51 ------------ src/app.cpp | 6 +- src/axis.cpp | 163 ++++++++++++++++++++++++++++++++++++++ src/axis.h | 22 +++++ src/canvas.cpp | 144 +++++++++++++++++++++------------ src/canvas.h | 36 ++++++--- src/loader.cpp | 19 ++--- src/loader.h | 5 -- src/main.cpp | 4 + src/mesh.cpp | 4 + src/mesh.h | 1 + src/window.cpp | 159 +++++++++++++++++++++++++++++-------- src/window.h | 22 ++++- 20 files changed, 637 insertions(+), 251 deletions(-) create mode 100644 gl/colored_lines.frag create mode 100644 gl/colored_lines.vert create mode 100644 gl/mesh_surfaceangle.frag delete mode 100644 qt/fstl.pro create mode 100644 src/axis.cpp create mode 100644 src/axis.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 66cc168..0628afa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,8 @@ cmake_minimum_required(VERSION 3.3) +project(fstl) + # Setting -std=c++11 set(CMAKE_CXX_STANDARD 11) # Setting standard to required, as requisted by DeveloperPaul123 on github @@ -11,11 +13,11 @@ set(CXX_STANDARD_REQUIRED ON) # Set the version number set (FSTL_VERSION_MAJOR "0") -set (FSTL_VERSION_MINOR "9") -set (FSTL_VERSION_PATCH "4") +set (FSTL_VERSION_MINOR "10") +set (FSTL_VERSION_PATCH "0") set (PROJECT_VERSION "${FSTL_VERSION_MAJOR}.${FSTL_VERSION_MINOR}.${FSTL_VERSION_PATCH}") -project(fstl) +message(STATUS "Version: ${PROJECT_VERSION}") set(CMAKE_AUTOMOC ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) @@ -24,6 +26,7 @@ set(CMAKE_AUTOUIC ON) #set project sources set(Project_Sources src/app.cpp src/backdrop.cpp +src/axis.cpp src/canvas.cpp src/glmesh.cpp src/loader.cpp @@ -34,6 +37,7 @@ src/window.cpp) #set project headers. set(Project_Headers src/app.h src/backdrop.h +src/axis.h src/canvas.h src/glmesh.h src/loader.h @@ -48,7 +52,7 @@ set(Icon_Resource exe/fstl.rc) set(OpenGL_GL_PREFERENCE GLVND) #find required packages. -find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets OpenGL) +find_package(Qt5 5.14 REQUIRED COMPONENTS Core Gui Widgets OpenGL) find_package(OpenGL REQUIRED) find_package(Threads REQUIRED) @@ -61,13 +65,18 @@ set_property(SOURCE ${Project_Resources_RCC} PROPERTY SKIP_AUTOGEN ON) #include opengl files. include_directories(${QT_QTOPENGL_INCLUDE_DIR} ${OPENGL_INCLUDE_DIR} ) -add_executable(fstl WIN32 ${Project_Sources} ${Project_Headers} ${Project_Resources_RCC} ${Icon_Resource}) -target_link_libraries(fstl Qt5::Widgets Qt5::Core Qt5::Gui Qt5::OpenGL ${OPENGL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) if(WIN32) + add_executable(fstl WIN32 ${Project_Sources} ${Project_Headers} ${Project_Resources_RCC} ${Icon_Resource}) set(Fstl_LINK_FLAGS ${CMAKE_CURRENT_SOURCE_DIR}/${Icon_Resource}) set_target_properties(fstl PROPERTIES LINK_FLAGS ${Fstl_LINK_FLAGS}) +elseif(APPLE) + add_executable(fstl MACOSX_BUNDLE ${Project_Sources} ${Project_Headers} ${Project_Resources_RCC} ${Icon_Resource}) +else() + add_executable(fstl ${Project_Sources} ${Project_Headers} ${Project_Resources_RCC} ${Icon_Resource}) endif(WIN32) +target_link_libraries(fstl Qt5::Widgets Qt5::Core Qt5::Gui Qt5::OpenGL ${OPENGL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) + # Add version definitions to use within the code. target_compile_definitions(fstl PRIVATE -DFSTL_VERSION="${PROJECT_VERSION}") @@ -79,69 +88,69 @@ set(CPACK_PACKAGE_VERSION_PATCH ${FSTL_VERSION_PATCH}) if(WIN32) - set(QT_USE_QTMAIN true) - - if(MSVC) - set_source_files_properties(fstl PROPERTIES LINKER_LANGUAGE "CXX") - set_target_properties(fstl PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS") - - install(TARGETS fstl DESTINATION bin COMPONENT all) - - install(FILES - $ - $ - $ - $ - $ - $ - DESTINATION bin COMPONENT all) - - #install file in the platforms directory. - install (FILES - ${Qt5Core_DIR}/../../../plugins/platforms/qwindows.dll - DESTINATION bin/platforms COMPONENT all - ) - - #custom commands based on: https://gist.github.com/Rod-Persky/e6b93e9ee31f9516261b - add_custom_command(TARGET fstl POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ - ) - endif(MSVC) - - # windows specific installer generation information - set(CPACK_GENERATOR NSIS) - set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL OFF) - set(CPACK_NSIS_MODIFY_PATH ON) - set(CPACK_NSIS_MUI_FINISHPAGE_RUN ${PROJECT_NAME}) - set(CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}") - set(CPACK_NSIS_INSTALLED_ICON_NAME "bin\\\\fstl.exe") - set(CPACK_NSIS_URL_INFO_ABOUT "https://github.com/mkeeter/fstl") - set(CPACK_NSIS_DISPLAY_NAME "fstl ${FSTL_VERSION}") - set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/exe/fstl.ico") - set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/exe/fstl.ico") - set(CPACK_NSIS_CREATE_ICONS_EXTRA - "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\fstl.lnk' '$INSTDIR\\\\bin\\\\fstl.exe'") - set(CPACK_COMPONENTS_ALL all) - if (CMAKE_CL_64) - set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64") - else (CMAKE_CL_64) - set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES") - endif (CMAKE_CL_64) + set(QT_USE_QTMAIN true) + + if(MSVC) + set_source_files_properties(fstl PROPERTIES LINKER_LANGUAGE "CXX") + set_target_properties(fstl PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS") + + install(TARGETS fstl DESTINATION bin COMPONENT all) + + install(FILES + $ + $ + $ + $ + $ + $ + DESTINATION bin COMPONENT all) + + #install file in the platforms directory. + install (FILES + ${Qt5Core_DIR}/../../../plugins/platforms/qwindows.dll + DESTINATION bin/platforms COMPONENT all + ) + + #custom commands based on: https://gist.github.com/Rod-Persky/e6b93e9ee31f9516261b + add_custom_command(TARGET fstl POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + ) + endif(MSVC) + + # windows specific installer generation information + set(CPACK_GENERATOR NSIS) + set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL OFF) + set(CPACK_NSIS_MODIFY_PATH ON) + set(CPACK_NSIS_MUI_FINISHPAGE_RUN ${PROJECT_NAME}) + set(CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}") + set(CPACK_NSIS_INSTALLED_ICON_NAME "bin\\\\fstl.exe") + set(CPACK_NSIS_URL_INFO_ABOUT "https://github.com/fstl-app/fstl") + set(CPACK_NSIS_DISPLAY_NAME "fstl ${FSTL_VERSION}") + set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/exe/fstl.ico") + set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/exe/fstl.ico") + set(CPACK_NSIS_CREATE_ICONS_EXTRA + "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\fstl.lnk' '$INSTDIR\\\\bin\\\\fstl.exe'") + set(CPACK_COMPONENTS_ALL all) + if (CMAKE_CL_64) + set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64") + else (CMAKE_CL_64) + set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES") + endif (CMAKE_CL_64) elseif(APPLE) - set(CPACK_GENERATOR "DragNDrop") - set(CPACK_DMG_FORMAT "UDBZ") - set(CPACK_DMG_VOLUME_NAME "${PROJECT_NAME}") - set(CPACK_SYSTEM_NAME "OSX") - set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}") + set(CPACK_GENERATOR "DragNDrop") + set(CPACK_DMG_FORMAT "UDBZ") + set(CPACK_DMG_VOLUME_NAME "${PROJECT_NAME}") + set(CPACK_SYSTEM_NAME "OSX") + set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}") set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/app/fstl.icns") else() - install(TARGETS fstl RUNTIME DESTINATION bin) + install(TARGETS fstl RUNTIME DESTINATION bin) - set(CPACK_GENERATOR "DEB;RPM") - set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}") + set(CPACK_GENERATOR "DEB;RPM") + set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}") endif(WIN32) include(CPack) diff --git a/README.md b/README.md index a8a3d18..c58eeae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # fstl -`fstl` is the fastest [.stl file](http://en.wikipedia.org/wiki/STL_\(file_format\)) viewer. +`fstl` is a very fast [.stl file](http://en.wikipedia.org/wiki/STL_\(file_format\)) viewer. + +It was originally written by [Matt Keeter](https://mattkeeter.com), +and is now primarily maintained by [@DeveloperPaul123](https://github.com/DeveloperPaul123). It is designed to quickly load and render very high-polygon models; showing 2 million triangles at 60+ FPS on a mid-range laptop. @@ -11,29 +14,75 @@ Issues and minor pull requests are welcome; the project is under 1K lines of code and should be fairly approachable. ## Screenshot + ![Eiffel tower](http://mattkeeter.com/projects/fstl/eiffel.png) (credit to [Pranav Panchal](https://grabcad.com/pranav.panchal)) +## Setting `fstl` as the Default STL Viewer + +### Windows + +1. Right-click an STL file +2. Select `Open With` >>> `Choose another app` +3. Select `More Apps` and `Look for another app on this PC` +4. Enter the path to the `fstl` EXE file + +### MacOS + +1. Ctrl+click an STL file +2. Select `Get Info` +3. Navigate to the `Open with` section +4. Select `fstl` in the dropdown +5. Click `Change All` + +### Linux + +If `mimeopen` is available on your system, it can be used to set `fstl` as the default viewer for STL files. +Run the following in your terminal: + +```bash +# replace example.stl with an actual file +mimeopen -d example.stl +``` + +The following output will result: + +``` +Please choose a default application for files of type model/stl + + 1) Other... + +use application # +``` + +Select the `Other` option and type `fstl` as the desired command to open STL files. +This will now become the system default, even when opening files from the file manager. + ## Building -The only dependency for `fstl` is [Qt 5](https://www.qt.io). +The only dependency for `fstl` is [Qt 5](https://www.qt.io), +plus [`cmake`](https://cmake.org/) for building. ### macOS -Install Qt from their website or [Homebrew](brew.sh), -making sure `qmake` is on your shell's path. +Install Qt from their website or [Homebrew](brew.sh). + +Install `cmake` through Homebrew or equivalent. Then, run through the following set of commands in a shell: + ``` -git clone https://github.com/mkeeter/fstl +git clone https://github.com/fstl-app/fstl cd fstl mkdir build cd build -qmake ../qt/fstl.pro +cmake -DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt/5.15.0/ .. make -j8 ./fstl.app/Contents/MacOS/fstl ``` +You may need to edit the Qt path depending on your installation. + To package a standalone app, go to the app directory and run `package.sh` ``` @@ -50,16 +99,13 @@ This should produce two new files in the root directory: Install Qt with your distro's package manager (required libraries are Core, Gui, Widgets and OpenGL, e.g. `qt5-default` and `libqt5opengl5-dev` on Debian). -You can build fstl with qmake (in some distros qmake-qt5) or with CMake: +You can build fstl with CMake: ``` -git clone https://github.com/mkeeter/fstl +git clone https://github.com/fstl-app/fstl cd fstl mkdir build cd build - -qmake ../qt/fstl.pro # For qmake build -cmake .. # For CMake build - +cmake .. make -j8 ./fstl ``` diff --git a/app/package.sh b/app/package.sh index bdb4999..6cf065b 100755 --- a/app/package.sh +++ b/app/package.sh @@ -12,6 +12,7 @@ APP=fstl MACDEPLOYQT=`otool -L $APP.app/Contents/MacOS/fstl | sed -n -e "s:\(.*\)lib/QtCore.*:\1/bin/macdeployqt:gp"` $MACDEPLOYQT $APP.app +cp ../app/Info.plist $APP.app/Contents/ # Delete unused Qt plugins cd fstl.app/Contents/PlugIns @@ -41,7 +42,6 @@ do done cd ../Resources -rm empty.lproj # Create a disk image cd ../../.. diff --git a/gl/colored_lines.frag b/gl/colored_lines.frag new file mode 100644 index 0000000..1c02e1b --- /dev/null +++ b/gl/colored_lines.frag @@ -0,0 +1,7 @@ +#version 120 + +varying vec3 frag_color; + +void main() { + gl_FragColor = vec4(frag_color, 1.0); +} diff --git a/gl/colored_lines.vert b/gl/colored_lines.vert new file mode 100644 index 0000000..2100403 --- /dev/null +++ b/gl/colored_lines.vert @@ -0,0 +1,14 @@ +#version 120 +attribute vec3 vertex_position; +attribute vec3 vertex_color; + +uniform mat4 transform_matrix; +uniform mat4 view_matrix; + +varying vec3 frag_color; + +void main() { + gl_Position = view_matrix*transform_matrix* + vec4(vertex_position, 1.0); + frag_color = vertex_color; +} diff --git a/gl/gl.qrc b/gl/gl.qrc index 776226c..6e8baa9 100644 --- a/gl/gl.qrc +++ b/gl/gl.qrc @@ -3,8 +3,11 @@ mesh.frag mesh.vert mesh_wireframe.frag + mesh_surfaceangle.frag quad.frag quad.vert + colored_lines.frag + colored_lines.vert sphere.stl diff --git a/gl/mesh_surfaceangle.frag b/gl/mesh_surfaceangle.frag new file mode 100644 index 0000000..3ee94dd --- /dev/null +++ b/gl/mesh_surfaceangle.frag @@ -0,0 +1,17 @@ +#version 120 + +uniform float zoom; + +varying vec3 ec_pos; + +void main() { + vec3 ec_normal = normalize(cross(dFdx(ec_pos), dFdy(ec_pos))); + ec_normal.z *= zoom; + ec_normal = normalize(ec_normal); + //rotated 10deg around the red axis for better color match + float x = dot(ec_normal, vec3(1.0, 0.0, 0.0)); + float y = dot(ec_normal, vec3(0.0, 0.985, 0.174)); + float z = dot(ec_normal, vec3(0.0, -0.174, 0.985)); + + gl_FragColor = vec4(0.5-0.5*x, 0.5-0.5*y, 0.5+0.5*z, 1.0); +} diff --git a/qt/fstl.pro b/qt/fstl.pro deleted file mode 100644 index 5d8613d..0000000 --- a/qt/fstl.pro +++ /dev/null @@ -1,51 +0,0 @@ -QT += core gui opengl widgets - -TARGET = fstl -TEMPLATE = app - -# Bump optimization up to -O3 in release builds -QMAKE_CXXFLAGS_RELEASE -= -O2 -QMAKE_CXXFLAGS_RELEASE += -O3 - -SOURCES += \ - ../src/app.cpp\ - ../src/main.cpp\ - ../src/canvas.cpp \ - ../src/mesh.cpp \ - ../src/glmesh.cpp \ - ../src/loader.cpp \ - ../src/window.cpp \ - ../src/backdrop.cpp - -HEADERS += \ - ../src/app.h\ - ../src/canvas.h \ - ../src/mesh.h \ - ../src/glmesh.h \ - ../src/loader.h \ - ../src/window.h \ - ../src/backdrop.h - -CONFIG += c++11 - -RESOURCES += \ - qt.qrc \ - ../gl/gl.qrc - -macx { - QMAKE_INFO_PLIST = ../app/Info.plist - ICON = ../app/fstl.icns -} - -win32 { - RC_FILE = ../exe/fstl.rc -} - -linux { - target.path = /usr/bin - INSTALLS += target -} - -static { - CONFIG += static -} diff --git a/src/app.cpp b/src/app.cpp index 982d0d0..a76a7da 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -7,10 +7,6 @@ App::App(int& argc, char *argv[]) : QApplication(argc, argv), window(new Window()) { - QCoreApplication::setOrganizationName("mkeeter"); - QCoreApplication::setOrganizationDomain("https://github.com/mkeeter/fstl"); - QCoreApplication::setApplicationName("fstl"); - if (argc > 1) window->load_stl(argv[1]); else @@ -20,7 +16,7 @@ App::App(int& argc, char *argv[]) : App::~App() { - delete window; + delete window; } bool App::event(QEvent* e) diff --git a/src/axis.cpp b/src/axis.cpp new file mode 100644 index 0000000..831f592 --- /dev/null +++ b/src/axis.cpp @@ -0,0 +1,163 @@ +#include "axis.h" + +const float xLet[] = { + -0.1, -0.2, 0, + 0.1, 0.2, 0, + 0.1, -0.2, 0, + -0.1, 0.2, 0 +}; +const float yLet[] = { + 0, -0.2, 0, + 0, 0, 0, + 0, 0, 0, + 0.1, 0.2, 0, + 0, 0, 0, + -0.1, 0.2, 0 +}; +const float zLet[] = { + -0.1, -0.2, 0, + 0.1, -0.2, 0, + 0.1, -0.2, 0, + -0.1, 0.2, 0, + -0.1, 0.2, 0, + 0.1, 0.2, 0 +}; +const int axisSegCount[] = {2, 3, 3}; +const float* axisLabels[] = {xLet, yLet, zLet}; + +Axis::Axis() +{ + initializeOpenGLFunctions(); + + shader.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/gl/colored_lines.vert"); + shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/colored_lines.frag"); + shader.link(); + const int ptSize = 6*sizeof(float); + for(int lIdx = 0; lIdx < 3; lIdx++) + { + const float* l = axisLabels[lIdx]; + const int ptCount = axisSegCount[lIdx]*2; + float c[3] = {0.0}; + c[lIdx] = 1.0;//set color + QOpenGLBuffer b = flowerLabelVertices[lIdx]; + b.create(); + b.bind(); + b.allocate(ptCount*ptSize); + for(int pIdx = 0; pIdx < ptCount; pIdx++) + { + b.write(pIdx*ptSize, &(l[pIdx*3]), ptSize/2);//write coords + b.write(pIdx*ptSize + ptSize/2, c, ptSize/2);//write color + } + b.release(); + } + //Axis buffer: 6 floats per vertex, 2 vert per line, 3 lines + float aBuf[6*2*3] = {0.0}; + for(int aIdx = 0; aIdx < 3; aIdx++) + { + aBuf[(2*aIdx)*6+3+aIdx] = 1.0;//Set color (last 3 floats) + aBuf[(2*aIdx+1)*6+3+aIdx] = 1.0;//Set color (last 3 floats) + aBuf[(2*aIdx+1)*6+aIdx] = 1.0;//Extend line in axis + } + //The lines which form the 'axis-flower' in the corner + flowerAxisVertices.create(); + flowerAxisVertices.bind(); + flowerAxisVertices.allocate(aBuf, sizeof(aBuf)); + flowerAxisVertices.release(); + //The lines which form the model-space axes + vertices.create(); + vertices.bind(); + vertices.allocate(aBuf, sizeof(aBuf)); + vertices.release(); +} +void Axis::setScale(QVector3D min, QVector3D max) +{ + //Max function. not worth importing just for max + auto Max = [](float a, float b) + { + return (a > b) ? a : b; + }; + //This is how much the axes extend beyond the model + //We want it to be dependent on the model's size, but uniform on all axes. + const float axismargin = 0.25*Max(Max(max[0]-min[0], max[1]-min[1]), max[2]-min[2]); + vertices.bind(); + //Manually rewrite coordinates to control axis draw lengths + float s = sizeof(float); + //aIdx*12+aIdx gets us to the set of 2 points of the axis line, plus the offset for that dimension + //+6 gets us to the other end of the line in that dimension + for(int aIdx = 0; aIdx < 3; aIdx++) + { + float t = min[aIdx]-axismargin; + vertices.write(s*(aIdx*12+aIdx), &t, s); + t = max[aIdx]+axismargin; + vertices.write(s*(aIdx*12+aIdx+6), &t, s); + } + vertices.release(); +} +void Axis::draw(QMatrix4x4 transMat, QMatrix4x4 viewMat, + QMatrix4x4 orientMat, QMatrix4x4 aspectMat, float aspectRatio) +{ + shader.bind(); + vertices.bind(); + // Load the transform and view matrices into the shader + auto loadMatrixUniforms = [&](QMatrix4x4 transform, QMatrix4x4 view) + { + glUniformMatrix4fv( + shader.uniformLocation("transform_matrix"), + 1, GL_FALSE, transform.data()); + glUniformMatrix4fv( + shader.uniformLocation("view_matrix"), + 1, GL_FALSE, view.data()); + }; + const GLuint vp = shader.attributeLocation("vertex_position"); + const GLuint vc = shader.attributeLocation("vertex_color"); + glEnableVertexAttribArray(vp); + glEnableVertexAttribArray(vc); + auto loadAttribPtr = [&]() + { + glVertexAttribPointer(vp, 3, GL_FLOAT, false, + 6 * sizeof(GLfloat), 0); + glVertexAttribPointer(vc, 3, GL_FLOAT, false, + 6 * sizeof(GLfloat), + (GLvoid*)(3 * sizeof(GLfloat))); + }; + loadMatrixUniforms(transMat, viewMat); + loadAttribPtr(); + + glDrawArrays(GL_LINES, 0, 3*6); + + vertices.release(); + //Next, we draw the hud axis-flower + flowerAxisVertices.bind(); + glClear(GL_DEPTH_BUFFER_BIT);//Ensure hud draws over everything + const float hudSize = 0.2; + QMatrix4x4 hudMat; + //Move the hud to the bottom left corner with margin + if (aspectRatio > 1.0) + { + hudMat.translate(aspectRatio-2*hudSize, -1.0+2*hudSize, 0); + } + else + { + hudMat.translate(1.0-2*hudSize, -1.0/aspectRatio+2*hudSize, 0); + } + //Scale the hud to be small + hudMat.scale(hudSize, hudSize, 1); + loadMatrixUniforms(orientMat, aspectMat*hudMat); + loadAttribPtr(); + glDrawArrays(GL_LINES, 0, 3*6); + flowerAxisVertices.release(); + for(int aIdx = 0; aIdx < 3; aIdx++){ + QVector3D transVec = QVector3D(); + transVec[aIdx] = 1.25;//This is how far we want the letters to be extended out + QOpenGLBuffer b = flowerLabelVertices[aIdx]; + //The only transform we want is to translate the letters to the ends of the axis lines + QMatrix4x4 labelTransMat = QMatrix4x4(); + labelTransMat.translate(orientMat * transVec); + b.bind(); + loadMatrixUniforms(labelTransMat, aspectMat * hudMat); + loadAttribPtr(); + glDrawArrays(GL_LINES, 0, axisSegCount[aIdx]*2*6); + b.release(); + } + shader.release(); +} diff --git a/src/axis.h b/src/axis.h new file mode 100644 index 0000000..7d0a991 --- /dev/null +++ b/src/axis.h @@ -0,0 +1,22 @@ +#ifndef AXIS_H +#define AXIS_H + +#include +#include +#include + +class Axis : protected QOpenGLFunctions +{ +public: + Axis(); + void setScale(QVector3D min, QVector3D max); + void draw(QMatrix4x4 transMat, QMatrix4x4 viewMat, + QMatrix4x4 orientMat, QMatrix4x4 aspectMat, float aspectRatio); +private: + QOpenGLShaderProgram shader; + QOpenGLBuffer vertices, //GL Buffer for model-space coords + flowerAxisVertices; //GL Buffer for hud-space axis lines + QOpenGLBuffer flowerLabelVertices[3];//Buffer for hud-space label lines +}; + +#endif // AXIS_H diff --git a/src/canvas.cpp b/src/canvas.cpp index dd188ff..4da41a7 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -4,15 +4,20 @@ #include "canvas.h" #include "backdrop.h" +#include "axis.h" #include "glmesh.h" #include "mesh.h" +const float Canvas::P_PERSPECTIVE = 0.25f; +const float Canvas::P_ORTHOGRAPHIC = 0.0f; + Canvas::Canvas(const QSurfaceFormat& format, QWidget *parent) : QOpenGLWidget(parent), mesh(nullptr), scale(1), zoom(1), tilt(90), yaw(0), - perspective(0.25), anim(this, "perspective"), status(" ") + anim(this, "perspective"), status(" "), + meshInfo("") { - setFormat(format); + setFormat(format); QFile styleFile(":/qt/style.qss"); styleFile.open( QFile::ReadOnly ); setStyleSheet(styleFile.readAll()); @@ -22,9 +27,12 @@ Canvas::Canvas(const QSurfaceFormat& format, QWidget *parent) Canvas::~Canvas() { - makeCurrent(); - delete mesh; - doneCurrent(); + makeCurrent(); + delete mesh; + delete mesh_vertshader; + delete backdrop; + delete axis; + doneCurrent(); } void Canvas::view_anim(float v) @@ -34,34 +42,37 @@ void Canvas::view_anim(float v) anim.start(); } -void Canvas::view_orthographic() -{ - view_anim(0); -} - -void Canvas::view_perspective() -{ - view_anim(0.25); +void Canvas::view_perspective(float p, bool animate){ + if(animate) + { + view_anim(p); + } + else + { + set_perspective(p); + } } -void Canvas::draw_shaded() +void Canvas::draw_axes(bool d) { - set_drawMode(0); + drawAxes = d; + update(); } -void Canvas::draw_wireframe() +void Canvas::invert_zoom(bool d) { - set_drawMode(1); + invertZoom = d; + update(); } void Canvas::load_mesh(Mesh* m, bool is_reload) { + delete mesh; mesh = new GLMesh(m); - + QVector3D lower(m->xmin(), m->ymin(), m->zmin()); + QVector3D upper(m->xmax(), m->ymax(), m->zmax()); if (!is_reload) { - QVector3D lower(m->xmin(), m->ymin(), m->zmin()); - QVector3D upper(m->xmax(), m->ymax(), m->zmax()); center = (lower + upper) / 2; scale = 2 / (upper - lower).length(); @@ -70,7 +81,9 @@ void Canvas::load_mesh(Mesh* m, bool is_reload) yaw = 0; tilt = 90; } - + meshInfo = QStringLiteral("Triangles: %1\nX: [%2, %3]\nY: [%4, %5]\nZ: [%6, %7]").arg(m->triCount()); + for(int dIdx = 0; dIdx < 3; dIdx++) meshInfo = meshInfo.arg(lower[dIdx]).arg(upper[dIdx]); + axis->setScale(lower, upper); update(); delete m; @@ -88,7 +101,7 @@ void Canvas::set_perspective(float p) update(); } -void Canvas::set_drawMode(int mode) +void Canvas::set_drawMode(enum DrawMode mode) { drawMode = mode; update(); @@ -104,45 +117,58 @@ void Canvas::initializeGL() { initializeOpenGLFunctions(); - mesh_shader.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/gl/mesh.vert"); + mesh_vertshader = new QOpenGLShader(QOpenGLShader::Vertex); + mesh_vertshader->compileSourceFile(":/gl/mesh.vert"); + mesh_shader.addShader(mesh_vertshader); mesh_shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/mesh.frag"); mesh_shader.link(); - mesh_wireframe_shader.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/gl/mesh.vert"); + mesh_wireframe_shader.addShader(mesh_vertshader); mesh_wireframe_shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/mesh_wireframe.frag"); mesh_wireframe_shader.link(); + mesh_surfaceangle_shader.addShader(mesh_vertshader); + mesh_surfaceangle_shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/mesh_surfaceangle.frag"); + mesh_surfaceangle_shader.link(); backdrop = new Backdrop(); + axis = new Axis(); } void Canvas::paintGL() { - glClearColor(0.0, 0.0, 0.0, 0.0); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - glEnable(GL_DEPTH_TEST); - - backdrop->draw(); - if (mesh) draw_mesh(); - - if (status.isNull()) return; - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - painter.drawText(10, height() - 10, status); + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glEnable(GL_DEPTH_TEST); + backdrop->draw(); + if (mesh) draw_mesh(); + if (drawAxes) axis->draw(transform_matrix(), view_matrix(), + orient_matrix(), aspect_matrix(), width() / float(height())); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + float textHeight = painter.fontInfo().pointSize(); + if (drawAxes) painter.drawText(QRect(10, textHeight, width(), height()), meshInfo); + painter.drawText(10, height() - textHeight, status); } void Canvas::draw_mesh() { QOpenGLShaderProgram* selected_mesh_shader = NULL; - // Set gl draw mode - if(drawMode == 1) + if(drawMode == wireframe) { selected_mesh_shader = &mesh_wireframe_shader; glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); } else { - selected_mesh_shader = &mesh_shader; + if(drawMode == shaded) + { + selected_mesh_shader = &mesh_shader; + } + else + { + selected_mesh_shader = &mesh_surfaceangle_shader; + } glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } @@ -173,18 +199,23 @@ void Canvas::draw_mesh() glDisableVertexAttribArray(vp); selected_mesh_shader->release(); } - -QMatrix4x4 Canvas::transform_matrix() const +QMatrix4x4 Canvas::orient_matrix() const { QMatrix4x4 m; m.rotate(tilt, QVector3D(1, 0, 0)); m.rotate(yaw, QVector3D(0, 0, 1)); - m.scale(-scale, scale, -scale); + //We want the x axis to the right, and the z axis up + m.scale(-1, 1, -1); + return m; +} +QMatrix4x4 Canvas::transform_matrix() const +{ + QMatrix4x4 m = orient_matrix(); + m.scale(scale); m.translate(-center); return m; } - -QMatrix4x4 Canvas::view_matrix() const +QMatrix4x4 Canvas::aspect_matrix() const { QMatrix4x4 m; if (width() > height()) @@ -195,6 +226,11 @@ QMatrix4x4 Canvas::view_matrix() const { m.scale(-1, width() / float(height()), 0.5); } + return m; +} +QMatrix4x4 Canvas::view_matrix() const +{ + QMatrix4x4 m = aspect_matrix(); m.scale(zoom, zoom, 1); m(3, 2) = perspective; return m; @@ -246,21 +282,27 @@ void Canvas::wheelEvent(QWheelEvent *event) { // Find GL position before the zoom operation // (to zoom about mouse cursor) - auto p = event->pos(); + auto p = event->position(); QVector3D v(1 - p.x() / (0.5*width()), p.y() / (0.5*height()) - 1, 0); QVector3D a = transform_matrix().inverted() * view_matrix().inverted() * v; - if (event->delta() < 0) + if (event->angleDelta().y() < 0) { - for (int i=0; i > event->delta(); --i) - zoom *= 1.001; + for (int i=0; i > event->angleDelta().y(); --i) + if (invertZoom) + zoom /= 1.001; + else + zoom *= 1.001; } - else if (event->delta() > 0) + else if (event->angleDelta().y() > 0) { - for (int i=0; i < event->delta(); ++i) - zoom /= 1.001; + for (int i=0; i < event->angleDelta().y(); ++i) + if (invertZoom) + zoom *= 1.001; + else + zoom /= 1.001; } // Then find the cursor's GL position post-zoom and adjust center. diff --git a/src/canvas.h b/src/canvas.h index 7fa80c8..0fdff64 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -8,19 +8,25 @@ class GLMesh; class Mesh; class Backdrop; +class Axis; + +enum DrawMode {shaded, wireframe, surfaceangle, DRAWMODECOUNT}; class Canvas : public QOpenGLWidget, protected QOpenGLFunctions { Q_OBJECT public: - explicit Canvas(const QSurfaceFormat& format, QWidget* parent=0); + explicit Canvas(const QSurfaceFormat& format, QWidget* parent=0); ~Canvas(); - void view_orthographic(); - void view_perspective(); - void draw_shaded(); - void draw_wireframe(); + const static float P_PERSPECTIVE; + const static float P_ORTHOGRAPHIC; + + void view_perspective(float p, bool animate); + void draw_axes(bool d); + void invert_zoom(bool d); + void set_drawMode(enum DrawMode mode); public slots: void set_status(const QString& s); @@ -28,31 +34,34 @@ public slots: void load_mesh(Mesh* m, bool is_reload); protected: - void paintGL() override; - void initializeGL() override; - void resizeGL(int width, int height) override; + void paintGL() override; + void initializeGL() override; + void resizeGL(int width, int height) override; void mousePressEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void wheelEvent(QWheelEvent* event) override; - void set_perspective(float p); - void set_drawMode(int mode); + void set_perspective(float p); void view_anim(float v); private: void draw_mesh(); + QMatrix4x4 orient_matrix() const; QMatrix4x4 transform_matrix() const; + QMatrix4x4 aspect_matrix() const; QMatrix4x4 view_matrix() const; + QOpenGLShader* mesh_vertshader; QOpenGLShaderProgram mesh_shader; QOpenGLShaderProgram mesh_wireframe_shader; - QOpenGLShaderProgram quad_shader; + QOpenGLShaderProgram mesh_surfaceangle_shader; GLMesh* mesh; Backdrop* backdrop; + Axis* axis; QVector3D center; float scale; @@ -61,12 +70,15 @@ private: float yaw; float perspective; - int drawMode; + enum DrawMode drawMode; + bool drawAxes; + bool invertZoom; Q_PROPERTY(float perspective MEMBER perspective WRITE set_perspective); QPropertyAnimation anim; QPoint mouse_pos; QString status; + QString meshInfo; }; #endif // CANVAS_H diff --git a/src/loader.cpp b/src/loader.cpp index b5a2f04..38bd2dd 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -127,14 +127,12 @@ Mesh* Loader::load_stl() file.seek(0); return read_stl_ascii(file); } - confusing_stl = true; - } - else - { - confusing_stl = false; + // Otherwise, this STL is a binary stl but contains 'solid' as + // the first five characters. This is a bad life choice, but + // we can gracefully handle it by falling through to the binary + // STL reader below. } - // Otherwise, skip the rest of the header material and read as binary file.seek(0); return read_stl_binary(file); } @@ -161,7 +159,7 @@ Mesh* Loader::read_stl_binary(QFile& file) QVector verts(tri_count*3); // Dummy array, because readRawData is faster than skipRawData - std::unique_ptr buffer(new uint8_t[tri_count * 50]); + std::unique_ptr buffer(new uint8_t[tri_count * 50]); data.readRawData((char*)buffer.get(), tri_count * 50); // Store vertices in the array, processing one triangle at a time. @@ -171,7 +169,7 @@ Mesh* Loader::read_stl_binary(QFile& file) // Load vertex data from .stl file into vertices for (unsigned i=0; i < 3; ++i) { - memcpy(&v[i], b, 3*sizeof(float)); + qFromLittleEndian(b, 3, &v[i]); b += 3 * sizeof(float); } @@ -179,11 +177,6 @@ Mesh* Loader::read_stl_binary(QFile& file) b += 3 * sizeof(float) + sizeof(uint16_t); } - if (confusing_stl) - { - emit warning_confusing_stl(); - } - return mesh_from_verts(tri_count, verts); } diff --git a/src/loader.h b/src/loader.h index 006f1d1..6f5942f 100644 --- a/src/loader.h +++ b/src/loader.h @@ -26,16 +26,11 @@ signals: void error_bad_stl(); void error_empty_mesh(); - void warning_confusing_stl(); void error_missing_file(); private: const QString filename; bool is_reload; - - /* Used to warn on binary STLs that begin with the word 'solid'" */ - bool confusing_stl; - }; #endif // LOADER_H diff --git a/src/main.cpp b/src/main.cpp index 4b76222..4ec8722 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,10 @@ int main(int argc, char *argv[]) { + QCoreApplication::setOrganizationName("fstl-app"); + QCoreApplication::setOrganizationDomain("https://github.com/fstl-app/fstl"); + QCoreApplication::setApplicationName("fstl"); + QCoreApplication::setApplicationVersion(FSTL_VERSION); App a(argc, argv); return a.exec(); } diff --git a/src/mesh.cpp b/src/mesh.cpp index b971029..2c44a2c 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -42,6 +42,10 @@ float Mesh::max(size_t start) const return v; } +int Mesh::triCount() const +{ + return indices.size()/3; +} bool Mesh::empty() const { return vertices.size() == 0; diff --git a/src/mesh.h b/src/mesh.h index c4e888b..9e2ab0c 100644 --- a/src/mesh.h +++ b/src/mesh.h @@ -21,6 +21,7 @@ public: float ymax() const { return max(1); } float zmax() const { return max(2); } + int triCount() const; bool empty() const; private: diff --git a/src/window.cpp b/src/window.cpp index 09d74ed..e9f3f94 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -5,6 +5,12 @@ #include "loader.h" const QString Window::RECENT_FILE_KEY = "recentFiles"; +const QString Window::INVERT_ZOOM_KEY = "invertZoom"; +const QString Window::AUTORELOAD_KEY = "autoreload"; +const QString Window::DRAW_AXES_KEY = "drawAxes"; +const QString Window::PROJECTION_KEY = "projection"; +const QString Window::DRAW_MODE_KEY = "drawMode"; +const QString Window::WINDOW_GEOM_KEY = "windowGeometry"; Window::Window(QWidget *parent) : QMainWindow(parent), @@ -12,12 +18,16 @@ Window::Window(QWidget *parent) : about_action(new QAction("About", this)), quit_action(new QAction("Quit", this)), perspective_action(new QAction("Perspective", this)), - orthogonal_action(new QAction("Orthographic", this)), + orthographic_action(new QAction("Orthographic", this)), shaded_action(new QAction("Shaded", this)), wireframe_action(new QAction("Wireframe", this)), + surfaceangle_action(new QAction("Surface Angle", this)), + axes_action(new QAction("Draw Axes", this)), + invert_zoom_action(new QAction("Invert Zoom", this)), reload_action(new QAction("Reload", this)), autoreload_action(new QAction("Autoreload", this)), save_screenshot_action(new QAction("Save Screenshot", this)), + hide_menuBar_action(new QAction("Hide Menu Bar", this)), recent_files(new QMenu("Open recent", this)), recent_files_group(new QActionGroup(this)), recent_files_clear_action(new QAction("Clear recent files", this)), @@ -44,14 +54,14 @@ Window::Window(QWidget *parent) : open_action->setShortcut(QKeySequence::Open); QObject::connect(open_action, &QAction::triggered, this, &Window::on_open); + this->addAction(open_action); quit_action->setShortcut(QKeySequence::Quit); QObject::connect(quit_action, &QAction::triggered, this, &Window::close); + this->addAction(quit_action); autoreload_action->setCheckable(true); - autoreload_action->setChecked(true); - autoreload_action->setEnabled(false); QObject::connect(autoreload_action, &QAction::triggered, this, &Window::on_autoreload_triggered); @@ -86,14 +96,13 @@ Window::Window(QWidget *parent) : auto view_menu = menuBar()->addMenu("View"); auto projection_menu = view_menu->addMenu("Projection"); projection_menu->addAction(perspective_action); - projection_menu->addAction(orthogonal_action); + projection_menu->addAction(orthographic_action); auto projections = new QActionGroup(projection_menu); - for (auto p : {perspective_action, orthogonal_action}) + for (auto p : {perspective_action, orthographic_action}) { projections->addAction(p); p->setCheckable(true); } - perspective_action->setChecked(true); projections->setExclusive(true); QObject::connect(projections, &QActionGroup::triggered, this, &Window::on_projection); @@ -101,27 +110,78 @@ Window::Window(QWidget *parent) : auto draw_menu = view_menu->addMenu("Draw Mode"); draw_menu->addAction(shaded_action); draw_menu->addAction(wireframe_action); + draw_menu->addAction(surfaceangle_action); auto drawModes = new QActionGroup(draw_menu); - for (auto p : {shaded_action, wireframe_action}) + for (auto p : {shaded_action, wireframe_action, surfaceangle_action}) { drawModes->addAction(p); p->setCheckable(true); } - shaded_action->setChecked(true); drawModes->setExclusive(true); QObject::connect(drawModes, &QActionGroup::triggered, this, &Window::on_drawMode); + view_menu->addAction(axes_action); + axes_action->setCheckable(true); + QObject::connect(axes_action, &QAction::triggered, + this, &Window::on_drawAxes); + + view_menu->addAction(invert_zoom_action); + invert_zoom_action->setCheckable(true); + QObject::connect(invert_zoom_action, &QAction::triggered, + this, &Window::on_invertZoom); + + view_menu->addAction(hide_menuBar_action); + hide_menuBar_action->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C); + hide_menuBar_action->setCheckable(true); + QObject::connect(hide_menuBar_action, &QAction::toggled, + this, &Window::on_hide_menuBar); + this->addAction(hide_menuBar_action); auto help_menu = menuBar()->addMenu("Help"); help_menu->addAction(about_action); + load_persist_settings(); +} + +void Window::load_persist_settings(){ + QSettings settings; + bool invert_zoom = settings.value(INVERT_ZOOM_KEY, false).toBool(); + canvas->invert_zoom(invert_zoom); + invert_zoom_action->setChecked(invert_zoom); + + autoreload_action->setChecked(settings.value(AUTORELOAD_KEY, true).toBool()); + + bool draw_axes = settings.value(DRAW_AXES_KEY, false).toBool(); + canvas->draw_axes(draw_axes); + axes_action->setChecked(draw_axes); + + QString projection = settings.value(PROJECTION_KEY, "perspective").toString(); + if(projection == "perspective"){ + canvas->view_perspective(Canvas::P_PERSPECTIVE, false); + perspective_action->setChecked(true); + }else{ + canvas->view_perspective(Canvas::P_ORTHOGRAPHIC, false); + orthographic_action->setChecked(true); + } + + DrawMode draw_mode = (DrawMode)settings.value(DRAW_MODE_KEY, DRAWMODECOUNT).toInt(); + + if(draw_mode >= DRAWMODECOUNT) + { + draw_mode = shaded; + } + canvas->set_drawMode(draw_mode); + QAction* (dm_acts[]) = {shaded_action, wireframe_action, surfaceangle_action}; + dm_acts[draw_mode]->setChecked(true); + resize(600, 400); + restoreGeometry(settings.value(WINDOW_GEOM_KEY).toByteArray()); } void Window::on_open() { QString filename = QFileDialog::getOpenFileName( - this, "Load .stl file", QString(), "*.stl"); + this, "Load .stl file", QString(), "STL files (*.stl, *.STL)"); if (!filename.isNull()) { load_stl(filename); @@ -131,11 +191,11 @@ void Window::on_open() void Window::on_about() { QMessageBox::about(this, "", - "

fstl

" + "

fstl
" FSTL_VERSION "

" "

A fast viewer for .stl files.
" - "https://github.com/mkeeter/fstl

" - "

© 2014-2017 Matthew Keeter
" + "https://github.com/fstl-app/fstl

" + "

© 2014-2022 Matthew Keeter
" "matt.j.keeter@gmail.com

"); } @@ -155,14 +215,6 @@ void Window::on_empty_mesh() "This file is syntactically correct
but contains no triangles."); } -void Window::on_confusing_stl() -{ - QMessageBox::warning(this, "Warning", - "Warning:
" - "This .stl file begins with solid but appears to be a binary file.
" - "fstl loaded it, but other programs may be confused by this file."); -} - void Window::on_missing_file() { QMessageBox::critical(this, "Error", @@ -206,24 +258,45 @@ void Window::on_projection(QAction* proj) { if (proj == perspective_action) { - canvas->view_perspective(); + canvas->view_perspective(Canvas::P_PERSPECTIVE, true); + QSettings().setValue(PROJECTION_KEY, "perspective"); } else { - canvas->view_orthographic(); + canvas->view_perspective(Canvas::P_ORTHOGRAPHIC, true); + QSettings().setValue(PROJECTION_KEY, "orthographic"); } } -void Window::on_drawMode(QAction* mode) +void Window::on_drawMode(QAction* act) { - if (mode == shaded_action) + DrawMode mode; + if (act == shaded_action) { - canvas->draw_shaded(); + mode = shaded; + } + else if (act == wireframe_action) + { + mode = wireframe; } else { - canvas->draw_wireframe(); + mode = surfaceangle; } + canvas->set_drawMode(mode); + QSettings().setValue(DRAW_MODE_KEY, mode); +} + +void Window::on_drawAxes(bool d) +{ + canvas->draw_axes(d); + QSettings().setValue(DRAW_AXES_KEY, d); +} + +void Window::on_invertZoom(bool d) +{ + canvas->invert_zoom(d); + QSettings().setValue(INVERT_ZOOM_KEY, d); } void Window::on_watched_change(const QString& filename) @@ -240,6 +313,7 @@ void Window::on_autoreload_triggered(bool b) { on_reload(); } + QSettings().setValue(AUTORELOAD_KEY, b); } void Window::on_clear_recent() @@ -293,6 +367,11 @@ void Window::on_save_screenshot() } } +void Window::on_hide_menuBar() +{ + menuBar()->setVisible(!hide_menuBar_action->isChecked()); +} + void Window::rebuild_recent_files() { QSettings settings; @@ -347,8 +426,6 @@ bool Window::load_stl(const QString& filename, bool is_reload) this, &Window::on_bad_stl); connect(loader, &Loader::error_empty_mesh, this, &Window::on_empty_mesh); - connect(loader, &Loader::warning_confusing_stl, - this, &Window::on_confusing_stl); connect(loader, &Loader::error_missing_file, this, &Window::on_missing_file); @@ -367,7 +444,6 @@ bool Window::load_stl(const QString& filename, bool is_reload) this, &Window::set_watched); connect(loader, &Loader::loaded_file, this, &Window::on_loaded); - autoreload_action->setEnabled(true); reload_action->setEnabled(true); } @@ -390,6 +466,18 @@ void Window::dropEvent(QDropEvent *event) load_stl(event->mimeData()->urls().front().toLocalFile()); } +void Window::resizeEvent(QResizeEvent *event) +{ + QSettings().setValue(WINDOW_GEOM_KEY, saveGeometry()); + QWidget::resizeEvent(event); +} + +void Window::moveEvent(QMoveEvent *event) +{ + QSettings().setValue(WINDOW_GEOM_KEY, saveGeometry()); + QWidget::moveEvent(event); +} + void Window::sorted_insert(QStringList& list, const QCollator& collator, const QString& value) { int start = 0; @@ -441,7 +529,7 @@ void Window::build_folder_file_list() QPair Window::get_file_neighbors() { if (current_file.isEmpty()) { - return QPair(QString::null, QString::null); + return QPair(QString(), QString()); } build_folder_file_list(); @@ -451,8 +539,8 @@ QPair Window::get_file_neighbors() QString current_dir = fileInfo.absoluteDir().absolutePath(); QString current_name = fileInfo.fileName(); - QString prev = QString::null; - QString next = QString::null; + QString prev = QString(); + QString next = QString(); QListIterator fileIterator(lookup_folder_files); while (fileIterator.hasNext()) { @@ -514,6 +602,11 @@ void Window::keyPressEvent(QKeyEvent* event) load_next(); return; } + else if (event->key() == Qt::Key_Escape) + { + hide_menuBar_action->setChecked(false); + return; + } QMainWindow::keyPressEvent(event); } diff --git a/src/window.h b/src/window.h index 8619b4d..81cd29f 100644 --- a/src/window.h +++ b/src/window.h @@ -20,6 +20,8 @@ public: protected: void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; + void resizeEvent(QResizeEvent *event) override; + void moveEvent(QMoveEvent *event) override; void keyPressEvent(QKeyEvent* event) override; public slots: @@ -28,7 +30,6 @@ public slots: void on_bad_stl(); void on_empty_mesh(); void on_missing_file(); - void on_confusing_stl(); void enable_open(); void disable_open(); @@ -38,6 +39,8 @@ public slots: private slots: void on_projection(QAction* proj); void on_drawMode(QAction* mode); + void on_drawAxes(bool d); + void on_invertZoom(bool d); void on_watched_change(const QString& filename); void on_reload(); void on_autoreload_triggered(bool r); @@ -45,9 +48,11 @@ private slots: void on_load_recent(QAction* a); void on_loaded(const QString& filename); void on_save_screenshot(); - + void on_hide_menuBar(); + private: void rebuild_recent_files(); + void load_persist_settings(); void sorted_insert(QStringList& list, const QCollator& collator, const QString& value); void build_folder_file_list(); QPair get_file_neighbors(); @@ -56,18 +61,29 @@ private: QAction* const about_action; QAction* const quit_action; QAction* const perspective_action; - QAction* const orthogonal_action; + QAction* const orthographic_action; QAction* const shaded_action; QAction* const wireframe_action; + QAction* const surfaceangle_action; + QAction* const axes_action; + QAction* const invert_zoom_action; QAction* const reload_action; QAction* const autoreload_action; QAction* const save_screenshot_action; + QAction* const hide_menuBar_action; QMenu* const recent_files; QActionGroup* const recent_files_group; QAction* const recent_files_clear_action; const static int MAX_RECENT_FILES=8; const static QString RECENT_FILE_KEY; + const static QString INVERT_ZOOM_KEY; + const static QString AUTORELOAD_KEY; + const static QString DRAW_AXES_KEY; + const static QString PROJECTION_KEY; + const static QString DRAW_MODE_KEY; + const static QString WINDOW_GEOM_KEY; + QString current_file; QString lookup_folder; QStringList lookup_folder_files; -- 2.39.2