From: Jakob Haufe Date: Wed, 29 Jun 2022 14:02:12 +0000 (+0200) Subject: New upstream version 0.10.0 X-Git-Url: https://git.sur5r.net/?p=fstl;a=commitdiff_plain;h=refs%2Fheads%2Fupstream;hp=967d178c4343c689c728571bb10d5c94ab8d9d13 New upstream version 0.10.0 --- diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0628afa --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,156 @@ +#### Fast .stl viewer ##### +# Original Project Author: Matt Keeter Copyright 2014 -2017 +# Author: Paul Tsouchlos Copyright 2017 + +cmake_minimum_required(VERSION 3.3) + +project(fstl) + +# 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 "10") +set (FSTL_VERSION_PATCH "0") +set (PROJECT_VERSION "${FSTL_VERSION_MAJOR}.${FSTL_VERSION_MINOR}.${FSTL_VERSION_PATCH}") + +message(STATUS "Version: ${PROJECT_VERSION}") + +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/axis.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/axis.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 5.14 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} ) + +if(WIN32) + add_executable(fstl WIN32 ${Project_Sources} ${Project_Headers} ${Project_Resources_RCC} ${Icon_Resource}) + set(Fstl_LINK_FLAGS ${CMAKE_CURRENT_SOURCE_DIR}/${Icon_Resource}) + set_target_properties(fstl PROPERTIES LINK_FLAGS ${Fstl_LINK_FLAGS}) +elseif(APPLE) + add_executable(fstl MACOSX_BUNDLE ${Project_Sources} ${Project_Headers} ${Project_Resources_RCC} ${Icon_Resource}) +else() + add_executable(fstl ${Project_Sources} ${Project_Headers} ${Project_Resources_RCC} ${Icon_Resource}) +endif(WIN32) + +target_link_libraries(fstl Qt5::Widgets Qt5::Core Qt5::Gui Qt5::OpenGL ${OPENGL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) + +# 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 + $ + $ + $ + $ + $ + $ + 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 $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + ) + 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/fstl-app/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) diff --git a/README.md b/README.md index a9a845f..c58eeae 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,123 @@ -`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 a very fast [.stl file](http://en.wikipedia.org/wiki/STL_\(file_format\)) viewer. + +It was originally written by [Matt Keeter](https://mattkeeter.com), +and is now primarily maintained by [@DeveloperPaul123](https://github.com/DeveloperPaul123). 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)) + +## Setting `fstl` as the Default STL Viewer + +### Windows + +1. Right-click an STL file +2. Select `Open With` >>> `Choose another app` +3. Select `More Apps` and `Look for another app on this PC` +4. Enter the path to the `fstl` EXE file + +### MacOS + +1. Ctrl+click an STL file +2. Select `Get Info` +3. Navigate to the `Open with` section +4. Select `fstl` in the dropdown +5. Click `Change All` + +### Linux + +If `mimeopen` is available on your system, it can be used to set `fstl` as the default viewer for STL files. +Run the following in your terminal: + +```bash +# replace example.stl with an actual file +mimeopen -d example.stl +``` + +The following output will result: + +``` +Please choose a default application for files of type model/stl + + 1) Other... + +use application # +``` -License -------- -(c) 2014 Matt Keeter +Select the `Other` option and type `fstl` as the desired command to open STL files. +This will now become the system default, even when opening files from the file manager. -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. +## Building + +The only dependency for `fstl` is [Qt 5](https://www.qt.io), +plus [`cmake`](https://cmake.org/) for building. + +### macOS + +Install Qt from their website or [Homebrew](brew.sh). + +Install `cmake` through Homebrew or equivalent. + +Then, run through the following set of commands in a shell: + +``` +git clone https://github.com/fstl-app/fstl +cd fstl +mkdir build +cd build +cmake -DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt/5.15.0/ .. +make -j8 +./fstl.app/Contents/MacOS/fstl +``` + +You may need to edit the Qt path depending on your installation. + +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 + +### 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 CMake: +``` +git clone https://github.com/fstl-app/fstl +cd fstl +mkdir build +cd build +cmake .. +make -j8 +./fstl +``` -------------------------------------------------------------------------------- -*p.s.* In my day job, I'm an engineer at [Formlabs](http://formlabs.com). -If you think `fstl` is cool, you should check out our -[jobs page](http://formlabs.com/jobs) -- we're hiring software developers -and engineers of all stripes to work on interesting, hard problems in -consumer-level 3D printing. +# License + +Copyright (c) 2014-2017 Matthew Keeter + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/app/package.sh b/app/package.sh index d51c470..6cf065b 100755 --- a/app/package.sh +++ b/app/package.sh @@ -1,14 +1,53 @@ #!/bin/sh +set -x -e + cd ../build -macdeployqt fstl.app +make clean +rm -rf fstl.app +make -j8 + +APP=fstl + +# Pull out framework paths info with otool +MACDEPLOYQT=`otool -L $APP.app/Contents/MacOS/fstl | sed -n -e "s:\(.*\)lib/QtCore.*:\1/bin/macdeployqt:gp"` + +$MACDEPLOYQT $APP.app +cp ../app/Info.plist $APP.app/Contents/ + +# Delete unused Qt plugins cd fstl.app/Contents/PlugIns rm -rf accessible audio imageformats mediaservice playlistformats position printsupport qml1tooling sensorgestures sensors -cd ../Frameworks -rm -rf QtDeclarative.framework QtMultimedia.framework QtMultimediaWidgets.framework QtNetwork.framework QtPositioning.framework QtQml.framework QtQuick.framework QtScript.framework QtSensors.framework QtSql.framework QtXmlPatterns.framework + +fix_qt () { + echo "Fixing Qt for $1" + for LIB in $( otool -L $1 | sed -n -e "s:\(.*Qt.*.framework[^ ]*\).*:\1:gp" ) + do + RENAMED=`echo $LIB | sed -n -e "s:.*\(Qt.*\)\.framework.*:@executable_path/../Frameworks/\1.framework/Versions/5/\1:gp"` + install_name_tool -change $LIB $RENAMED $1 + done +} + +# Remap platform links +cd platforms +fix_qt libqcocoa.dylib + +# Delete unused Qt frameworks +cd ../../Frameworks +rm -rf QtDeclarative.framework QtMultimedia.framework QtMultimediaWidgets.framework QtNetwork.framework QtPositioning.framework QtQml.framework QtQuick.framework QtScript.framework QtSensors.framework QtSql.framework QtXmlPatterns.framework Qt3DCore.framework Qt3DRender.framework QtLocation.framework QtSerialBus.framework QtSerialPort.framework + +# Clean up remaining Qt frameworks +for LIB in $( ls|sed -n -e "s:\(Qt.*\)\.framework:\1:gp" ) +do + fix_qt $LIB.framework/Versions/Current/$LIB +done + cd ../Resources -rm empty.lproj -cd ../../.. -cp -r fstl.app .. -cd .. -zip -r fstl_mac.zip fstl.app README.md +# Create a disk image +cd ../../.. +mkdir $APP +cp ../README.md ./$APP/README.txt +cp -R $APP.app ./$APP +hdiutil create $APP.dmg -volname "$APP" -srcfolder $APP +rm -rf $APP +mv $APP.dmg .. diff --git a/exe/package.sh b/exe/package.sh index ef81b62..dfc4115 100644 --- a/exe/package.sh +++ b/exe/package.sh @@ -1,3 +1,13 @@ -cd .. -cp build/release/fstl.exe . -/c/Program\ Files/7-Zip/7z.exe a fstl_win.zip fstl.exe README.md +#!/bin/sh +set -e -x + +cd ../build +make clean +rm -rf fstl.exe +make -j8 + +cp ../README.md README.txt +cp release/fstl.exe . +/c/Program\ Files/7-Zip/7z.exe a fstl.zip fstl.exe README.txt +cp fstl.zip .. +rm README.txt fstl.exe diff --git a/gl/colored_lines.frag b/gl/colored_lines.frag new file mode 100644 index 0000000..1c02e1b --- /dev/null +++ b/gl/colored_lines.frag @@ -0,0 +1,7 @@ +#version 120 + +varying vec3 frag_color; + +void main() { + gl_FragColor = vec4(frag_color, 1.0); +} diff --git a/gl/colored_lines.vert b/gl/colored_lines.vert new file mode 100644 index 0000000..2100403 --- /dev/null +++ b/gl/colored_lines.vert @@ -0,0 +1,14 @@ +#version 120 +attribute vec3 vertex_position; +attribute vec3 vertex_color; + +uniform mat4 transform_matrix; +uniform mat4 view_matrix; + +varying vec3 frag_color; + +void main() { + gl_Position = view_matrix*transform_matrix* + vec4(vertex_position, 1.0); + frag_color = vertex_color; +} diff --git a/gl/gl.qrc b/gl/gl.qrc index ef93a0e..6e8baa9 100644 --- a/gl/gl.qrc +++ b/gl/gl.qrc @@ -2,8 +2,12 @@ mesh.frag mesh.vert + mesh_wireframe.frag + mesh_surfaceangle.frag quad.frag quad.vert + colored_lines.frag + colored_lines.vert sphere.stl diff --git a/gl/mesh_surfaceangle.frag b/gl/mesh_surfaceangle.frag new file mode 100644 index 0000000..3ee94dd --- /dev/null +++ b/gl/mesh_surfaceangle.frag @@ -0,0 +1,17 @@ +#version 120 + +uniform float zoom; + +varying vec3 ec_pos; + +void main() { + vec3 ec_normal = normalize(cross(dFdx(ec_pos), dFdy(ec_pos))); + ec_normal.z *= zoom; + ec_normal = normalize(ec_normal); + //rotated 10deg around the red axis for better color match + float x = dot(ec_normal, vec3(1.0, 0.0, 0.0)); + float y = dot(ec_normal, vec3(0.0, 0.985, 0.174)); + float z = dot(ec_normal, vec3(0.0, -0.174, 0.985)); + + gl_FragColor = vec4(0.5-0.5*x, 0.5-0.5*y, 0.5+0.5*z, 1.0); +} diff --git a/gl/mesh_wireframe.frag b/gl/mesh_wireframe.frag new file mode 100644 index 0000000..13f001c --- /dev/null +++ b/gl/mesh_wireframe.frag @@ -0,0 +1,9 @@ +#version 120 + +uniform float zoom; + +varying vec3 ec_pos; + +void main() { + gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); +} diff --git a/qt/fstl.pro b/qt/fstl.pro deleted file mode 100644 index 7a0f449..0000000 --- a/qt/fstl.pro +++ /dev/null @@ -1,46 +0,0 @@ -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/style.qss b/qt/style.qss index 0297148..3f0ca9b 100644 --- a/qt/style.qss +++ b/qt/style.qss @@ -1,19 +1,4 @@ QWidget { - background-color: #fdf6e3; + background-color: null; color: #839496; } - -QPushButton { - background-color: #eee8d5; - border-top-width: 5px; - border-bottom-width: 5px; - border-left-width: 30px; - border-right-width: 30px; - border-style: flat; - margin: 0px; - color: #839496; -} - -QPushButton:pressed { - background-color: #ddd7c4; -} diff --git a/src/app.cpp b/src/app.cpp index b6eb3af..a76a7da 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -4,14 +4,19 @@ #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(); 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) diff --git a/src/app.h b/src/app.h index 7a7672e..6afb4d7 100644 --- a/src/app.h +++ b/src/app.h @@ -9,9 +9,10 @@ class App : public QApplication { Q_OBJECT public: - explicit App(int argc, char *argv[]); + explicit App(int& argc, char *argv[]); + ~App(); protected: - bool event(QEvent* e); + bool event(QEvent* e) override; private: Window* const window; diff --git a/src/axis.cpp b/src/axis.cpp new file mode 100644 index 0000000..831f592 --- /dev/null +++ b/src/axis.cpp @@ -0,0 +1,163 @@ +#include "axis.h" + +const float xLet[] = { + -0.1, -0.2, 0, + 0.1, 0.2, 0, + 0.1, -0.2, 0, + -0.1, 0.2, 0 +}; +const float yLet[] = { + 0, -0.2, 0, + 0, 0, 0, + 0, 0, 0, + 0.1, 0.2, 0, + 0, 0, 0, + -0.1, 0.2, 0 +}; +const float zLet[] = { + -0.1, -0.2, 0, + 0.1, -0.2, 0, + 0.1, -0.2, 0, + -0.1, 0.2, 0, + -0.1, 0.2, 0, + 0.1, 0.2, 0 +}; +const int axisSegCount[] = {2, 3, 3}; +const float* axisLabels[] = {xLet, yLet, zLet}; + +Axis::Axis() +{ + initializeOpenGLFunctions(); + + shader.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/gl/colored_lines.vert"); + shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/colored_lines.frag"); + shader.link(); + const int ptSize = 6*sizeof(float); + for(int lIdx = 0; lIdx < 3; lIdx++) + { + const float* l = axisLabels[lIdx]; + const int ptCount = axisSegCount[lIdx]*2; + float c[3] = {0.0}; + c[lIdx] = 1.0;//set color + QOpenGLBuffer b = flowerLabelVertices[lIdx]; + b.create(); + b.bind(); + b.allocate(ptCount*ptSize); + for(int pIdx = 0; pIdx < ptCount; pIdx++) + { + b.write(pIdx*ptSize, &(l[pIdx*3]), ptSize/2);//write coords + b.write(pIdx*ptSize + ptSize/2, c, ptSize/2);//write color + } + b.release(); + } + //Axis buffer: 6 floats per vertex, 2 vert per line, 3 lines + float aBuf[6*2*3] = {0.0}; + for(int aIdx = 0; aIdx < 3; aIdx++) + { + aBuf[(2*aIdx)*6+3+aIdx] = 1.0;//Set color (last 3 floats) + aBuf[(2*aIdx+1)*6+3+aIdx] = 1.0;//Set color (last 3 floats) + aBuf[(2*aIdx+1)*6+aIdx] = 1.0;//Extend line in axis + } + //The lines which form the 'axis-flower' in the corner + flowerAxisVertices.create(); + flowerAxisVertices.bind(); + flowerAxisVertices.allocate(aBuf, sizeof(aBuf)); + flowerAxisVertices.release(); + //The lines which form the model-space axes + vertices.create(); + vertices.bind(); + vertices.allocate(aBuf, sizeof(aBuf)); + vertices.release(); +} +void Axis::setScale(QVector3D min, QVector3D max) +{ + //Max function. not worth importing just for max + auto Max = [](float a, float b) + { + return (a > b) ? a : b; + }; + //This is how much the axes extend beyond the model + //We want it to be dependent on the model's size, but uniform on all axes. + const float axismargin = 0.25*Max(Max(max[0]-min[0], max[1]-min[1]), max[2]-min[2]); + vertices.bind(); + //Manually rewrite coordinates to control axis draw lengths + float s = sizeof(float); + //aIdx*12+aIdx gets us to the set of 2 points of the axis line, plus the offset for that dimension + //+6 gets us to the other end of the line in that dimension + for(int aIdx = 0; aIdx < 3; aIdx++) + { + float t = min[aIdx]-axismargin; + vertices.write(s*(aIdx*12+aIdx), &t, s); + t = max[aIdx]+axismargin; + vertices.write(s*(aIdx*12+aIdx+6), &t, s); + } + vertices.release(); +} +void Axis::draw(QMatrix4x4 transMat, QMatrix4x4 viewMat, + QMatrix4x4 orientMat, QMatrix4x4 aspectMat, float aspectRatio) +{ + shader.bind(); + vertices.bind(); + // Load the transform and view matrices into the shader + auto loadMatrixUniforms = [&](QMatrix4x4 transform, QMatrix4x4 view) + { + glUniformMatrix4fv( + shader.uniformLocation("transform_matrix"), + 1, GL_FALSE, transform.data()); + glUniformMatrix4fv( + shader.uniformLocation("view_matrix"), + 1, GL_FALSE, view.data()); + }; + const GLuint vp = shader.attributeLocation("vertex_position"); + const GLuint vc = shader.attributeLocation("vertex_color"); + glEnableVertexAttribArray(vp); + glEnableVertexAttribArray(vc); + auto loadAttribPtr = [&]() + { + glVertexAttribPointer(vp, 3, GL_FLOAT, false, + 6 * sizeof(GLfloat), 0); + glVertexAttribPointer(vc, 3, GL_FLOAT, false, + 6 * sizeof(GLfloat), + (GLvoid*)(3 * sizeof(GLfloat))); + }; + loadMatrixUniforms(transMat, viewMat); + loadAttribPtr(); + + glDrawArrays(GL_LINES, 0, 3*6); + + vertices.release(); + //Next, we draw the hud axis-flower + flowerAxisVertices.bind(); + glClear(GL_DEPTH_BUFFER_BIT);//Ensure hud draws over everything + const float hudSize = 0.2; + QMatrix4x4 hudMat; + //Move the hud to the bottom left corner with margin + if (aspectRatio > 1.0) + { + hudMat.translate(aspectRatio-2*hudSize, -1.0+2*hudSize, 0); + } + else + { + hudMat.translate(1.0-2*hudSize, -1.0/aspectRatio+2*hudSize, 0); + } + //Scale the hud to be small + hudMat.scale(hudSize, hudSize, 1); + loadMatrixUniforms(orientMat, aspectMat*hudMat); + loadAttribPtr(); + glDrawArrays(GL_LINES, 0, 3*6); + flowerAxisVertices.release(); + for(int aIdx = 0; aIdx < 3; aIdx++){ + QVector3D transVec = QVector3D(); + transVec[aIdx] = 1.25;//This is how far we want the letters to be extended out + QOpenGLBuffer b = flowerLabelVertices[aIdx]; + //The only transform we want is to translate the letters to the ends of the axis lines + QMatrix4x4 labelTransMat = QMatrix4x4(); + labelTransMat.translate(orientMat * transVec); + b.bind(); + loadMatrixUniforms(labelTransMat, aspectMat * hudMat); + loadAttribPtr(); + glDrawArrays(GL_LINES, 0, axisSegCount[aIdx]*2*6); + b.release(); + } + shader.release(); +} diff --git a/src/axis.h b/src/axis.h new file mode 100644 index 0000000..7d0a991 --- /dev/null +++ b/src/axis.h @@ -0,0 +1,22 @@ +#ifndef AXIS_H +#define AXIS_H + +#include +#include +#include + +class Axis : protected QOpenGLFunctions +{ +public: + Axis(); + void setScale(QVector3D min, QVector3D max); + void draw(QMatrix4x4 transMat, QMatrix4x4 viewMat, + QMatrix4x4 orientMat, QMatrix4x4 aspectMat, float aspectRatio); +private: + QOpenGLShaderProgram shader; + QOpenGLBuffer vertices, //GL Buffer for model-space coords + flowerAxisVertices; //GL Buffer for hud-space axis lines + QOpenGLBuffer flowerLabelVertices[3];//Buffer for hud-space label lines +}; + +#endif // AXIS_H diff --git a/src/backdrop.cpp b/src/backdrop.cpp index 13d0aa3..4ef1bba 100644 --- a/src/backdrop.cpp +++ b/src/backdrop.cpp @@ -2,10 +2,10 @@ 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[] = { diff --git a/src/backdrop.h b/src/backdrop.h index a2398c8..95ae48b 100644 --- a/src/backdrop.h +++ b/src/backdrop.h @@ -1,18 +1,18 @@ #ifndef BACKDROP_H #define BACKDROP_H -#include -#include -#include +#include +#include +#include -class Backdrop : protected QGLFunctions +class Backdrop : protected QOpenGLFunctions { public: Backdrop(); void draw(); private: - QGLShaderProgram shader; - QGLBuffer vertices; + QOpenGLShaderProgram shader; + QOpenGLBuffer vertices; }; #endif // BACKDROP_H diff --git a/src/canvas.cpp b/src/canvas.cpp index dd645a5..4da41a7 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -1,41 +1,89 @@ #include -#include #include #include "canvas.h" #include "backdrop.h" +#include "axis.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(" ") +const float Canvas::P_PERSPECTIVE = 0.25f; +const float Canvas::P_ORTHOGRAPHIC = 0.0f; + +Canvas::Canvas(const QSurfaceFormat& format, QWidget *parent) + : QOpenGLWidget(parent), mesh(nullptr), + scale(1), zoom(1), tilt(90), yaw(0), + anim(this, "perspective"), status(" "), + meshInfo("") { - // Nothing to do here + setFormat(format); + QFile styleFile(":/qt/style.qss"); + styleFile.open( QFile::ReadOnly ); + setStyleSheet(styleFile.readAll()); + + anim.setDuration(100); } Canvas::~Canvas() { + makeCurrent(); delete mesh; + delete mesh_vertshader; + delete backdrop; + delete axis; + doneCurrent(); +} + +void Canvas::view_anim(float v) +{ + anim.setStartValue(perspective); + anim.setEndValue(v); + anim.start(); +} + +void Canvas::view_perspective(float p, bool animate){ + if(animate) + { + view_anim(p); + } + else + { + set_perspective(p); + } +} + +void Canvas::draw_axes(bool d) +{ + drawAxes = d; + update(); +} + +void Canvas::invert_zoom(bool d) +{ + invertZoom = d; + update(); } -void Canvas::load_mesh(Mesh* m) +void Canvas::load_mesh(Mesh* m, bool is_reload) { + delete mesh; 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; + QVector3D lower(m->xmin(), m->ymin(), m->zmin()); + QVector3D upper(m->xmax(), m->ymax(), m->zmax()); + if (!is_reload) + { + center = (lower + upper) / 2; + scale = 2 / (upper - lower).length(); + // Reset other camera parameters + zoom = 1; + yaw = 0; + tilt = 90; + } + meshInfo = QStringLiteral("Triangles: %1\nX: [%2, %3]\nY: [%4, %5]\nZ: [%6, %7]").arg(m->triCount()); + for(int dIdx = 0; dIdx < 3; dIdx++) meshInfo = meshInfo.arg(lower[dIdx]).arg(upper[dIdx]); + axis->setScale(lower, upper); update(); delete m; @@ -47,6 +95,18 @@ void Canvas::set_status(const QString &s) update(); } +void Canvas::set_perspective(float p) +{ + perspective = p; + update(); +} + +void Canvas::set_drawMode(enum DrawMode mode) +{ + drawMode = mode; + update(); +} + void Canvas::clear_status() { status = ""; @@ -55,72 +115,107 @@ void Canvas::clear_status() void Canvas::initializeGL() { - initializeGLFunctions(); + initializeOpenGLFunctions(); - mesh_shader.addShaderFromSourceFile(QGLShader::Vertex, ":/gl/mesh.vert"); - mesh_shader.addShaderFromSourceFile(QGLShader::Fragment, ":/gl/mesh.frag"); + mesh_vertshader = new QOpenGLShader(QOpenGLShader::Vertex); + mesh_vertshader->compileSourceFile(":/gl/mesh.vert"); + mesh_shader.addShader(mesh_vertshader); + mesh_shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/mesh.frag"); mesh_shader.link(); + mesh_wireframe_shader.addShader(mesh_vertshader); + mesh_wireframe_shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/mesh_wireframe.frag"); + mesh_wireframe_shader.link(); + mesh_surfaceangle_shader.addShader(mesh_vertshader); + mesh_surfaceangle_shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/gl/mesh_surfaceangle.frag"); + mesh_surfaceangle_shader.link(); backdrop = new Backdrop(); + axis = new Axis(); } -void Canvas::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); +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(); - - if (status.isNull()) return; + if (drawAxes) axis->draw(transform_matrix(), view_matrix(), + orient_matrix(), aspect_matrix(), width() / float(height())); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); - painter.drawText(10, height() - 10, status); + float textHeight = painter.fontInfo().pointSize(); + if (drawAxes) painter.drawText(QRect(10, textHeight, width(), height()), meshInfo); + painter.drawText(10, height() - textHeight, status); } - void Canvas::draw_mesh() { - mesh_shader.bind(); + QOpenGLShaderProgram* selected_mesh_shader = NULL; + if(drawMode == wireframe) + { + selected_mesh_shader = &mesh_wireframe_shader; + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + } + else + { + if(drawMode == shaded) + { + selected_mesh_shader = &mesh_shader; + } + else + { + selected_mesh_shader = &mesh_surfaceangle_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 +QMatrix4x4 Canvas::orient_matrix() const { QMatrix4x4 m; m.rotate(tilt, QVector3D(1, 0, 0)); m.rotate(yaw, QVector3D(0, 0, 1)); + //We want the x axis to the right, and the z axis up + m.scale(-1, 1, -1); + return m; +} +QMatrix4x4 Canvas::transform_matrix() const +{ + QMatrix4x4 m = orient_matrix(); m.scale(scale); m.translate(-center); return m; } - -QMatrix4x4 Canvas::view_matrix() const +QMatrix4x4 Canvas::aspect_matrix() const { QMatrix4x4 m; if (width() > height()) @@ -131,7 +226,13 @@ QMatrix4x4 Canvas::view_matrix() const { m.scale(-1, width() / float(height()), 0.5); } + return m; +} +QMatrix4x4 Canvas::view_matrix() const +{ + QMatrix4x4 m = aspect_matrix(); m.scale(zoom, zoom, 1); + m(3, 2) = perspective; return m; } @@ -159,10 +260,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) @@ -180,21 +282,27 @@ void Canvas::wheelEvent(QWheelEvent *event) { // Find GL position before the zoom operation // (to zoom about mouse cursor) - auto p = event->pos(); + auto p = event->position(); 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) + if (event->angleDelta().y() < 0) { - for (int i=0; i > event->delta(); --i) - zoom *= 1.001; + for (int i=0; i > event->angleDelta().y(); --i) + if (invertZoom) + zoom /= 1.001; + else + zoom *= 1.001; } - else if (event->delta() > 0) + else if (event->angleDelta().y() > 0) { - for (int i=0; i < event->delta(); ++i) - zoom /= 1.001; + for (int i=0; i < event->angleDelta().y(); ++i) + if (invertZoom) + zoom *= 1.001; + else + zoom /= 1.001; } // Then find the cursor's GL position post-zoom and adjust center. @@ -203,3 +311,8 @@ void Canvas::wheelEvent(QWheelEvent *event) center += b - a; update(); } + +void Canvas::resizeGL(int width, int height) +{ + glViewport(0, 0, width, height); +} diff --git a/src/canvas.h b/src/canvas.h index 33313cf..0fdff64 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -1,51 +1,67 @@ #ifndef CANVAS_H #define CANVAS_H -#include -#include -#include -#include -#include +#include +#include +#include class GLMesh; class Mesh; class Backdrop; +class Axis; -class Canvas : public QGLWidget, protected QGLFunctions +enum DrawMode {shaded, wireframe, surfaceangle, DRAWMODECOUNT}; + +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(); + const static float P_PERSPECTIVE; + const static float P_ORTHOGRAPHIC; + + void view_perspective(float p, bool animate); + void draw_axes(bool d); + void invert_zoom(bool d); + void set_drawMode(enum DrawMode mode); + 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: - void mousePressEvent(QMouseEvent* event); - void mouseReleaseEvent(QMouseEvent* event); - void mouseMoveEvent(QMouseEvent* event); - void wheelEvent(QWheelEvent* event); - + 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 view_anim(float v); private: void draw_mesh(); + QMatrix4x4 orient_matrix() const; QMatrix4x4 transform_matrix() const; + QMatrix4x4 aspect_matrix() const; QMatrix4x4 view_matrix() const; - QGLShaderProgram mesh_shader; - QGLShaderProgram quad_shader; + QOpenGLShader* mesh_vertshader; + QOpenGLShaderProgram mesh_shader; + QOpenGLShaderProgram mesh_wireframe_shader; + QOpenGLShaderProgram mesh_surfaceangle_shader; GLMesh* mesh; Backdrop* backdrop; + Axis* axis; QVector3D center; float scale; @@ -53,8 +69,16 @@ private: float tilt; float yaw; + float perspective; + enum DrawMode drawMode; + bool drawAxes; + bool invertZoom; + Q_PROPERTY(float perspective MEMBER perspective WRITE set_perspective); + QPropertyAnimation anim; + QPoint mouse_pos; QString status; + QString meshInfo; }; #endif // CANVAS_H diff --git a/src/glmesh.cpp b/src/glmesh.cpp index 7053809..863f558 100644 --- a/src/glmesh.cpp +++ b/src/glmesh.cpp @@ -2,15 +2,15 @@ #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(), diff --git a/src/glmesh.h b/src/glmesh.h index 74a193f..5c47c2d 100644 --- a/src/glmesh.h +++ b/src/glmesh.h @@ -1,19 +1,20 @@ #ifndef GLMESH_H #define GLMESH_H -#include -#include +#include +#include +// 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 diff --git a/src/loader.cpp b/src/loader.cpp index 0d6b165..38bd2dd 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -1,7 +1,10 @@ +#include + #include "loader.h" +#include "vertex.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,51 +14,137 @@ 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 +void parallel_sort(Vertex* begin, Vertex* end, int threads) +{ + if (threads < 2 || end - begin < 2) + { + std::sort(begin, end); + } + else + { + const auto mid = begin + (end - begin) / 2; + if (threads == 2) + { + auto future = std::async(parallel_sort, begin, mid, threads / 2); + std::sort(mid, end); + future.wait(); + } + else + { + auto a = std::async(std::launch::async, parallel_sort, begin, mid, threads / 2); + auto b = std::async(std::launch::async, parallel_sort, mid, end, threads / 2); + a.wait(); + b.wait(); + } + std::inplace_merge(begin, mid, end); + } +} + +Mesh* mesh_from_verts(uint32_t tri_count, QVector& verts) { - GLfloat x, y, z; - bool operator!=(const Vec3& rhs) const + // 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) { - return x != rhs.x || y != rhs.y || z != rhs.z; + verts[i].i = i; } - bool operator<(const Vec3& rhs) const + + // 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) { - 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; + threads = 8; } -}; -typedef std::pair Vec3i; + // Sort the set of vertices (to deduplicate) + parallel_sort(verts.begin(), verts.end(), threads); + + // This vector will store triangles as sets of 3 indices + std::vector indices(tri_count*3); + + // Go through the sorted vertex list, deduplicating and creating + // an indexed geometry representation for the triangles. + // Unique vertices are moved so that they occupy the first vertex_count + // positions in the verts array. + size_t vertex_count = 0; + for (auto v : verts) + { + if (!vertex_count || v != verts[vertex_count-1]) + { + verts[vertex_count++] = v; + } + indices[v.i] = vertex_count - 1; + } + verts.resize(vertex_count); + + std::vector flat_verts; + flat_verts.reserve(vertex_count*3); + for (auto v : verts) + { + flat_verts.push_back(v.x); + flat_verts.push_back(v.y); + flat_verts.push_back(v.z); + } + + return new Mesh(std::move(flat_verts), std::move(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(5) == "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); + } + // Otherwise, this STL is a binary stl but contains 'solid' as + // the first five characters. This is a bad life choice, but + // we can gracefully handle it by falling through to the binary + // STL reader below. + } + + 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; @@ -67,63 +156,81 @@ Mesh* Loader::load_stl() } // Extract vertices into an array of xyz, unsigned pairs - QVector verts(tri_count*3); + QVector verts(tri_count*3); // Dummy array, because readRawData is faster than skipRawData - char buffer[sizeof(float)*3]; + std::unique_ptr 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.get() + 3 * sizeof(float); 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)); - } + for (unsigned i=0; i < 3; ++i) + { + qFromLittleEndian(b, 3, &v[i]); + b += 3 * sizeof(float); + } - // 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; + // Skip face attribute and next face's normal vector + b += 3 * sizeof(float) + sizeof(uint16_t); } - // Sort the set of vertices (to deduplicate) - std::sort(verts.begin(), verts.end()); + return mesh_from_verts(tri_count, verts); +} - // This vector will store triangles as sets of 3 indices - std::vector indices(tri_count*3); +Mesh* Loader::read_stl_ascii(QFile& file) +{ + file.readLine(); + uint32_t tri_count = 0; + QVector verts(tri_count*3); - // Go through the sorted vertex list, deduplicating and creating - // an indexed geometry representation for the triangles. - // Unique vertices are moved so that they occupy the first vertex_count - // positions in the verts array. - size_t vertex_count = 0; - for (auto v : verts) + bool okay = true; + while (!file.atEnd() && okay) { - if (!vertex_count || v.first != verts[vertex_count-1].first) + const auto line = file.readLine().simplified(); + if (line.startsWith("endsolid")) { - verts[vertex_count++] = v; + break; + } + 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(Vertex(x, y, z)); } - indices[v.second] = vertex_count - 1; + if (!file.readLine().trimmed().startsWith("endloop") || + !file.readLine().trimmed().startsWith("endfacet")) + { + okay = false; + break; + } + tri_count++; } - verts.resize(vertex_count); - std::vector flat_verts; - flat_verts.reserve(vertex_count*3); - for (auto v : verts) + if (okay) { - flat_verts.push_back(v.first.x); - flat_verts.push_back(v.first.y); - flat_verts.push_back(v.first.z); + return mesh_from_verts(tri_count, verts); + } + else + { + emit error_bad_stl(); + return NULL; } - - return new Mesh(flat_verts, indices); } diff --git a/src/loader.h b/src/loader.h index fb0c8d8..6f5942f 100644 --- a/src/loader.h +++ b/src/loader.h @@ -9,22 +9,28 @@ 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 error_missing_file(); private: const QString filename; - + bool is_reload; }; #endif // LOADER_H diff --git a/src/main.cpp b/src/main.cpp index 4b76222..4ec8722 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,10 @@ int main(int argc, char *argv[]) { + QCoreApplication::setOrganizationName("fstl-app"); + QCoreApplication::setOrganizationDomain("https://github.com/fstl-app/fstl"); + QCoreApplication::setApplicationName("fstl"); + QCoreApplication::setApplicationVersion(FSTL_VERSION); App a(argc, argv); return a.exec(); } diff --git a/src/mesh.cpp b/src/mesh.cpp index cfb4ca4..2c44a2c 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -8,14 +8,18 @@ //////////////////////////////////////////////////////////////////////////////// -Mesh::Mesh(std::vector v, std::vector i) - : vertices(v), indices(i) +Mesh::Mesh(std::vector&& v, std::vector&& i) + : vertices(std::move(v)), indices(std::move(i)) { // Nothing to do here } 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,12 @@ float Mesh::max(size_t start) const } return v; } + +int Mesh::triCount() const +{ + return indices.size()/3; +} +bool Mesh::empty() const +{ + return vertices.size() == 0; +} diff --git a/src/mesh.h b/src/mesh.h index e8a02f0..9e2ab0c 100644 --- a/src/mesh.h +++ b/src/mesh.h @@ -9,7 +9,7 @@ class Mesh { public: - Mesh(std::vector vertices, std::vector indices); + Mesh(std::vector&& vertices, std::vector&& indices); float min(size_t start) const; float max(size_t start) const; @@ -21,6 +21,9 @@ public: float ymax() const { return max(1); } float zmax() const { return max(2); } + int triCount() const; + bool empty() const; + private: std::vector vertices; std::vector indices; diff --git a/src/vertex.h b/src/vertex.h new file mode 100644 index 0000000..9738a75 --- /dev/null +++ b/src/vertex.h @@ -0,0 +1,30 @@ +#ifndef VEC3_H +#define VEC3_H + +#include + +/* + * 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 diff --git a/src/window.cpp b/src/window.cpp index 3002ecc..e9f3f94 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -1,58 +1,188 @@ #include -#include -#include #include "window.h" #include "canvas.h" #include "loader.h" +const QString Window::RECENT_FILE_KEY = "recentFiles"; +const QString Window::INVERT_ZOOM_KEY = "invertZoom"; +const QString Window::AUTORELOAD_KEY = "autoreload"; +const QString Window::DRAW_AXES_KEY = "drawAxes"; +const QString Window::PROJECTION_KEY = "projection"; +const QString Window::DRAW_MODE_KEY = "drawMode"; +const QString Window::WINDOW_GEOM_KEY = "windowGeometry"; + 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)), + orthographic_action(new QAction("Orthographic", this)), + shaded_action(new QAction("Shaded", this)), + wireframe_action(new QAction("Wireframe", this)), + surfaceangle_action(new QAction("Surface Angle", this)), + axes_action(new QAction("Draw Axes", this)), + invert_zoom_action(new QAction("Invert Zoom", this)), + reload_action(new QAction("Reload", this)), + autoreload_action(new QAction("Autoreload", this)), + save_screenshot_action(new QAction("Save Screenshot", this)), + hide_menuBar_action(new QAction("Hide Menu Bar", 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; + 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(watcher, &QFileSystemWatcher::fileChanged, + this, &Window::on_watched_change); + open_action->setShortcut(QKeySequence::Open); QObject::connect(open_action, &QAction::triggered, this, &Window::on_open); + this->addAction(open_action); quit_action->setShortcut(QKeySequence::Quit); QObject::connect(quit_action, &QAction::triggered, this, &Window::close); + this->addAction(quit_action); + + autoreload_action->setCheckable(true); + 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); + + 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->addAction(open_action); + file_menu->addMenu(recent_files); + 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"); + auto projection_menu = view_menu->addMenu("Projection"); + projection_menu->addAction(perspective_action); + projection_menu->addAction(orthographic_action); + auto projections = new QActionGroup(projection_menu); + for (auto p : {perspective_action, orthographic_action}) + { + projections->addAction(p); + p->setCheckable(true); + } + projections->setExclusive(true); + 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); + draw_menu->addAction(surfaceangle_action); + auto drawModes = new QActionGroup(draw_menu); + for (auto p : {shaded_action, wireframe_action, surfaceangle_action}) + { + drawModes->addAction(p); + p->setCheckable(true); + } + drawModes->setExclusive(true); + QObject::connect(drawModes, &QActionGroup::triggered, + this, &Window::on_drawMode); + view_menu->addAction(axes_action); + axes_action->setCheckable(true); + QObject::connect(axes_action, &QAction::triggered, + this, &Window::on_drawAxes); + + view_menu->addAction(invert_zoom_action); + invert_zoom_action->setCheckable(true); + QObject::connect(invert_zoom_action, &QAction::triggered, + this, &Window::on_invertZoom); + + view_menu->addAction(hide_menuBar_action); + hide_menuBar_action->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C); + hide_menuBar_action->setCheckable(true); + QObject::connect(hide_menuBar_action, &QAction::toggled, + this, &Window::on_hide_menuBar); + this->addAction(hide_menuBar_action); + auto help_menu = menuBar()->addMenu("Help"); help_menu->addAction(about_action); + load_persist_settings(); +} + +void Window::load_persist_settings(){ + QSettings settings; + bool invert_zoom = settings.value(INVERT_ZOOM_KEY, false).toBool(); + canvas->invert_zoom(invert_zoom); + invert_zoom_action->setChecked(invert_zoom); + + autoreload_action->setChecked(settings.value(AUTORELOAD_KEY, true).toBool()); + + bool draw_axes = settings.value(DRAW_AXES_KEY, false).toBool(); + canvas->draw_axes(draw_axes); + axes_action->setChecked(draw_axes); + + QString projection = settings.value(PROJECTION_KEY, "perspective").toString(); + if(projection == "perspective"){ + canvas->view_perspective(Canvas::P_PERSPECTIVE, false); + perspective_action->setChecked(true); + }else{ + canvas->view_perspective(Canvas::P_ORTHOGRAPHIC, false); + orthographic_action->setChecked(true); + } + + DrawMode draw_mode = (DrawMode)settings.value(DRAW_MODE_KEY, DRAWMODECOUNT).toInt(); + + if(draw_mode >= DRAWMODECOUNT) + { + draw_mode = shaded; + } + canvas->set_drawMode(draw_mode); + QAction* (dm_acts[]) = {shaded_action, wireframe_action, surfaceangle_action}; + dm_acts[draw_mode]->setChecked(true); + resize(600, 400); + restoreGeometry(settings.value(WINDOW_GEOM_KEY).toByteArray()); } void Window::on_open() { QString filename = QFileDialog::getOpenFileName( - this, "Load .stl file", QString(), "*.stl"); - if (not filename.isNull()) + this, "Load .stl file", QString(), "STL files (*.stl, *.STL)"); + if (!filename.isNull()) { load_stl(filename); } @@ -61,29 +191,35 @@ void Window::on_open() void Window::on_about() { QMessageBox::about(this, "", - "

