]> git.sur5r.net Git - fstl/commitdiff
New upstream version 0.10.0 upstream
authorJakob Haufe <sur5r@debian.org>
Wed, 29 Jun 2022 14:02:12 +0000 (16:02 +0200)
committerJakob Haufe <sur5r@debian.org>
Wed, 29 Jun 2022 14:02:12 +0000 (16:02 +0200)
29 files changed:
CMakeLists.txt [new file with mode: 0644]
README.md
app/package.sh
exe/package.sh
gl/colored_lines.frag [new file with mode: 0644]
gl/colored_lines.vert [new file with mode: 0644]
gl/gl.qrc
gl/mesh_surfaceangle.frag [new file with mode: 0644]
gl/mesh_wireframe.frag [new file with mode: 0644]
qt/fstl.pro [deleted file]
qt/style.qss
src/app.cpp
src/app.h
src/axis.cpp [new file with mode: 0644]
src/axis.h [new file with mode: 0644]
src/backdrop.cpp
src/backdrop.h
src/canvas.cpp
src/canvas.h
src/glmesh.cpp
src/glmesh.h
src/loader.cpp
src/loader.h
src/main.cpp
src/mesh.cpp
src/mesh.h
src/vertex.h [new file with mode: 0644]
src/window.cpp
src/window.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..0628afa
--- /dev/null
@@ -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
+            $<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/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)
index a9a845ffaed12717b7cf290eec5255968bc72226..c58eeae8f676ff86cb557d9048fa1644352f8b77 100644 (file)
--- a/README.md
+++ b/README.md
-`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.
index d51c470818b0589e07721a0663d78d678be8c522..6cf065beba3ec8adf5c497a42571fc52a76c2a28 100755 (executable)
@@ -1,14 +1,53 @@
 #!/bin/sh
+set -x -e
+
 cd ../build
-macdeployqt fstl.app
+make clean
+rm -rf fstl.app
+make -j8
+
+APP=fstl
+
+# Pull out framework paths info with otool
+MACDEPLOYQT=`otool -L $APP.app/Contents/MacOS/fstl | sed -n -e "s:\(.*\)lib/QtCore.*:\1/bin/macdeployqt:gp"`
+
+$MACDEPLOYQT $APP.app
+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 ..
index ef81b620474638f3043fc3bed41418a3ff2abc2b..dfc411569f91f919af670a97df7f20508119a243 100644 (file)
@@ -1,3 +1,13 @@
-cd ..
-cp build/release/fstl.exe .
-/c/Program\ Files/7-Zip/7z.exe a fstl_win.zip fstl.exe README.md
+#!/bin/sh
+set -e -x
+
+cd ../build
+make clean
+rm -rf fstl.exe
+make -j8
+
+cp ../README.md README.txt
+cp release/fstl.exe .
+/c/Program\ Files/7-Zip/7z.exe a fstl.zip fstl.exe README.txt
+cp fstl.zip ..
+rm README.txt fstl.exe
diff --git a/gl/colored_lines.frag b/gl/colored_lines.frag
new file mode 100644 (file)
index 0000000..1c02e1b
--- /dev/null
@@ -0,0 +1,7 @@
+#version 120
+
+varying vec3 frag_color;
+
+void main() {
+    gl_FragColor = vec4(frag_color, 1.0);
+}
diff --git a/gl/colored_lines.vert b/gl/colored_lines.vert
new file mode 100644 (file)
index 0000000..2100403
--- /dev/null
@@ -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;
+}
index ef93a0e4895e6d87a73ed11ef709646cbbb65d74..6e8baa90cd9b64fa52a89c0f0eae0a482915767c 100644 (file)
--- a/gl/gl.qrc
+++ b/gl/gl.qrc
@@ -2,8 +2,12 @@
     <qresource prefix="gl/">
         <file>mesh.frag</file>
         <file>mesh.vert</file>
+        <file>mesh_wireframe.frag</file>
+        <file>mesh_surfaceangle.frag</file>
         <file>quad.frag</file>
         <file>quad.vert</file>
+        <file>colored_lines.frag</file>
+        <file>colored_lines.vert</file>
         <file>sphere.stl</file>
     </qresource>
 </RCC>
diff --git a/gl/mesh_surfaceangle.frag b/gl/mesh_surfaceangle.frag
new file mode 100644 (file)
index 0000000..3ee94dd
--- /dev/null
@@ -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 (file)
index 0000000..13f001c
--- /dev/null
@@ -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 (file)
index 7a0f449..0000000
+++ /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
-}
index 02971482c0585beccbf00c2187f3675e1ce3585f..3f0ca9b0dccb9c8fa23ddd32619c15e9107fea55 100644 (file)
@@ -1,19 +1,4 @@
 QWidget {
-    background-color: #fdf6e3;
+    background-color: null;
     color: #839496;
 }
-
-QPushButton {
-    background-color: #eee8d5;
-    border-top-width: 5px;
-    border-bottom-width: 5px;
-    border-left-width: 30px;
-    border-right-width: 30px;
-    border-style: flat;
-    margin: 0px;
-    color: #839496;
-}
-
-QPushButton:pressed {
-    background-color: #ddd7c4;
-}
index b6eb3af88c300e9925e5408105d7b3ebb0461c06..a76a7dafdc9d11f25bc60d5fc4178d827ec11882 100644 (file)
@@ -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)
index 7a7672e7acd384de5a21966a1e7a9756dd16867e..6afb4d702e1b3d327e7e2b4df509bbb0adb01c50 100644 (file)
--- 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 (file)
index 0000000..831f592
--- /dev/null
@@ -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 <algorithm> 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 (file)
index 0000000..7d0a991
--- /dev/null
@@ -0,0 +1,22 @@
+#ifndef AXIS_H
+#define AXIS_H
+
+#include <QOpenGLBuffer>
+#include <QOpenGLShaderProgram>
+#include <QOpenGLFunctions>
+
+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
index 13d0aa39ae7e22b54978a79eaf4d661830d06ce1..4ef1bba7c8d455208297a8afb2641be954ad32fd 100644 (file)
@@ -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[] = {
index a2398c82ac546096997edd113a86b0601623c780..95ae48bd59d7e5b4294419812f30e7dcec08ba63 100644 (file)
@@ -1,18 +1,18 @@
 #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
index dd645a538838f790a1ea61e75ffbe67f3762187b..4da41a7e0cc17c8e049182fdb2c287205c2a7c9d 100644 (file)
@@ -1,41 +1,89 @@
 #include <QMouseEvent>
-#include <QDebug>
 
 #include <cmath>
 
 #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);
+}
index 33313cfcbf894c47aae09119c20398442fbe5369..0fdff64cb501cc98c88e44294edc150823ced4b4 100644 (file)
@@ -1,51 +1,67 @@
 #ifndef CANVAS_H
 #define CANVAS_H
 
-#include <QWidget>
-#include <QtOpenGL/QGLWidget>
-#include <QtOpenGL/QGLFunctions>
-#include <QtOpenGL/QGLShaderProgram>
-#include <QMatrix4x4>
+#include <QtOpenGL>
+#include <QSurfaceFormat>
+#include <QOpenGLShaderProgram>
 
 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
index 7053809e178f652c6500bb665e3b747853da96bb..863f558079ca3829f29014708eb0799af4cd448b 100644 (file)
@@ -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(),
index 74a193f93dce7f5fede53c3dd91ef0885661b6a6..5c47c2ddd921aed3eb05b98e8f22863a702f6a93 100644 (file)
@@ -1,19 +1,20 @@
 #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
index 0d6b165ad696d436324a2961d71d6788c6c67c51..38bd2ddc66796ba7498eb3ece007e445d2f564ce 100644 (file)
@@ -1,7 +1,10 @@
+#include <future>
+
 #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<Vertex>& 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<Vec3, GLuint> 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<GLuint> indices(tri_count*3);
+
+    // Go through the sorted vertex list, deduplicating and creating
+    // an indexed geometry representation for the triangles.
+    // Unique vertices are moved so that they occupy the first vertex_count
+    // positions in the verts array.
+    size_t vertex_count = 0;
+    for (auto v : verts)
+    {
+        if (!vertex_count || v != verts[vertex_count-1])
+        {
+            verts[vertex_count++] = v;
+        }
+        indices[v.i] = vertex_count - 1;
+    }
+    verts.resize(vertex_count);
+
+    std::vector<GLfloat> flat_verts;
+    flat_verts.reserve(vertex_count*3);
+    for (auto v : verts)
+    {
+        flat_verts.push_back(v.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<Vec3i> verts(tri_count*3);
+    QVector<Vertex> verts(tri_count*3);
 
     // Dummy array, because readRawData is faster than skipRawData
-    char buffer[sizeof(float)*3];
+    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.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<float>(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<GLuint> indices(tri_count*3);
+Mesh* Loader::read_stl_ascii(QFile& file)
+{
+    file.readLine();
+    uint32_t tri_count = 0;
+    QVector<Vertex> 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<GLfloat> flat_verts;
-    flat_verts.reserve(vertex_count*3);
-    for (auto v : verts)
+    if (okay)
     {
-        flat_verts.push_back(v.first.x);
-        flat_verts.push_back(v.first.y);
-        flat_verts.push_back(v.first.z);
+        return mesh_from_verts(tri_count, verts);
+    }
+    else
+    {
+        emit error_bad_stl();
+        return NULL;
     }
-
-    return new Mesh(flat_verts, indices);
 }
 
index fb0c8d81eb10bf8a610fb38a4345dfa2f65ce6ea..6f5942f0bf9733322208f14e01401ff7755580dd 100644 (file)
@@ -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
index 4b76222110f3f3fa97c1793b78626e2f6db32537..4ec872275b0eef795c7c68c6751392babe3be565 100644 (file)
@@ -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();
 }
index cfb4ca464f5be66adb121ac7dd26adf795d061c2..2c44a2c5e19fc4c70f40bf3a3ba59637a53fbef4 100644 (file)
@@ -8,14 +8,18 @@
 
 ////////////////////////////////////////////////////////////////////////////////
 
-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
 }
 
 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;
+}
index e8a02f09a2c369b0788b90f7ca0ad97fd7166b18..9e2ab0c96c3ce3f8231b63f52964233a11ff86c5 100644 (file)
@@ -9,7 +9,7 @@
 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;
@@ -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<GLfloat> vertices;
     std::vector<GLuint> indices;
diff --git a/src/vertex.h b/src/vertex.h
new file mode 100644 (file)
index 0000000..9738a75
--- /dev/null
@@ -0,0 +1,30 @@
+#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
index 3002ecc83daa6008dd3c42c420b933c24605008e..e9f3f94ebf2d980e1e088d876d8acf53ae89c699 100644 (file)
 #include <QMenuBar>
-#include <QMessageBox>
-#include <QFileDialog>
 
 #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, "",
-        "<p align=\"center\"><b>fstl</b></p>"
+        "<p align=\"center\"><b>fstl</b><br>" FSTL_VERSION "</p>"
         "<p>A fast viewer for <code>.stl</code> files.<br>"
-        "<a href=\"https://github.com/mkeeter/fstl\""
-        "   style=\"color: #93a1a1;\">https://github.com/mkeeter/fstl</a></p>"
-        "<p>© 2014 Matthew Keeter<br>"
+        "<a href=\"https://github.com/fstl-app/fstl\""
+        "   style=\"color: #93a1a1;\">https://github.com/fstl-app/fstl</a></p>"
+        "<p>© 2014-2022 Matthew Keeter<br>"
         "<a href=\"mailto:matt.j.keeter@gmail.com\""
         "   style=\"color: #93a1a1;\">matt.j.keeter@gmail.com</a></p>");
 }
 
-void Window::on_ascii_stl()
+void Window::on_bad_stl()
 {
     QMessageBox::critical(this, "Error",
                           "<b>Error:</b><br>"
-                          "Cannot open ASCII <code>.stl</code> file<br>"
-                          "Please convert to binary <code>.stl</code> and retry");
+                          "This <code>.stl</code> file is invalid or corrupted.<br>"
+                          "Please export it from the original source, verify, and retry.");
 }
 
-void Window::on_bad_stl()
+void Window::on_empty_mesh()
 {
     QMessageBox::critical(this, "Error",
                           "<b>Error:</b><br>"
-                          "This <code>.stl</code> file is invalid or corrupted.<br>"
-                          "Please export it from the original source, verify, and retry.");
+                          "This file is syntactically correct<br>but contains no triangles.");
+}
+
+void Window::on_missing_file()
+{
+    QMessageBox::critical(this, "Error",
+                          "<b>Error:</b><br>"
+                          "The target file is missing.<br>");
 }
 
 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<QString, QString> Window::get_file_neighbors()
+{
+    if (current_file.isEmpty()) {
+        return QPair<QString, QString>(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<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;
+    }
+    else if (event->key() == Qt::Key_Escape)
+    {
+        hide_menuBar_action->setChecked(false);
+        return;
+    }
+
+    QMainWindow::keyPressEvent(event);
+}
index cb92ebb4708d2d68c52b3a1af82915c2b31c3940..81cd29f5f6a42b15f91539bc3048f448c0d49c38 100644 (file)
@@ -2,6 +2,9 @@
 #define WINDOW_H
 
 #include <QMainWindow>
+#include <QActionGroup>
+#include <QFileSystemWatcher>
+#include <QCollator>
 
 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<QString, QString> 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;
 };