--- /dev/null
+#### Fast .stl viewer #####
+# Original Project Author: Matt Keeter Copyright 2014 -2017
+# Author: Paul Tsouchlos Copyright 2017
+
+cmake_minimum_required(VERSION 3.3)
+
+# Setting -std=c++11
+set(CMAKE_CXX_STANDARD 11)
+# Setting standard to required, as requisted by DeveloperPaul123 on github
+set(CXX_STANDARD_REQUIRED ON)
+
+# Set the version number
+set (FSTL_VERSION_MAJOR "0")
+set (FSTL_VERSION_MINOR "9")
+set (FSTL_VERSION_PATCH "4")
+set (PROJECT_VERSION "${FSTL_VERSION_MAJOR}.${FSTL_VERSION_MINOR}.${FSTL_VERSION_PATCH}")
+
+project(fstl)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+set(CMAKE_AUTOUIC ON)
+
+#set project sources
+set(Project_Sources src/app.cpp
+src/backdrop.cpp
+src/canvas.cpp
+src/glmesh.cpp
+src/loader.cpp
+src/main.cpp
+src/mesh.cpp
+src/window.cpp)
+
+#set project headers.
+set(Project_Headers src/app.h
+src/backdrop.h
+src/canvas.h
+src/glmesh.h
+src/loader.h
+src/mesh.h
+src/window.h)
+
+#set project resources and icon resource
+set(Project_Resources qt/qt.qrc gl/gl.qrc)
+set(Icon_Resource exe/fstl.rc)
+
+#set Policy CMP0072 FindOpenGL behavior
+set(OpenGL_GL_PREFERENCE GLVND)
+
+#find required packages.
+find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets OpenGL)
+find_package(OpenGL REQUIRED)
+find_package(Threads REQUIRED)
+
+#add resources to RCC
+qt5_add_resources(Project_Resources_RCC ${Project_Resources})
+
+#tell CMake AUTOGEN to skip autogen on the generated qrc files
+set_property(SOURCE ${Project_Resources_RCC} PROPERTY SKIP_AUTOGEN ON)
+
+#include opengl files.
+include_directories(${QT_QTOPENGL_INCLUDE_DIR} ${OPENGL_INCLUDE_DIR} )
+
+add_executable(fstl WIN32 ${Project_Sources} ${Project_Headers} ${Project_Resources_RCC} ${Icon_Resource})
+target_link_libraries(fstl Qt5::Widgets Qt5::Core Qt5::Gui Qt5::OpenGL ${OPENGL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
+if(WIN32)
+ set(Fstl_LINK_FLAGS ${CMAKE_CURRENT_SOURCE_DIR}/${Icon_Resource})
+ set_target_properties(fstl PROPERTIES LINK_FLAGS ${Fstl_LINK_FLAGS})
+endif(WIN32)
+
+# Add version definitions to use within the code.
+target_compile_definitions(fstl PRIVATE -DFSTL_VERSION="${PROJECT_VERSION}")
+
+#installer information that is platform independent
+set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Fast .stl file viewer.")
+set(CPACK_PACKAGE_VERSION_MAJOR ${FSTL_VERSION_MAJOR})
+set(CPACK_PACKAGE_VERSION_MINOR ${FSTL_VERSION_MINOR})
+set(CPACK_PACKAGE_VERSION_PATCH ${FSTL_VERSION_PATCH})
+
+if(WIN32)
+
+ set(QT_USE_QTMAIN true)
+
+ if(MSVC)
+ set_source_files_properties(fstl PROPERTIES LINKER_LANGUAGE "CXX")
+ set_target_properties(fstl PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS")
+
+ install(TARGETS fstl DESTINATION bin COMPONENT all)
+
+ install(FILES
+ $<TARGET_FILE:Qt5::Gui_EGL>
+ $<TARGET_FILE:Qt5::Gui_GLESv2>
+ $<TARGET_FILE:Qt5::Core>
+ $<TARGET_FILE:Qt5::Gui>
+ $<TARGET_FILE:Qt5::OpenGL>
+ $<TARGET_FILE:Qt5::Widgets>
+ DESTINATION bin COMPONENT all)
+
+ #install file in the platforms directory.
+ install (FILES
+ ${Qt5Core_DIR}/../../../plugins/platforms/qwindows.dll
+ DESTINATION bin/platforms COMPONENT all
+ )
+
+ #custom commands based on: https://gist.github.com/Rod-Persky/e6b93e9ee31f9516261b
+ add_custom_command(TARGET fstl POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:Qt5::Core> $<TARGET_FILE_DIR:${PROJECT_NAME}>
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:Qt5::Gui> $<TARGET_FILE_DIR:${PROJECT_NAME}>
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:Qt5::Widgets> $<TARGET_FILE_DIR:${PROJECT_NAME}>
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:Qt5::OpenGL> $<TARGET_FILE_DIR:${PROJECT_NAME}>
+ )
+ endif(MSVC)
+
+ # windows specific installer generation information
+ set(CPACK_GENERATOR NSIS)
+ set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL OFF)
+ set(CPACK_NSIS_MODIFY_PATH ON)
+ set(CPACK_NSIS_MUI_FINISHPAGE_RUN ${PROJECT_NAME})
+ set(CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}")
+ set(CPACK_NSIS_INSTALLED_ICON_NAME "bin\\\\fstl.exe")
+ set(CPACK_NSIS_URL_INFO_ABOUT "https://github.com/mkeeter/fstl")
+ set(CPACK_NSIS_DISPLAY_NAME "fstl ${FSTL_VERSION}")
+ set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/exe/fstl.ico")
+ set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/exe/fstl.ico")
+ set(CPACK_NSIS_CREATE_ICONS_EXTRA
+ "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\fstl.lnk' '$INSTDIR\\\\bin\\\\fstl.exe'")
+ set(CPACK_COMPONENTS_ALL all)
+ if (CMAKE_CL_64)
+ set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
+ else (CMAKE_CL_64)
+ set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES")
+ endif (CMAKE_CL_64)
+elseif(APPLE)
+ set(CPACK_GENERATOR "DragNDrop")
+ set(CPACK_DMG_FORMAT "UDBZ")
+ set(CPACK_DMG_VOLUME_NAME "${PROJECT_NAME}")
+ set(CPACK_SYSTEM_NAME "OSX")
+ set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
+ set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/app/fstl.icns")
+else()
+ install(TARGETS fstl RUNTIME DESTINATION bin)
+
+ set(CPACK_GENERATOR "DEB;RPM")
+ set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
+endif(WIN32)
+
+include(CPack)
the project is under 1K lines of code and should be fairly approachable.
## Screenshot
-![Eiffel tower](http://mattkeeter.com/projects/fstl/eiffel.png)
+![Eiffel tower](http://mattkeeter.com/projects/fstl/eiffel.png)
(credit to [Pranav Panchal](https://grabcad.com/pranav.panchal))
## Building
-The only dependency for `fstl` is [Qt](https://www.qt.io).
+The only dependency for `fstl` is [Qt 5](https://www.qt.io).
### macOS
- `fstl.app` is a standalone application that can be copied to `/Applications`
- `fstl.dmg` is a disk image that can be given to a friend
+### Linux
+
+Install Qt with your distro's package manager (required libraries are Core, Gui,
+Widgets and OpenGL, e.g. `qt5-default` and `libqt5opengl5-dev` on Debian).
+
+You can build fstl with qmake (in some distros qmake-qt5) or with CMake:
+```
+git clone https://github.com/mkeeter/fstl
+cd fstl
+mkdir build
+cd build
+
+qmake ../qt/fstl.pro # For qmake build
+cmake .. # For CMake build
+
+make -j8
+./fstl
+```
+
--------------------------------------------------------------------------------
# License
<qresource prefix="gl/">
<file>mesh.frag</file>
<file>mesh.vert</file>
+ <file>mesh_wireframe.frag</file>
<file>quad.frag</file>
<file>quad.vert</file>
<file>sphere.stl</file>
--- /dev/null
+#version 120
+
+uniform float zoom;
+
+varying vec3 ec_pos;
+
+void main() {
+ gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
+}
App::App(int& argc, char *argv[]) :
QApplication(argc, argv), window(new Window())
{
- window->show();
+ QCoreApplication::setOrganizationName("mkeeter");
+ QCoreApplication::setOrganizationDomain("https://github.com/mkeeter/fstl");
+ QCoreApplication::setApplicationName("fstl");
+
if (argc > 1)
window->load_stl(argv[1]);
else
window->load_stl(":gl/sphere.stl");
+ window->show();
+}
+
+App::~App()
+{
+ delete window;
}
bool App::event(QEvent* e)
Q_OBJECT
public:
explicit App(int& argc, char *argv[]);
+ ~App();
protected:
- bool event(QEvent* e);
+ bool event(QEvent* e) override;
private:
Window* const window;
Backdrop::Backdrop()
{
- initializeGLFunctions();
+ initializeOpenGLFunctions();
- shader.addShaderFromSourceFile(QGLShader::Vertex, ":/gl/quad.vert");
- shader.addShaderFromSourceFile(QGLShader::Fragment, ":/gl/quad.frag");
+ shader.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/gl/quad.vert");
+ shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/quad.frag");
shader.link();
float vbuf[] = {
#ifndef BACKDROP_H
#define BACKDROP_H
-#include <QtOpenGL/QGLFunctions>
-#include <QtOpenGL/QGLShaderProgram>
-#include <QtOpenGL/QGLBuffer>
+#include <QOpenGLBuffer>
+#include <QOpenGLShaderProgram>
+#include <QOpenGLFunctions>
-class Backdrop : protected QGLFunctions
+class Backdrop : protected QOpenGLFunctions
{
public:
Backdrop();
void draw();
private:
- QGLShaderProgram shader;
- QGLBuffer vertices;
+ QOpenGLShaderProgram shader;
+ QOpenGLBuffer vertices;
};
#endif // BACKDROP_H
#include <QMouseEvent>
-#include <QDebug>
#include <cmath>
#include "glmesh.h"
#include "mesh.h"
-Canvas::Canvas(const QGLFormat& format, QWidget *parent)
- : QGLWidget(format, parent), mesh(NULL),
+Canvas::Canvas(const QSurfaceFormat& format, QWidget *parent)
+ : QOpenGLWidget(parent), mesh(nullptr),
scale(1), zoom(1), tilt(90), yaw(0),
perspective(0.25), anim(this, "perspective"), status(" ")
{
+ setFormat(format);
QFile styleFile(":/qt/style.qss");
styleFile.open( QFile::ReadOnly );
setStyleSheet(styleFile.readAll());
Canvas::~Canvas()
{
- delete mesh;
+ makeCurrent();
+ delete mesh;
+ doneCurrent();
}
void Canvas::view_anim(float v)
view_anim(0.25);
}
+void Canvas::draw_shaded()
+{
+ set_drawMode(0);
+}
+
+void Canvas::draw_wireframe()
+{
+ set_drawMode(1);
+}
+
void Canvas::load_mesh(Mesh* m, bool is_reload)
{
mesh = new GLMesh(m);
update();
}
+void Canvas::set_drawMode(int mode)
+{
+ drawMode = mode;
+ update();
+}
+
void Canvas::clear_status()
{
status = "";
void Canvas::initializeGL()
{
- initializeGLFunctions();
+ initializeOpenGLFunctions();
- mesh_shader.addShaderFromSourceFile(QGLShader::Vertex, ":/gl/mesh.vert");
- mesh_shader.addShaderFromSourceFile(QGLShader::Fragment, ":/gl/mesh.frag");
+ mesh_shader.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/gl/mesh.vert");
+ mesh_shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/mesh.frag");
mesh_shader.link();
+ mesh_wireframe_shader.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/gl/mesh.vert");
+ mesh_wireframe_shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/mesh_wireframe.frag");
+ mesh_wireframe_shader.link();
backdrop = new Backdrop();
}
-void Canvas::paintEvent(QPaintEvent *event)
-{
- Q_UNUSED(event);
- glClearColor(0.0, 0.0, 0.0, 0.0);
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
- glEnable(GL_DEPTH_TEST);
+void Canvas::paintGL()
+{
+ glClearColor(0.0, 0.0, 0.0, 0.0);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+ glEnable(GL_DEPTH_TEST);
- backdrop->draw();
- if (mesh) draw_mesh();
+ backdrop->draw();
+ if (mesh) draw_mesh();
- if (status.isNull()) return;
+ if (status.isNull()) return;
- QPainter painter(this);
- painter.setRenderHint(QPainter::Antialiasing);
- painter.drawText(10, height() - 10, status);
+ QPainter painter(this);
+ painter.setRenderHint(QPainter::Antialiasing);
+ painter.drawText(10, height() - 10, status);
}
-
void Canvas::draw_mesh()
{
- mesh_shader.bind();
+ QOpenGLShaderProgram* selected_mesh_shader = NULL;
+ // Set gl draw mode
+ if(drawMode == 1)
+ {
+ selected_mesh_shader = &mesh_wireframe_shader;
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+ }
+ else
+ {
+ selected_mesh_shader = &mesh_shader;
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+ }
+
+ selected_mesh_shader->bind();
// Load the transform and view matrices into the shader
glUniformMatrix4fv(
- mesh_shader.uniformLocation("transform_matrix"),
+ selected_mesh_shader->uniformLocation("transform_matrix"),
1, GL_FALSE, transform_matrix().data());
glUniformMatrix4fv(
- mesh_shader.uniformLocation("view_matrix"),
+ selected_mesh_shader->uniformLocation("view_matrix"),
1, GL_FALSE, view_matrix().data());
// Compensate for z-flattening when zooming
- glUniform1f(mesh_shader.uniformLocation("zoom"), 1/zoom);
+ glUniform1f(selected_mesh_shader->uniformLocation("zoom"), 1/zoom);
// Find and enable the attribute location for vertex position
- const GLuint vp = mesh_shader.attributeLocation("vertex_position");
+ const GLuint vp = selected_mesh_shader->attributeLocation("vertex_position");
glEnableVertexAttribArray(vp);
// Then draw the mesh with that vertex position
mesh->draw(vp);
+ // Reset draw mode for the background and anything else that needs to be drawn
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+
// Clean up state machine
glDisableVertexAttribArray(vp);
- mesh_shader.release();
+ selected_mesh_shader->release();
}
QMatrix4x4 Canvas::transform_matrix() const
#ifndef CANVAS_H
#define CANVAS_H
-#include <QWidget>
-#include <QPropertyAnimation>
-#include <QtOpenGL/QGLWidget>
-#include <QtOpenGL/QGLFunctions>
-#include <QtOpenGL/QGLShaderProgram>
-#include <QMatrix4x4>
+#include <QtOpenGL>
+#include <QSurfaceFormat>
+#include <QOpenGLShaderProgram>
class GLMesh;
class Mesh;
class Backdrop;
-class Canvas : public QGLWidget, protected QGLFunctions
+class Canvas : public QOpenGLWidget, protected QOpenGLFunctions
{
Q_OBJECT
public:
- Canvas(const QGLFormat& format, QWidget* parent=0);
-
- void initializeGL();
- void paintEvent(QPaintEvent* event);
+ explicit Canvas(const QSurfaceFormat& format, QWidget* parent=0);
~Canvas();
void view_orthographic();
void view_perspective();
+ void draw_shaded();
+ void draw_wireframe();
public slots:
void set_status(const QString& s);
void clear_status();
void load_mesh(Mesh* m, bool is_reload);
-
protected:
- void mousePressEvent(QMouseEvent* event);
- void mouseReleaseEvent(QMouseEvent* event);
- void mouseMoveEvent(QMouseEvent* event);
- void wheelEvent(QWheelEvent* event);
- void resizeGL(int width, int height);
- void set_perspective(float p);
+ void paintGL() override;
+ void initializeGL() override;
+ void resizeGL(int width, int height) override;
+
+ void mousePressEvent(QMouseEvent* event) override;
+ void mouseReleaseEvent(QMouseEvent* event) override;
+ void mouseMoveEvent(QMouseEvent* event) override;
+ void wheelEvent(QWheelEvent* event) override;
+
+ void set_perspective(float p);
+ void set_drawMode(int mode);
void view_anim(float v);
-
private:
void draw_mesh();
QMatrix4x4 transform_matrix() const;
QMatrix4x4 view_matrix() const;
- QGLShaderProgram mesh_shader;
- QGLShaderProgram quad_shader;
+ QOpenGLShaderProgram mesh_shader;
+ QOpenGLShaderProgram mesh_wireframe_shader;
+ QOpenGLShaderProgram quad_shader;
GLMesh* mesh;
Backdrop* backdrop;
float yaw;
float perspective;
- Q_PROPERTY(float perspective WRITE set_perspective);
+ int drawMode;
+ Q_PROPERTY(float perspective MEMBER perspective WRITE set_perspective);
QPropertyAnimation anim;
QPoint mouse_pos;
#include "mesh.h"
GLMesh::GLMesh(const Mesh* const mesh)
- : vertices(QGLBuffer::VertexBuffer), indices(QGLBuffer::IndexBuffer)
+ : vertices(QOpenGLBuffer::VertexBuffer), indices(QOpenGLBuffer::IndexBuffer)
{
- initializeGLFunctions();
+ initializeOpenGLFunctions();
vertices.create();
indices.create();
- vertices.setUsagePattern(QGLBuffer::StaticDraw);
- indices.setUsagePattern(QGLBuffer::StaticDraw);
+ vertices.setUsagePattern(QOpenGLBuffer::StaticDraw);
+ indices.setUsagePattern(QOpenGLBuffer::StaticDraw);
vertices.bind();
vertices.allocate(mesh->vertices.data(),
#ifndef GLMESH_H
#define GLMESH_H
-#include <QtOpenGL/QGLBuffer>
-#include <QtOpenGL/QGLFunctions>
+#include <QOpenGLBuffer>
+#include <QOpenGLFunctions>
+// forward declaration
class Mesh;
-class GLMesh : protected QGLFunctions
+class GLMesh : protected QOpenGLFunctions
{
public:
GLMesh(const Mesh* const mesh);
void draw(GLuint vp);
private:
- QGLBuffer vertices;
- QGLBuffer indices;
+ QOpenGLBuffer vertices;
+ QOpenGLBuffer indices;
};
#endif // GLMESH_H
#include <future>
#include "loader.h"
+#include "vertex.h"
Loader::Loader(QObject* parent, const QString& filename, bool is_reload)
: QThread(parent), filename(filename), is_reload(is_reload)
////////////////////////////////////////////////////////////////////////////////
-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;
-
-void parallel_sort(Vec3i* begin, Vec3i* end, int threads)
+void parallel_sort(Vertex* begin, Vertex* end, int threads)
{
if (threads < 2 || end - begin < 2)
{
}
}
-Mesh* mesh_from_verts(uint32_t tri_count, QVector<Vec3i>& verts)
+Mesh* mesh_from_verts(uint32_t tri_count, QVector<Vertex>& verts)
{
// Save indicies as the second element in the array
// (so that we can reconstruct triangle order after sorting)
for (size_t i=0; i < tri_count*3; ++i)
{
- verts[i].second = i;
+ verts[i].i = i;
+ }
+
+ // Check how many threads the hardware can safely support. This may return
+ // 0 if the property can't be read so we shoud check for that too.
+ auto threads = std::thread::hardware_concurrency();
+ if (threads == 0)
+ {
+ threads = 8;
}
// Sort the set of vertices (to deduplicate)
- parallel_sort(verts.begin(), verts.end(), 8);
+ parallel_sort(verts.begin(), verts.end(), threads);
// This vector will store triangles as sets of 3 indices
std::vector<GLuint> indices(tri_count*3);
size_t vertex_count = 0;
for (auto v : verts)
{
- if (!vertex_count || v.first != verts[vertex_count-1].first)
+ if (!vertex_count || v != verts[vertex_count-1])
{
verts[vertex_count++] = v;
}
- indices[v.second] = vertex_count - 1;
+ indices[v.i] = vertex_count - 1;
}
verts.resize(vertex_count);
flat_verts.reserve(vertex_count*3);
for (auto v : verts)
{
- flat_verts.push_back(v.first.x);
- flat_verts.push_back(v.first.y);
- flat_verts.push_back(v.first.z);
+ flat_verts.push_back(v.x);
+ flat_verts.push_back(v.y);
+ flat_verts.push_back(v.z);
}
- return new Mesh(flat_verts, indices);
+ return new Mesh(std::move(flat_verts), std::move(indices));
}
////////////////////////////////////////////////////////////////////////////////
}
// First, try to read the stl as an ASCII file
- if (file.read(6) == "solid ")
+ if (file.read(5) == "solid")
{
file.readLine(); // skip solid name
const auto line = file.readLine().trimmed();
}
// Extract vertices into an array of xyz, unsigned pairs
- QVector<Vec3i> verts(tri_count*3);
+ QVector<Vertex> verts(tri_count*3);
// Dummy array, because readRawData is faster than skipRawData
- uint8_t* buffer = (uint8_t*)malloc(tri_count * 50);
- data.readRawData((char*)buffer, tri_count * 50);
+ std::unique_ptr<uint8_t> buffer(new uint8_t[tri_count * 50]);
+ data.readRawData((char*)buffer.get(), tri_count * 50);
// Store vertices in the array, processing one triangle at a time.
- auto b = buffer;
+ auto b = buffer.get() + 3 * sizeof(float);
for (auto v=verts.begin(); v != verts.end(); v += 3)
{
- // Skip face's normal vector
- b += 3 * sizeof(float);
-
// Load vertex data from .stl file into vertices
for (unsigned i=0; i < 3; ++i)
{
- memcpy(&v[i].first, b, 3*sizeof(float));
+ memcpy(&v[i], b, 3*sizeof(float));
b += 3 * sizeof(float);
}
- // Skip face attribute
- b += sizeof(uint16_t);
+ // Skip face attribute and next face's normal vector
+ b += 3 * sizeof(float) + sizeof(uint16_t);
}
if (confusing_stl)
{
emit warning_confusing_stl();
}
- free(buffer);
return mesh_from_verts(tri_count, verts);
}
{
file.readLine();
uint32_t tri_count = 0;
- QVector<Vec3i> verts(tri_count*3);
+ QVector<Vertex> verts(tri_count*3);
bool okay = true;
while (!file.atEnd() && okay)
const float x = line[1].toFloat(&okay);
const float y = line[2].toFloat(&okay);
const float z = line[3].toFloat(&okay);
- verts.push_back({{x, y, z}, 0});
+ verts.push_back(Vertex(x, y, z));
}
if (!file.readLine().trimmed().startsWith("endloop") ||
!file.readLine().trimmed().startsWith("endfacet"))
////////////////////////////////////////////////////////////////////////////////
-Mesh::Mesh(std::vector<GLfloat> v, std::vector<GLuint> i)
- : vertices(v), indices(i)
+Mesh::Mesh(std::vector<GLfloat>&& v, std::vector<GLuint>&& i)
+ : vertices(std::move(v)), indices(std::move(i))
{
// Nothing to do here
}
class Mesh
{
public:
- Mesh(std::vector<GLfloat> vertices, std::vector<GLuint> indices);
+ Mesh(std::vector<GLfloat>&& vertices, std::vector<GLuint>&& indices);
float min(size_t start) const;
float max(size_t start) const;
--- /dev/null
+#ifndef VEC3_H
+#define VEC3_H
+
+#include <QtOpenGL/QtOpenGL>
+
+/*
+ * Represents an optionally-indexed vertex in space
+ */
+struct Vertex
+{
+ Vertex() {}
+ Vertex(float x, float y, float z) : x(x), y(y), z(z) {}
+
+ GLfloat x, y, z;
+ GLuint i=0;
+
+ bool operator!=(const Vertex& rhs) const
+ {
+ return x != rhs.x || y != rhs.y || z != rhs.z;
+ }
+ bool operator<(const Vertex& rhs) const
+ {
+ if (x != rhs.x) return x < rhs.x;
+ else if (y != rhs.y) return y < rhs.y;
+ else if (z != rhs.z) return z < rhs.z;
+ else return false;
+ }
+};
+
+#endif
#include <QMenuBar>
-#include <QMessageBox>
-#include <QFileDialog>
#include "window.h"
#include "canvas.h"
quit_action(new QAction("Quit", this)),
perspective_action(new QAction("Perspective", this)),
orthogonal_action(new QAction("Orthographic", this)),
+ shaded_action(new QAction("Shaded", this)),
+ wireframe_action(new QAction("Wireframe", this)),
reload_action(new QAction("Reload", this)),
autoreload_action(new QAction("Autoreload", this)),
+ save_screenshot_action(new QAction("Save Screenshot", this)),
recent_files(new QMenu("Open recent", this)),
recent_files_group(new QActionGroup(this)),
recent_files_clear_action(new QAction("Clear recent files", this)),
setWindowTitle("fstl");
setAcceptDrops(true);
- QGLFormat format;
+ QSurfaceFormat format;
+ format.setDepthBufferSize(24);
+ format.setStencilBufferSize(8);
format.setVersion(2, 1);
- format.setSampleBuffers(true);
+ format.setProfile(QSurfaceFormat::CoreProfile);
+ QSurfaceFormat::setDefaultFormat(format);
+
canvas = new Canvas(format, this);
setCentralWidget(canvas);
QObject::connect(recent_files_group, &QActionGroup::triggered,
this, &Window::on_load_recent);
+ save_screenshot_action->setCheckable(false);
+ QObject::connect(save_screenshot_action, &QAction::triggered,
+ this, &Window::on_save_screenshot);
+
rebuild_recent_files();
auto file_menu = menuBar()->addMenu("File");
file_menu->addSeparator();
file_menu->addAction(reload_action);
file_menu->addAction(autoreload_action);
+ file_menu->addAction(save_screenshot_action);
file_menu->addAction(quit_action);
auto view_menu = menuBar()->addMenu("View");
QObject::connect(projections, &QActionGroup::triggered,
this, &Window::on_projection);
+ auto draw_menu = view_menu->addMenu("Draw Mode");
+ draw_menu->addAction(shaded_action);
+ draw_menu->addAction(wireframe_action);
+ auto drawModes = new QActionGroup(draw_menu);
+ for (auto p : {shaded_action, wireframe_action})
+ {
+ drawModes->addAction(p);
+ p->setCheckable(true);
+ }
+ shaded_action->setChecked(true);
+ drawModes->setExclusive(true);
+ QObject::connect(drawModes, &QActionGroup::triggered,
+ this, &Window::on_drawMode);
+
auto help_menu = menuBar()->addMenu("Help");
help_menu->addAction(about_action);
{
QString filename = QFileDialog::getOpenFileName(
this, "Load .stl file", QString(), "*.stl");
- if (not filename.isNull())
+ if (!filename.isNull())
{
load_stl(filename);
}
}
}
+void Window::on_drawMode(QAction* mode)
+{
+ if (mode == shaded_action)
+ {
+ canvas->draw_shaded();
+ }
+ else
+ {
+ canvas->draw_wireframe();
+ }
+}
+
void Window::on_watched_change(const QString& filename)
{
if (autoreload_action->isChecked())
load_stl(a->data().toString());
}
+void Window::on_loaded(const QString& filename)
+{
+ current_file = filename;
+}
+
+void Window::on_save_screenshot()
+{
+ const auto image = canvas->grabFramebuffer();
+ auto file_name = QFileDialog::getSaveFileName(
+ this,
+ tr("Save Screenshot Image"),
+ QStandardPaths::standardLocations(QStandardPaths::StandardLocation::PicturesLocation).first(),
+ "Images (*.png *.jpg)");
+
+ auto get_file_extension = [](const std::string& file_name) -> std::string
+ {
+ const auto location = std::find(file_name.rbegin(), file_name.rend(), '.');
+ if (location == file_name.rend())
+ {
+ return "";
+ }
+
+ const auto index = std::distance(file_name.rbegin(), location);
+ return file_name.substr(file_name.size() - index);
+ };
+
+ const auto extension = get_file_extension(file_name.toStdString());
+ if(extension.empty() || (extension != "png" && extension != "jpg"))
+ {
+ file_name.append(".png");
+ }
+
+ const auto save_ok = image.save(file_name);
+ if(!save_ok)
+ {
+ QMessageBox::warning(this, tr("Error Saving Image"), tr("Unable to save screen shot image."));
+ }
+}
+
void Window::rebuild_recent_files()
{
QSettings settings;
this, &Window::setWindowTitle);
connect(loader, &Loader::loaded_file,
this, &Window::set_watched);
+ connect(loader, &Loader::loaded_file,
+ this, &Window::on_loaded);
autoreload_action->setEnabled(true);
reload_action->setEnabled(true);
}
{
load_stl(event->mimeData()->urls().front().toLocalFile());
}
+
+void Window::sorted_insert(QStringList& list, const QCollator& collator, const QString& value)
+{
+ int start = 0;
+ int end = list.size() - 1;
+ int index = 0;
+ while (start <= end){
+ int mid = (start+end)/2;
+ if (list[mid] == value) {
+ return;
+ }
+ int compare = collator.compare(value, list[mid]);
+ if (compare < 0) {
+ end = mid-1;
+ index = mid;
+ } else {
+ start = mid+1;
+ index = start;
+ }
+ }
+
+ list.insert(index, value);
+}
+
+void Window::build_folder_file_list()
+{
+ QString current_folder_path = QFileInfo(current_file).absoluteDir().absolutePath();
+ if (!lookup_folder_files.isEmpty())
+ {
+ if (current_folder_path == lookup_folder) {
+ return;
+ }
+
+ lookup_folder_files.clear();
+ }
+ lookup_folder = current_folder_path;
+
+ QCollator collator;
+ collator.setNumericMode(true);
+
+ QDirIterator dirIterator(lookup_folder, QStringList() << "*.stl", QDir::Files | QDir::Readable | QDir::Hidden);
+ while (dirIterator.hasNext()) {
+ dirIterator.next();
+
+ QString name = dirIterator.fileName();
+ sorted_insert(lookup_folder_files, collator, name);
+ }
+}
+
+QPair<QString, QString> Window::get_file_neighbors()
+{
+ if (current_file.isEmpty()) {
+ return QPair<QString, QString>(QString::null, QString::null);
+ }
+
+ build_folder_file_list();
+
+ QFileInfo fileInfo(current_file);
+
+ QString current_dir = fileInfo.absoluteDir().absolutePath();
+ QString current_name = fileInfo.fileName();
+
+ QString prev = QString::null;
+ QString next = QString::null;
+
+ QListIterator<QString> fileIterator(lookup_folder_files);
+ while (fileIterator.hasNext()) {
+ QString name = fileIterator.next();
+
+ if (name == current_name) {
+ if (fileIterator.hasNext()) {
+ next = current_dir + QDir::separator() + fileIterator.next();
+ }
+ break;
+ }
+
+ prev = name;
+ }
+
+ if (!prev.isEmpty()) {
+ prev.prepend(QDir::separator());
+ prev.prepend(current_dir);
+ }
+
+ return QPair<QString, QString>(prev, next);
+}
+
+bool Window::load_prev(void)
+{
+ QPair<QString, QString> neighbors = get_file_neighbors();
+ if (neighbors.first.isEmpty()) {
+ return false;
+ }
+
+ return load_stl(neighbors.first);
+}
+
+bool Window::load_next(void)
+{
+ QPair<QString, QString> neighbors = get_file_neighbors();
+ if (neighbors.second.isEmpty()) {
+ return false;
+ }
+
+ return load_stl(neighbors.second);
+}
+
+void Window::keyPressEvent(QKeyEvent* event)
+{
+ if (!open_action->isEnabled())
+ {
+ QMainWindow::keyPressEvent(event);
+ return;
+ }
+
+ if (event->key() == Qt::Key_Left)
+ {
+ load_prev();
+ return;
+ }
+ else if (event->key() == Qt::Key_Right)
+ {
+ load_next();
+ return;
+ }
+
+ QMainWindow::keyPressEvent(event);
+}
#include <QMainWindow>
#include <QActionGroup>
#include <QFileSystemWatcher>
+#include <QCollator>
class Canvas;
public:
explicit Window(QWidget* parent=0);
bool load_stl(const QString& filename, bool is_reload=false);
+ bool load_prev(void);
+ bool load_next(void);
protected:
- void dragEnterEvent(QDragEnterEvent* event);
- void dropEvent(QDropEvent* event);
+ void dragEnterEvent(QDragEnterEvent* event) override;
+ void dropEvent(QDropEvent* event) override;
+ void keyPressEvent(QKeyEvent* event) override;
public slots:
void on_open();
private slots:
void on_projection(QAction* proj);
+ void on_drawMode(QAction* mode);
void on_watched_change(const QString& filename);
void on_reload();
void on_autoreload_triggered(bool r);
void on_clear_recent();
void on_load_recent(QAction* a);
-
+ void on_loaded(const QString& filename);
+ void on_save_screenshot();
+
private:
void rebuild_recent_files();
+ void sorted_insert(QStringList& list, const QCollator& collator, const QString& value);
+ void build_folder_file_list();
+ QPair<QString, QString> get_file_neighbors();
QAction* const open_action;
QAction* const about_action;
QAction* const quit_action;
QAction* const perspective_action;
QAction* const orthogonal_action;
+ QAction* const shaded_action;
+ QAction* const wireframe_action;
QAction* const reload_action;
QAction* const autoreload_action;
+ QAction* const save_screenshot_action;
QMenu* const recent_files;
QActionGroup* const recent_files_group;
QAction* const recent_files_clear_action;
const static int MAX_RECENT_FILES=8;
const static QString RECENT_FILE_KEY;
+ QString current_file;
+ QString lookup_folder;
+ QStringList lookup_folder_files;
QFileSystemWatcher* watcher;