From: Jakob Haufe Date: Sun, 12 Aug 2018 20:44:21 +0000 (+0000) Subject: New upstream version 0.9.3 X-Git-Tag: upstream/0.9.3 X-Git-Url: https://git.sur5r.net/?p=fstl;a=commitdiff_plain;h=refs%2Ftags%2Fupstream%2F0.9.3 New upstream version 0.9.3 --- 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; };