fstl

" + "

fstl
" FSTL_VERSION "

" "

A fast viewer for .stl files.
" - "https://github.com/mkeeter/fstl

" - "

© 2014 Matthew Keeter
" + "https://github.com/fstl-app/fstl

" + "

© 2014-2022 Matthew Keeter
" "matt.j.keeter@gmail.com

"); } -void Window::on_ascii_stl() +void Window::on_bad_stl() { QMessageBox::critical(this, "Error", "Error:
" - "Cannot open ASCII .stl file
" - "Please convert to binary .stl and retry"); + "This .stl file is invalid or corrupted.
" + "Please export it from the original source, verify, and retry."); } -void Window::on_bad_stl() +void Window::on_empty_mesh() { QMessageBox::critical(this, "Error", "Error:
" - "This .stl file is invalid or corrupted.
" - "Please export it from the original source, verify, and retry."); + "This file is syntactically correct
but contains no triangles."); +} + +void Window::on_missing_file() +{ + QMessageBox::critical(this, "Error", + "Error:
" + "The target file is missing.
"); } void Window::enable_open() @@ -96,22 +232,202 @@ 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(Canvas::P_PERSPECTIVE, true); + QSettings().setValue(PROJECTION_KEY, "perspective"); + } + else + { + canvas->view_perspective(Canvas::P_ORTHOGRAPHIC, true); + QSettings().setValue(PROJECTION_KEY, "orthographic"); + } +} + +void Window::on_drawMode(QAction* act) +{ + DrawMode mode; + if (act == shaded_action) + { + mode = shaded; + } + else if (act == wireframe_action) + { + mode = wireframe; + } + else + { + mode = surfaceangle; + } + canvas->set_drawMode(mode); + QSettings().setValue(DRAW_MODE_KEY, mode); +} + +void Window::on_drawAxes(bool d) +{ + canvas->draw_axes(d); + QSettings().setValue(DRAW_AXES_KEY, d); +} + +void Window::on_invertZoom(bool d) +{ + canvas->invert_zoom(d); + QSettings().setValue(INVERT_ZOOM_KEY, d); +} + +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(); + } + QSettings().setValue(AUTORELOAD_KEY, b); +} + +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::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::on_hide_menuBar() +{ + menuBar()->setVisible(!hide_menuBar_action->isChecked()); +} + +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::error_missing_file, + this, &Window::on_missing_file); connect(loader, &Loader::finished, loader, &Loader::deleteLater); @@ -124,6 +440,11 @@ bool Window::load_stl(const QString& filename) { connect(loader, &Loader::loaded_file, this, &Window::setWindowTitle); + connect(loader, &Loader::loaded_file, + this, &Window::set_watched); + connect(loader, &Loader::loaded_file, + this, &Window::on_loaded); + reload_action->setEnabled(true); } loader->start(); @@ -144,3 +465,148 @@ void Window::dropEvent(QDropEvent *event) { load_stl(event->mimeData()->urls().front().toLocalFile()); } + +void Window::resizeEvent(QResizeEvent *event) +{ + QSettings().setValue(WINDOW_GEOM_KEY, saveGeometry()); + QWidget::resizeEvent(event); +} + +void Window::moveEvent(QMoveEvent *event) +{ + QSettings().setValue(WINDOW_GEOM_KEY, saveGeometry()); + QWidget::moveEvent(event); +} + +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 Window::get_file_neighbors() +{ + if (current_file.isEmpty()) { + return QPair(QString(), QString()); + } + + build_folder_file_list(); + + QFileInfo fileInfo(current_file); + + QString current_dir = fileInfo.absoluteDir().absolutePath(); + QString current_name = fileInfo.fileName(); + + QString prev = QString(); + QString next = QString(); + + QListIterator 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(prev, next); +} + +bool Window::load_prev(void) +{ + QPair neighbors = get_file_neighbors(); + if (neighbors.first.isEmpty()) { + return false; + } + + return load_stl(neighbors.first); +} + +bool Window::load_next(void) +{ + QPair 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; + } + else if (event->key() == Qt::Key_Escape) + { + hide_menuBar_action->setChecked(false); + return; + } + + QMainWindow::keyPressEvent(event); +} diff --git a/src/window.h b/src/window.h index cb92ebb..81cd29f 100644 --- a/src/window.h +++ b/src/window.h @@ -2,6 +2,9 @@ #define WINDOW_H #include +#include +#include +#include class Canvas; @@ -10,25 +13,82 @@ 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); + 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 resizeEvent(QResizeEvent *event) override; + void moveEvent(QMoveEvent *event) override; + void keyPressEvent(QKeyEvent* event) override; 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 enable_open(); void disable_open(); + void set_watched(const QString& filename); + +private slots: + void on_projection(QAction* proj); + void on_drawMode(QAction* mode); + void on_drawAxes(bool d); + void on_invertZoom(bool d); + 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(); + void on_hide_menuBar(); + private: + void rebuild_recent_files(); + void load_persist_settings(); + void sorted_insert(QStringList& list, const QCollator& collator, const QString& value); + void build_folder_file_list(); + QPair get_file_neighbors(); + QAction* const open_action; QAction* const about_action; QAction* const quit_action; + QAction* const perspective_action; + QAction* const orthographic_action; + QAction* const shaded_action; + QAction* const wireframe_action; + QAction* const surfaceangle_action; + QAction* const axes_action; + QAction* const invert_zoom_action; + QAction* const reload_action; + QAction* const autoreload_action; + QAction* const save_screenshot_action; + QAction* const hide_menuBar_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; + const static QString INVERT_ZOOM_KEY; + const static QString AUTORELOAD_KEY; + const static QString DRAW_AXES_KEY; + const static QString PROJECTION_KEY; + const static QString DRAW_MODE_KEY; + const static QString WINDOW_GEOM_KEY; + + QString current_file; + QString lookup_folder; + QStringList lookup_folder_files; + + QFileSystemWatcher* watcher; Canvas* canvas; };