]> git.sur5r.net Git - fstl/commitdiff
New upstream version 0.9.3 upstream/0.9.3
authorJakob Haufe <sur5r@sur5r.net>
Sun, 12 Aug 2018 20:44:21 +0000 (20:44 +0000)
committerJakob Haufe <sur5r@sur5r.net>
Sun, 12 Aug 2018 20:44:21 +0000 (20:44 +0000)
15 files changed:
README.md
app/package.sh
exe/package.sh
qt/fstl.pro
qt/style.qss
src/app.cpp
src/app.h
src/canvas.cpp
src/canvas.h
src/loader.cpp
src/loader.h
src/mesh.cpp
src/mesh.h
src/window.cpp
src/window.h

index a9a845ffaed12717b7cf290eec5255968bc72226..b4ab04b55bf6ee1f58861c01d59e3b3d5b1b786e 100644 (file)
--- 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.
index d51c470818b0589e07721a0663d78d678be8c522..bdb4999a91d18306e453a66f17056394719f1abd 100755 (executable)
@@ -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 ..
index ef81b620474638f3043fc3bed41418a3ff2abc2b..dfc411569f91f919af670a97df7f20508119a243 100644 (file)
@@ -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
index 7a0f449862e534c1aaa7e20b4c92835ed565f26e..5d8613dad0dc3b4e7c557e1af78a5a6320e6dc1e 100644 (file)
@@ -41,6 +41,11 @@ win32 {
     RC_FILE = ../exe/fstl.rc
 }
 
+linux {
+    target.path = /usr/bin
+    INSTALLS += target
+}
+
 static {
     CONFIG += static
 }
index 02971482c0585beccbf00c2187f3675e1ce3585f..3f0ca9b0dccb9c8fa23ddd32619c15e9107fea55 100644 (file)
@@ -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;
-}
index b6eb3af88c300e9925e5408105d7b3ebb0461c06..d18204898d5dd5d5eb006d8d880a9b67a1bd2f76 100644 (file)
@@ -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();
index 7a7672e7acd384de5a21966a1e7a9756dd16867e..2e06c249b26d2b20f6a59843f37bfe54332f4f0e 100644 (file)
--- 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:
index dd645a538838f790a1ea61e75ffbe67f3762187b..b42a2be85957b2679696a0029c4371019c0a9f22 100644 (file)
 
 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);
+}
index 33313cfcbf894c47aae09119c20398442fbe5369..4dae29891c0c3e60d14b9223b6668141dc9a65db 100644 (file)
@@ -2,6 +2,7 @@
 #define CANVAS_H
 
 #include <QWidget>
+#include <QPropertyAnimation>
 #include <QtOpenGL/QGLWidget>
 #include <QtOpenGL/QGLFunctions>
 #include <QtOpenGL/QGLShaderProgram>
@@ -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;
 };
index 0d6b165ad696d436324a2961d71d6788c6c67c51..93f5525320ce1f7ddc29dd9493b0d3b8a35852ca 100644 (file)
@@ -1,7 +1,9 @@
+#include <future>
+
 #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<Vec3, GLuint> 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<Vec3i>& 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<GLuint> 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<GLfloat> 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<Vec3i> 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<GLuint> indices(tri_count*3);
+Mesh* Loader::read_stl_ascii(QFile& file)
+{
+    file.readLine();
+    uint32_t tri_count = 0;
+    QVector<Vec3i> 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<GLfloat> 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);
 }
 
index fb0c8d81eb10bf8a610fb38a4345dfa2f65ce6ea..006f1d1d1469a5524c4d2fdc0a1c7168aad4639d 100644 (file)
@@ -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;
 
 };
 
index cfb4ca464f5be66adb121ac7dd26adf795d061c2..140a769b5eab739ff35454a098a45e27cac4039d 100644 (file)
@@ -16,6 +16,10 @@ Mesh::Mesh(std::vector<GLfloat> v, std::vector<GLuint> 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;
+}
index e8a02f09a2c369b0788b90f7ca0ad97fd7166b18..141278139b64d3086234e08c66632233a6bca395 100644 (file)
@@ -21,6 +21,8 @@ public:
     float ymax() const { return max(1); }
     float zmax() const { return max(2); }
 
+    bool empty() const;
+
 private:
     std::vector<GLfloat> vertices;
     std::vector<GLuint> indices;
index 3002ecc83daa6008dd3c42c420b933c24605008e..0954b33eb92f46b2951f7330c50221dcfc8917fe 100644 (file)
@@ -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()
         "<p>A fast viewer for <code>.stl</code> files.<br>"
         "<a href=\"https://github.com/mkeeter/fstl\""
         "   style=\"color: #93a1a1;\">https://github.com/mkeeter/fstl</a></p>"
-        "<p>© 2014 Matthew Keeter<br>"
+        "<p>© 2014-2017 Matthew Keeter<br>"
         "<a href=\"mailto:matt.j.keeter@gmail.com\""
         "   style=\"color: #93a1a1;\">matt.j.keeter@gmail.com</a></p>");
 }
 
-void Window::on_ascii_stl()
+void Window::on_bad_stl()
 {
     QMessageBox::critical(this, "Error",
                           "<b>Error:</b><br>"
-                          "Cannot open ASCII <code>.stl</code> file<br>"
-                          "Please convert to binary <code>.stl</code> and retry");
+                          "This <code>.stl</code> file is invalid or corrupted.<br>"
+                          "Please export it from the original source, verify, and retry.");
 }
 
-void Window::on_bad_stl()
+void Window::on_empty_mesh()
 {
     QMessageBox::critical(this, "Error",
                           "<b>Error:</b><br>"
-                          "This <code>.stl</code> file is invalid or corrupted.<br>"
-                          "Please export it from the original source, verify, and retry.");
+                          "This file is syntactically correct<br>but contains no triangles.");
+}
+
+void Window::on_confusing_stl()
+{
+    QMessageBox::warning(this, "Warning",
+                         "<b>Warning:</b><br>"
+                         "This <code>.stl</code> file begins with <code>solid </code>but appears to be a binary file.<br>"
+                         "<code>fstl</code> loaded it, but other programs may be confused by this file.");
+}
+
+void Window::on_missing_file()
+{
+    QMessageBox::critical(this, "Error",
+                          "<b>Error:</b><br>"
+                          "The target file is missing.<br>");
 }
 
 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();
index cb92ebb4708d2d68c52b3a1af82915c2b31c3940..fd0a77001e4eac827951c8fa9344c6c268d2a10e 100644 (file)
@@ -2,6 +2,8 @@
 #define WINDOW_H
 
 #include <QMainWindow>
+#include <QActionGroup>
+#include <QFileSystemWatcher>
 
 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;
 };