--- /dev/null
+build/*
+*.pro.user
+*.qmake.stash
--- /dev/null
+`fstl` is a viewer for [.stl files](http://en.wikipedia.org/wiki/STL_\(file_format\)).
+
+Here's a screenshot:
+![Eiffel tower](http://mattkeeter.com/projects/fstl/eiffel.png)
+(credit to [Pranav Panchal](https://grabcad.com/pranav.panchal))
+
+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;
+the project is under 1K lines of code and should be fairly approachable.
+
+--------------------------------------------------------------------------------
+
+License
+-------
+(c) 2014 Matt Keeter
+
+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.
+
+--------------------------------------------------------------------------------
+*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.
+
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+ <key>CFBundleIconFile</key>
+ <string>fstl.icns</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleGetInfoString</key>
+ <string>A minimal fast STL viewer</string>
+ <key>CFBundleExecutable</key>
+ <string>fstl</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.impraxical.fstl</string>
+ <key>CFBundleDocumentTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeExtensions</key>
+ <array>
+ <string>stl</string>
+ </array>
+ <key>CFBundleTypeName</key>
+ <string>Stereolithography file</string>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ </dict>
+ </array>
+</dict>
+</plist>
--- /dev/null
+#!/bin/sh
+cd ../build
+macdeployqt fstl.app
+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
+cd ../Resources
+rm empty.lproj
+cd ../../..
+cp -r fstl.app ..
+cd ..
+zip -r fstl_mac.zip fstl.app README.md
+
--- /dev/null
+IDI_ICON1 ICON DISCARDABLE "fstl.ico"
--- /dev/null
+cd ..
+cp build/release/fstl.exe .
+/c/Program\ Files/7-Zip/7z.exe a fstl_win.zip fstl.exe README.md
--- /dev/null
+<RCC>
+ <qresource prefix="gl/">
+ <file>mesh.frag</file>
+ <file>mesh.vert</file>
+ <file>quad.frag</file>
+ <file>quad.vert</file>
+ <file>sphere.stl</file>
+ </qresource>
+</RCC>
--- /dev/null
+#version 120
+
+uniform float zoom;
+
+varying vec3 ec_pos;
+
+void main() {
+ vec3 base3 = vec3(0.99, 0.96, 0.89);
+ vec3 base2 = vec3(0.92, 0.91, 0.83);
+ vec3 base00 = vec3(0.40, 0.48, 0.51);
+
+ vec3 ec_normal = normalize(cross(dFdx(ec_pos), dFdy(ec_pos)));
+ ec_normal.z *= zoom;
+ ec_normal = normalize(ec_normal);
+
+ float a = dot(ec_normal, vec3(0.0, 0.0, 1.0));
+ float b = dot(ec_normal, vec3(-0.57, -0.57, 0.57));
+
+ gl_FragColor = vec4((a*base2 + (1-a)*base00)*0.5 +
+ (b*base3 + (1-b)*base00)*0.5, 1.0);
+}
--- /dev/null
+#version 120
+attribute vec3 vertex_position;
+
+uniform mat4 transform_matrix;
+uniform mat4 view_matrix;
+
+varying vec3 ec_pos;
+
+void main() {
+ gl_Position = view_matrix*transform_matrix*
+ vec4(vertex_position, 1.0);
+ ec_pos = gl_Position.xyz;
+}
--- /dev/null
+#version 120
+
+varying vec3 frag_color;
+
+void main() {
+ gl_FragColor = vec4(frag_color, 1.0);
+}
--- /dev/null
+#version 120
+attribute vec2 vertex_position;
+attribute vec3 vertex_color;
+
+varying vec3 frag_color;
+
+void main() {
+ gl_Position = vec4(vertex_position, 0.9, 1.0);
+ frag_color = vertex_color;
+}
--- /dev/null
+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
+}
+
+static {
+ CONFIG += static
+}
--- /dev/null
+<RCC>
+ <qresource prefix="qt/">
+ <file>style.qss</file>
+ </qresource>
+</RCC>
--- /dev/null
+QWidget {
+ background-color: #fdf6e3;
+ 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;
+}
--- /dev/null
+#include <QDebug>
+#include <QFileOpenEvent>
+
+#include "app.h"
+#include "window.h"
+
+App::App(int argc, char *argv[]) :
+ QApplication(argc, argv), window(new Window())
+{
+ window->show();
+ if (argc > 1)
+ window->load_stl(argv[1]);
+ else
+ window->load_stl(":gl/sphere.stl");
+}
+
+bool App::event(QEvent* e)
+{
+ if (e->type() == QEvent::FileOpen)
+ {
+ window->load_stl(static_cast<QFileOpenEvent*>(e)->file());
+ return true;
+ }
+ else
+ {
+ return QApplication::event(e);
+ }
+}
--- /dev/null
+#ifndef APP_H
+#define APP_H
+
+#include <QApplication>
+
+class Window;
+
+class App : public QApplication
+{
+ Q_OBJECT
+public:
+ explicit App(int argc, char *argv[]);
+protected:
+ bool event(QEvent* e);
+private:
+ Window* const window;
+
+};
+
+#endif // APP_H
--- /dev/null
+#include "backdrop.h"
+
+Backdrop::Backdrop()
+{
+ initializeGLFunctions();
+
+ shader.addShaderFromSourceFile(QGLShader::Vertex, ":/gl/quad.vert");
+ shader.addShaderFromSourceFile(QGLShader::Fragment, ":/gl/quad.frag");
+ shader.link();
+
+ float vbuf[] = {
+ -1, -1, 0.00, 0.10, 0.15,
+ -1, 1, 0.03, 0.21, 0.26,
+ 1, -1, 0.00, 0.12, 0.18,
+ 1, 1, 0.06, 0.26, 0.30};
+
+ vertices.create();
+ vertices.bind();
+ vertices.allocate(vbuf, sizeof(vbuf));
+ vertices.release();
+}
+
+void Backdrop::draw()
+{
+ shader.bind();
+ vertices.bind();
+
+ const GLuint vp = shader.attributeLocation("vertex_position");
+ const GLuint vc = shader.attributeLocation("vertex_color");
+
+ glEnableVertexAttribArray(vp);
+ glEnableVertexAttribArray(vc);
+
+ glVertexAttribPointer(vp, 2, GL_FLOAT, false,
+ 5 * sizeof(GLfloat), 0);
+ glVertexAttribPointer(vc, 3, GL_FLOAT, false,
+ 5 * sizeof(GLfloat),
+ (GLvoid*)(2 * sizeof(GLfloat)));
+
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 8);
+
+ vertices.release();
+ shader.release();
+}
--- /dev/null
+#ifndef BACKDROP_H
+#define BACKDROP_H
+
+#include <QtOpenGL/QGLFunctions>
+#include <QtOpenGL/QGLShaderProgram>
+#include <QtOpenGL/QGLBuffer>
+
+class Backdrop : protected QGLFunctions
+{
+public:
+ Backdrop();
+ void draw();
+private:
+ QGLShaderProgram shader;
+ QGLBuffer vertices;
+};
+
+#endif // BACKDROP_H
--- /dev/null
+#include <QMouseEvent>
+#include <QDebug>
+
+#include <cmath>
+
+#include "canvas.h"
+#include "backdrop.h"
+#include "glmesh.h"
+#include "mesh.h"
+
+Canvas::Canvas(const QGLFormat& format, QWidget *parent)
+ : QGLWidget(format, parent), mesh(NULL),
+ scale(1), zoom(1), tilt(90), yaw(0), status(" ")
+{
+ // Nothing to do here
+}
+
+Canvas::~Canvas()
+{
+ delete mesh;
+}
+
+void Canvas::load_mesh(Mesh* m)
+{
+ 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;
+
+ update();
+
+ delete m;
+}
+
+void Canvas::set_status(const QString &s)
+{
+ status = s;
+ update();
+}
+
+void Canvas::clear_status()
+{
+ status = "";
+ update();
+}
+
+void Canvas::initializeGL()
+{
+ initializeGLFunctions();
+
+ mesh_shader.addShaderFromSourceFile(QGLShader::Vertex, ":/gl/mesh.vert");
+ mesh_shader.addShaderFromSourceFile(QGLShader::Fragment, ":/gl/mesh.frag");
+ mesh_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);
+
+ backdrop->draw();
+ if (mesh) draw_mesh();
+
+ if (status.isNull()) return;
+
+ QPainter painter(this);
+ painter.setRenderHint(QPainter::Antialiasing);
+ painter.drawText(10, height() - 10, status);
+}
+
+
+void Canvas::draw_mesh()
+{
+ mesh_shader.bind();
+
+ // Load the transform and view matrices into the shader
+ glUniformMatrix4fv(
+ mesh_shader.uniformLocation("transform_matrix"),
+ 1, GL_FALSE, transform_matrix().data());
+ glUniformMatrix4fv(
+ mesh_shader.uniformLocation("view_matrix"),
+ 1, GL_FALSE, view_matrix().data());
+
+ // Compensate for z-flattening when zooming
+ glUniform1f(mesh_shader.uniformLocation("zoom"), 1/zoom);
+
+ // Find and enable the attribute location for vertex position
+ const GLuint vp = mesh_shader.attributeLocation("vertex_position");
+ glEnableVertexAttribArray(vp);
+
+ // Then draw the mesh with that vertex position
+ mesh->draw(vp);
+
+ // Clean up state machine
+ glDisableVertexAttribArray(vp);
+ mesh_shader.release();
+}
+
+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.translate(-center);
+ return m;
+}
+
+QMatrix4x4 Canvas::view_matrix() const
+{
+ QMatrix4x4 m;
+ if (width() > height())
+ {
+ m.scale(-height() / float(width()), 1, 0.5);
+ }
+ else
+ {
+ m.scale(-1, width() / float(height()), 0.5);
+ }
+ m.scale(zoom, zoom, 1);
+ return m;
+}
+
+void Canvas::mousePressEvent(QMouseEvent* event)
+{
+ if (event->button() == Qt::LeftButton ||
+ event->button() == Qt::RightButton)
+ {
+ mouse_pos = event->pos();
+ setCursor(Qt::ClosedHandCursor);
+ }
+}
+
+void Canvas::mouseReleaseEvent(QMouseEvent* event)
+{
+ if (event->button() == Qt::LeftButton ||
+ event->button() == Qt::RightButton)
+ {
+ unsetCursor();
+ }
+}
+
+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()));
+ update();
+ }
+ else if (event->buttons() & Qt::RightButton)
+ {
+ center = transform_matrix().inverted() *
+ view_matrix().inverted() *
+ QVector3D(-d.x() / (0.5*width()),
+ d.y() / (0.5*height()), 0);
+ update();
+ }
+ mouse_pos = p;
+}
+
+void Canvas::wheelEvent(QWheelEvent *event)
+{
+ // Find GL position before the zoom operation
+ // (to zoom about mouse cursor)
+ auto p = event->pos();
+ 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)
+ {
+ for (int i=0; i > event->delta(); --i)
+ zoom *= 1.001;
+ }
+ else if (event->delta() > 0)
+ {
+ for (int i=0; i < event->delta(); ++i)
+ zoom /= 1.001;
+ }
+
+ // Then find the cursor's GL position post-zoom and adjust center.
+ QVector3D b = transform_matrix().inverted() *
+ view_matrix().inverted() * v;
+ center += b - a;
+ update();
+}
--- /dev/null
+#ifndef CANVAS_H
+#define CANVAS_H
+
+#include <QWidget>
+#include <QtOpenGL/QGLWidget>
+#include <QtOpenGL/QGLFunctions>
+#include <QtOpenGL/QGLShaderProgram>
+#include <QMatrix4x4>
+
+class GLMesh;
+class Mesh;
+class Backdrop;
+
+class Canvas : public QGLWidget, protected QGLFunctions
+{
+ Q_OBJECT
+
+public:
+ Canvas(const QGLFormat& format, QWidget* parent=0);
+
+ void initializeGL();
+ void paintEvent(QPaintEvent* event);
+ ~Canvas();
+
+public slots:
+ void set_status(const QString& s);
+ void clear_status();
+ void load_mesh(Mesh* m);
+
+
+protected:
+ void mousePressEvent(QMouseEvent* event);
+ void mouseReleaseEvent(QMouseEvent* event);
+ void mouseMoveEvent(QMouseEvent* event);
+ void wheelEvent(QWheelEvent* event);
+
+
+private:
+ void draw_mesh();
+
+ QMatrix4x4 transform_matrix() const;
+ QMatrix4x4 view_matrix() const;
+
+ QGLShaderProgram mesh_shader;
+ QGLShaderProgram quad_shader;
+
+ GLMesh* mesh;
+ Backdrop* backdrop;
+
+ QVector3D center;
+ float scale;
+ float zoom;
+ float tilt;
+ float yaw;
+
+ QPoint mouse_pos;
+ QString status;
+};
+
+#endif // CANVAS_H
--- /dev/null
+#include "glmesh.h"
+#include "mesh.h"
+
+GLMesh::GLMesh(const Mesh* const mesh)
+ : vertices(QGLBuffer::VertexBuffer), indices(QGLBuffer::IndexBuffer)
+{
+ initializeGLFunctions();
+
+ vertices.create();
+ indices.create();
+
+ vertices.setUsagePattern(QGLBuffer::StaticDraw);
+ indices.setUsagePattern(QGLBuffer::StaticDraw);
+
+ vertices.bind();
+ vertices.allocate(mesh->vertices.data(),
+ mesh->vertices.size() * sizeof(float));
+ vertices.release();
+
+ indices.bind();
+ indices.allocate(mesh->indices.data(),
+ mesh->indices.size() * sizeof(uint32_t));
+ indices.release();
+}
+
+void GLMesh::draw(GLuint vp)
+{
+ vertices.bind();
+ indices.bind();
+
+ glVertexAttribPointer(vp, 3, GL_FLOAT, false, 3*sizeof(float), NULL);
+ glDrawElements(GL_TRIANGLES, indices.size() / sizeof(uint32_t),
+ GL_UNSIGNED_INT, NULL);
+
+ vertices.release();
+ indices.release();
+}
--- /dev/null
+#ifndef GLMESH_H
+#define GLMESH_H
+
+#include <QtOpenGL/QGLBuffer>
+#include <QtOpenGL/QGLFunctions>
+
+class Mesh;
+
+class GLMesh : protected QGLFunctions
+{
+public:
+ GLMesh(const Mesh* const mesh);
+ void draw(GLuint vp);
+private:
+ QGLBuffer vertices;
+ QGLBuffer indices;
+};
+
+#endif // GLMESH_H
--- /dev/null
+#include "loader.h"
+
+Loader::Loader(QObject* parent, const QString& filename)
+ : QThread(parent), filename(filename)
+{
+ // Nothing to do here
+}
+
+void Loader::run()
+{
+ Mesh* mesh = load_stl();
+ if (mesh)
+ {
+ emit got_mesh(mesh);
+ emit loaded_file(filename);
+ }
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+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<Vec3, GLuint> Vec3i;
+
+////////////////////////////////////////////////////////////////////////////////
+
+Mesh* Loader::load_stl()
+{
+ QFile file(filename);
+ file.open(QIODevice::ReadOnly);
+ if (file.read(5) == "solid")
+ {
+ emit error_ascii_stl();
+ return NULL;
+ }
+ // Skip the rest of the header material
+ file.read(75);
+
+ QDataStream data(&file);
+ data.setByteOrder(QDataStream::LittleEndian);
+ data.setFloatingPointPrecision(QDataStream::SinglePrecision);
+
+ // Load the triangle count from the .stl file
+ uint32_t tri_count;
+ data >> tri_count;
+
+ // Verify that the file is the right size
+ if (file.size() != 84 + tri_count*50)
+ {
+ emit error_bad_stl();
+ return NULL;
+ }
+
+ // Extract vertices into an array of xyz, unsigned pairs
+ QVector<Vec3i> verts(tri_count*3);
+
+ // Dummy array, because readRawData is faster than skipRawData
+ char buffer[sizeof(float)*3];
+
+ // Store vertices in the array, processing one triangle at a time.
+ for (auto v=verts.begin(); v != verts.end(); v += 3)
+ {
+ // Skip face's normal vector
+ data.readRawData(buffer, 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;
+
+ // Skip face attribute
+ data.readRawData(buffer, 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)
+ {
+ verts[i].second = i;
+ }
+
+ // Sort the set of vertices (to deduplicate)
+ std::sort(verts.begin(), verts.end());
+
+ // 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);
+}
+
--- /dev/null
+#ifndef LOADER_H
+#define LOADER_H
+
+#include <QThread>
+
+#include "mesh.h"
+
+class Loader : public QThread
+{
+ Q_OBJECT
+public:
+ explicit Loader(QObject* parent, const QString& filename);
+ void run();
+
+protected:
+ Mesh* load_stl();
+
+signals:
+ void loaded_file(QString filename);
+ void got_mesh(Mesh* m);
+
+ void error_ascii_stl();
+ void error_bad_stl();
+
+private:
+ const QString filename;
+
+};
+
+#endif // LOADER_H
--- /dev/null
+#include <QApplication>
+
+#include "app.h"
+
+int main(int argc, char *argv[])
+{
+ App a(argc, argv);
+ return a.exec();
+}
--- /dev/null
+#include <QFile>
+#include <QDataStream>
+#include <QVector3D>
+
+#include <cmath>
+
+#include "mesh.h"
+
+////////////////////////////////////////////////////////////////////////////////
+
+Mesh::Mesh(std::vector<GLfloat> v, std::vector<GLuint> i)
+ : vertices(v), indices(i)
+{
+ // Nothing to do here
+}
+
+float Mesh::min(size_t start) const
+{
+ float v = vertices[start];
+ for (size_t i=start; i < vertices.size(); i += 3)
+ {
+ v = fmin(v, vertices[i]);
+ }
+ return v;
+}
+
+float Mesh::max(size_t start) const
+{
+ float v = vertices[start];
+ for (size_t i=start; i < vertices.size(); i += 3)
+ {
+ v = fmax(v, vertices[i]);
+ }
+ return v;
+}
--- /dev/null
+#ifndef MESH_H
+#define MESH_H
+
+#include <QString>
+#include <QtOpenGL/QtOpenGL>
+
+#include <vector>
+
+class Mesh
+{
+public:
+ Mesh(std::vector<GLfloat> vertices, std::vector<GLuint> indices);
+
+ float min(size_t start) const;
+ float max(size_t start) const;
+
+ float xmin() const { return min(0); }
+ float ymin() const { return min(1); }
+ float zmin() const { return min(2); }
+ float xmax() const { return max(0); }
+ float ymax() const { return max(1); }
+ float zmax() const { return max(2); }
+
+private:
+ std::vector<GLfloat> vertices;
+ std::vector<GLuint> indices;
+
+ friend class GLMesh;
+};
+
+#endif // MESH_H
--- /dev/null
+#include <QMenuBar>
+#include <QMessageBox>
+#include <QFileDialog>
+
+#include "window.h"
+#include "canvas.h"
+#include "loader.h"
+
+Window::Window(QWidget *parent) :
+ QMainWindow(parent),
+ open_action(new QAction("Open", this)),
+ about_action(new QAction("About", this)),
+ quit_action(new QAction("Quit", 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);
+
+ canvas = new Canvas(format, this);
+ setCentralWidget(canvas);
+
+ open_action->setShortcut(QKeySequence::Open);
+ QObject::connect(open_action, &QAction::triggered,
+ this, &Window::on_open);
+
+ quit_action->setShortcut(QKeySequence::Quit);
+ QObject::connect(quit_action, &QAction::triggered,
+ this, &Window::close);
+
+ QObject::connect(about_action, &QAction::triggered,
+ this, &Window::on_about);
+
+ auto file_menu = menuBar()->addMenu("File");
+ file_menu->addAction(open_action);
+ file_menu->addAction(quit_action);
+
+ auto help_menu = menuBar()->addMenu("Help");
+ help_menu->addAction(about_action);
+
+ resize(600, 400);
+}
+
+void Window::on_open()
+{
+ QString filename = QFileDialog::getOpenFileName(
+ this, "Load .stl file", QString(), "*.stl");
+ if (not filename.isNull())
+ {
+ load_stl(filename);
+ }
+}
+
+void Window::on_about()
+{
+ QMessageBox::about(this, "",
+ "<p align=\"center\"><b>fstl</b></p>"
+ "<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>"
+ "<a href=\"mailto:matt.j.keeter@gmail.com\""
+ " style=\"color: #93a1a1;\">matt.j.keeter@gmail.com</a></p>");
+}
+
+void Window::on_ascii_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");
+}
+
+void Window::on_bad_stl()
+{
+ 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.");
+}
+
+void Window::enable_open()
+{
+ open_action->setEnabled(true);
+}
+
+void Window::disable_open()
+{
+ open_action->setEnabled(false);
+}
+
+bool Window::load_stl(const QString& filename)
+{
+ if (!open_action->isEnabled()) return false;
+
+ canvas->set_status("Loading " + filename);
+
+ Loader* loader = new Loader(this, filename);
+ 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::finished,
+ loader, &Loader::deleteLater);
+ connect(loader, &Loader::finished,
+ this, &Window::enable_open);
+ connect(loader, &Loader::finished,
+ canvas, &Canvas::clear_status);
+
+ if (filename[0] != ':')
+ {
+ connect(loader, &Loader::loaded_file,
+ this, &Window::setWindowTitle);
+ }
+
+ loader->start();
+ return true;
+}
+
+void Window::dragEnterEvent(QDragEnterEvent *event)
+{
+ if (event->mimeData()->hasUrls())
+ {
+ auto urls = event->mimeData()->urls();
+ if (urls.size() == 1 && urls.front().path().endsWith(".stl"))
+ event->acceptProposedAction();
+ }
+}
+
+void Window::dropEvent(QDropEvent *event)
+{
+ load_stl(event->mimeData()->urls().front().toLocalFile());
+}
--- /dev/null
+#ifndef WINDOW_H
+#define WINDOW_H
+
+#include <QMainWindow>
+
+class Canvas;
+
+class Window : public QMainWindow
+{
+ Q_OBJECT
+public:
+ explicit Window(QWidget* parent=0);
+ bool load_stl(const QString& filename);
+
+protected:
+ void dragEnterEvent(QDragEnterEvent* event);
+ void dropEvent(QDropEvent* event);
+
+public slots:
+ void on_open();
+ void on_about();
+ void on_ascii_stl();
+ void on_bad_stl();
+
+ void enable_open();
+ void disable_open();
+
+private:
+ QAction* const open_action;
+ QAction* const about_action;
+ QAction* const quit_action;
+
+ Canvas* canvas;
+};
+
+#endif // WINDOW_H