Imported Upstream version 0.9.2 upstream/0.9.2
authorJakob Haufe <sur5r@sur5r.net>
Sun, 13 Mar 2016 22:07:22 +0000 (23:07 +0100)
committerJakob Haufe <sur5r@sur5r.net>
Sun, 13 Mar 2016 22:07:22 +0000 (23:07 +0100)
32 files changed:
.gitignore [new file with mode: 0644]
README.md [new file with mode: 0644]
app/Info.plist [new file with mode: 0644]
app/fstl.icns [new file with mode: 0644]
app/package.sh [new file with mode: 0755]
exe/fstl.ico [new file with mode: 0644]
exe/fstl.rc [new file with mode: 0644]
exe/package.sh [new file with mode: 0644]
gl/gl.qrc [new file with mode: 0644]
gl/mesh.frag [new file with mode: 0644]
gl/mesh.vert [new file with mode: 0644]
gl/quad.frag [new file with mode: 0644]
gl/quad.vert [new file with mode: 0644]
gl/sphere.stl [new file with mode: 0644]
qt/fstl.pro [new file with mode: 0644]
qt/qt.qrc [new file with mode: 0644]
qt/style.qss [new file with mode: 0644]
src/app.cpp [new file with mode: 0644]
src/app.h [new file with mode: 0644]
src/backdrop.cpp [new file with mode: 0644]
src/backdrop.h [new file with mode: 0644]
src/canvas.cpp [new file with mode: 0644]
src/canvas.h [new file with mode: 0644]
src/glmesh.cpp [new file with mode: 0644]
src/glmesh.h [new file with mode: 0644]
src/loader.cpp [new file with mode: 0644]
src/loader.h [new file with mode: 0644]
src/main.cpp [new file with mode: 0644]
src/mesh.cpp [new file with mode: 0644]
src/mesh.h [new file with mode: 0644]
src/window.cpp [new file with mode: 0644]
src/window.h [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..deaad71
--- /dev/null
@@ -0,0 +1,3 @@
+build/*
+*.pro.user
+*.qmake.stash
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..a9a845f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+`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.
+
diff --git a/app/Info.plist b/app/Info.plist
new file mode 100644 (file)
index 0000000..ff3047b
--- /dev/null
@@ -0,0 +1,31 @@
+<?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>
diff --git a/app/fstl.icns b/app/fstl.icns
new file mode 100644 (file)
index 0000000..69c011c
Binary files /dev/null and b/app/fstl.icns differ
diff --git a/app/package.sh b/app/package.sh
new file mode 100755 (executable)
index 0000000..d51c470
--- /dev/null
@@ -0,0 +1,14 @@
+#!/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
+
diff --git a/exe/fstl.ico b/exe/fstl.ico
new file mode 100644 (file)
index 0000000..2e2d891
Binary files /dev/null and b/exe/fstl.ico differ
diff --git a/exe/fstl.rc b/exe/fstl.rc
new file mode 100644 (file)
index 0000000..3620f29
--- /dev/null
@@ -0,0 +1 @@
+IDI_ICON1 ICON DISCARDABLE "fstl.ico"
diff --git a/exe/package.sh b/exe/package.sh
new file mode 100644 (file)
index 0000000..ef81b62
--- /dev/null
@@ -0,0 +1,3 @@
+cd ..
+cp build/release/fstl.exe .
+/c/Program\ Files/7-Zip/7z.exe a fstl_win.zip fstl.exe README.md
diff --git a/gl/gl.qrc b/gl/gl.qrc
new file mode 100644 (file)
index 0000000..ef93a0e
--- /dev/null
+++ b/gl/gl.qrc
@@ -0,0 +1,9 @@
+<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>
diff --git a/gl/mesh.frag b/gl/mesh.frag
new file mode 100644 (file)
index 0000000..d7a54d5
--- /dev/null
@@ -0,0 +1,21 @@
+#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);
+}
diff --git a/gl/mesh.vert b/gl/mesh.vert
new file mode 100644 (file)
index 0000000..e60e76b
--- /dev/null
@@ -0,0 +1,13 @@
+#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;
+}
diff --git a/gl/quad.frag b/gl/quad.frag
new file mode 100644 (file)
index 0000000..1c02e1b
--- /dev/null
@@ -0,0 +1,7 @@
+#version 120
+
+varying vec3 frag_color;
+
+void main() {
+    gl_FragColor = vec4(frag_color, 1.0);
+}
diff --git a/gl/quad.vert b/gl/quad.vert
new file mode 100644 (file)
index 0000000..6698297
--- /dev/null
@@ -0,0 +1,10 @@
+#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;
+}
diff --git a/gl/sphere.stl b/gl/sphere.stl
new file mode 100644 (file)
index 0000000..b78c7b4
Binary files /dev/null and b/gl/sphere.stl differ
diff --git a/qt/fstl.pro b/qt/fstl.pro
new file mode 100644 (file)
index 0000000..7a0f449
--- /dev/null
@@ -0,0 +1,46 @@
+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
+}
diff --git a/qt/qt.qrc b/qt/qt.qrc
new file mode 100644 (file)
index 0000000..883ac14
--- /dev/null
+++ b/qt/qt.qrc
@@ -0,0 +1,5 @@
+<RCC>
+    <qresource prefix="qt/">
+        <file>style.qss</file>
+    </qresource>
+</RCC>
diff --git a/qt/style.qss b/qt/style.qss
new file mode 100644 (file)
index 0000000..0297148
--- /dev/null
@@ -0,0 +1,19 @@
+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;
+}
diff --git a/src/app.cpp b/src/app.cpp
new file mode 100644 (file)
index 0000000..b6eb3af
--- /dev/null
@@ -0,0 +1,28 @@
+#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);
+    }
+}
diff --git a/src/app.h b/src/app.h
new file mode 100644 (file)
index 0000000..7a7672e
--- /dev/null
+++ b/src/app.h
@@ -0,0 +1,20 @@
+#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
diff --git a/src/backdrop.cpp b/src/backdrop.cpp
new file mode 100644 (file)
index 0000000..13d0aa3
--- /dev/null
@@ -0,0 +1,44 @@
+#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();
+}
diff --git a/src/backdrop.h b/src/backdrop.h
new file mode 100644 (file)
index 0000000..a2398c8
--- /dev/null
@@ -0,0 +1,18 @@
+#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
diff --git a/src/canvas.cpp b/src/canvas.cpp
new file mode 100644 (file)
index 0000000..dd645a5
--- /dev/null
@@ -0,0 +1,205 @@
+#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();
+}
diff --git a/src/canvas.h b/src/canvas.h
new file mode 100644 (file)
index 0000000..33313cf
--- /dev/null
@@ -0,0 +1,60 @@
+#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
diff --git a/src/glmesh.cpp b/src/glmesh.cpp
new file mode 100644 (file)
index 0000000..7053809
--- /dev/null
@@ -0,0 +1,37 @@
+#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();
+}
diff --git a/src/glmesh.h b/src/glmesh.h
new file mode 100644 (file)
index 0000000..74a193f
--- /dev/null
@@ -0,0 +1,19 @@
+#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
diff --git a/src/loader.cpp b/src/loader.cpp
new file mode 100644 (file)
index 0000000..0d6b165
--- /dev/null
@@ -0,0 +1,129 @@
+#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);
+}
+
diff --git a/src/loader.h b/src/loader.h
new file mode 100644 (file)
index 0000000..fb0c8d8
--- /dev/null
@@ -0,0 +1,30 @@
+#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
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644 (file)
index 0000000..4b76222
--- /dev/null
@@ -0,0 +1,9 @@
+#include <QApplication>
+
+#include "app.h"
+
+int main(int argc, char *argv[])
+{
+    App a(argc, argv);
+    return a.exec();
+}
diff --git a/src/mesh.cpp b/src/mesh.cpp
new file mode 100644 (file)
index 0000000..cfb4ca4
--- /dev/null
@@ -0,0 +1,35 @@
+#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;
+}
diff --git a/src/mesh.h b/src/mesh.h
new file mode 100644 (file)
index 0000000..e8a02f0
--- /dev/null
@@ -0,0 +1,31 @@
+#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
diff --git a/src/window.cpp b/src/window.cpp
new file mode 100644 (file)
index 0000000..3002ecc
--- /dev/null
@@ -0,0 +1,146 @@
+#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());
+}
diff --git a/src/window.h b/src/window.h
new file mode 100644 (file)
index 0000000..cb92ebb
--- /dev/null
@@ -0,0 +1,36 @@
+#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