From 38fb0c022a13f6a1f5c83c65d8f41d58d4e9df94 Mon Sep 17 00:00:00 2001 From: Jeremy Hu Date: Tue, 10 Apr 2018 15:59:20 +0800 Subject: [PATCH] Add main menu. - Add about window, - Add acknowlegements window, - Add multiple documents support. --- ACKNOWLEDGEMENTS.html | 402 ++++++++++++++++++++++++++++++ README.md | 17 -- dust3d.pro | 18 ++ dust3d_jezzasoft.xcf | Bin 4104 -> 0 bytes resources.qrc | 3 +- resources/dust3d_jezzasoft.png | Bin 2087 -> 0 bytes resources/dust3d_vertical.png | Bin 0 -> 1567 bytes src/aboutwidget.cpp | 18 ++ src/aboutwidget.h | 12 + src/logbrowser.cpp | 18 +- src/logbrowser.h | 5 +- src/logbrowserdialog.cpp | 13 +- src/main.cpp | 17 +- src/modelwidget.cpp | 15 ++ src/modelwidget.h | 6 + src/skeletondocument.cpp | 22 +- src/skeletondocument.h | 2 + src/skeletondocumentwindow.cpp | 440 +++++++++++++++++++++++++++++++-- src/skeletondocumentwindow.h | 62 +++++ src/skeletongraphicswidget.cpp | 78 +++++- src/skeletongraphicswidget.h | 7 + src/skeletonsnapshot.cpp | 280 --------------------- src/skeletonsnapshot.h | 19 -- 23 files changed, 1074 insertions(+), 380 deletions(-) create mode 100644 ACKNOWLEDGEMENTS.html delete mode 100644 dust3d_jezzasoft.xcf delete mode 100644 resources/dust3d_jezzasoft.png create mode 100644 resources/dust3d_vertical.png create mode 100644 src/aboutwidget.cpp create mode 100644 src/aboutwidget.h diff --git a/ACKNOWLEDGEMENTS.html b/ACKNOWLEDGEMENTS.html new file mode 100644 index 00000000..ae461b54 --- /dev/null +++ b/ACKNOWLEDGEMENTS.html @@ -0,0 +1,402 @@ +

Acknowledgements

+

+ Portions of this Software(Dust3D) may utilize the following copyrighted material, the use of which is hereby acknowledged. +

+ +

QtAwesome

+
+    MIT License
+    ===========
+    
+    Copyright 2013-2015 [Reliable Bits Software by Blommers IT](http://blommersit.nl). All Rights Reserved.
+    Author [Rick Blommers](mailto:rick@blommersit.nl)
+    
+    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.
+    
+    
+    Font Awesome License 
+    ====================
+    
+    [https://github.com/FortAwesome/Font-Awesome](https://github.com/FortAwesome/Font-Awesome)
+    
+    The Font Awesome font is licensed under the SIL Open Font License - [http://scripts.sil.org/OFL](http://scripts.sil.org/OFL)
+    The Font Awesome pictograms are licensed under the CC BY 3.0 License - [http://creativecommons.org/licenses/by/3.0/](http://creativecommons.org/licenses/by/3.0/)
+    "Font Awesome by Dave Gandy - http://fortawesome.github.com/Font-Awesome"
+
+ +

CGAL

+
+    LICENSE
+    ----------------------------------------------------------------------------
+    
+    The CGAL software consists of several parts, each of which is licensed under
+    an open source license. It is also possible to obtain commercial licenses
+    from GeometryFactory (www.geometryfactory.com) for all or parts of CGAL.
+    
+    The source code of the CGAL library can be found in the directories
+    "src/CGAL", "src/CGALQt", "src/CGALQt5" and "include/CGAL" (with the
+    exception of "include/CGAL/CORE", "include/CGAL/OpenNL").
+    It is specified in each file of the CGAL library which
+    license applies to it. This is either the GNU General Public License
+    or the GNU Lesser General Public License (as published by the Free Software
+    Foundation; either version 3 of the License or (at your option) any later
+    version). The texts of both licenses can be found in the files LICENSE.GPL
+    and LICENSE.LGPL.
+    
+    The following files are modified versions taken from Boost and are licensed
+    under the Boost Software License (see LICENSE.BSL).
+    - include/CGAL/auto_link/auto_link.h
+    - include/CGAL/internal/container_fwd_fixed.hpp
+    - include/CGAL/internal/boost/array_binary_tree.hpp
+    - include/CGAL/internal/boost/mutable_heap.hpp
+    - include/CGAL/internal/boost/mutable_queue.hpp
+    
+    Distributed along with CGAL (for the users' convenience), but not part of
+    CGAL, are the following third-party libraries, available under their own
+    licenses:
+    
+    - CORE, in the directories "include/CGAL/CORE" and "src/CGAL_Core", is
+        licensed under the LGPL (see LICENSE.LGPL).
+    - ImageIO, in the directory "src/CGAL_ImageIO", is licensed under the LGPL
+        (see LICENSE.LGPL).
+    - OpenNL, in the directory "include/CGAL/OpenNL", is licensed under the LGPL
+        (see LICENSE.LGPL).
+    
+    All other files that do not have an explicit copyright notice (e.g., all
+    examples and some demos) are licensed under a very permissive license. The
+    exact license text can be found in the file LICENSE.FREE_USE.
+    
+    More information on the CGAL license can be found at
+    https://www.cgal.org/license.html
+
+ +

cgmath

+
+        Apache License
+        Version 2.0, January 2004
+        http://www.apache.org/licenses/
+
+    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+    1. Definitions.
+
+    "License" shall mean the terms and conditions for use, reproduction,
+    and distribution as defined by Sections 1 through 9 of this document.
+
+    "Licensor" shall mean the copyright owner or entity authorized by
+    the copyright owner that is granting the License.
+
+    "Legal Entity" shall mean the union of the acting entity and all
+    other entities that control, are controlled by, or are under common
+    control with that entity. For the purposes of this definition,
+    "control" means (i) the power, direct or indirect, to cause the
+    direction or management of such entity, whether by contract or
+    otherwise, or (ii) ownership of fifty percent (50%) or more of the
+    outstanding shares, or (iii) beneficial ownership of such entity.
+
+    "You" (or "Your") shall mean an individual or Legal Entity
+    exercising permissions granted by this License.
+
+    "Source" form shall mean the preferred form for making modifications,
+    including but not limited to software source code, documentation
+    source, and configuration files.
+
+    "Object" form shall mean any form resulting from mechanical
+    transformation or translation of a Source form, including but
+    not limited to compiled object code, generated documentation,
+    and conversions to other media types.
+
+    "Work" shall mean the work of authorship, whether in Source or
+    Object form, made available under the License, as indicated by a
+    copyright notice that is included in or attached to the work
+    (an example is provided in the Appendix below).
+
+    "Derivative Works" shall mean any work, whether in Source or Object
+    form, that is based on (or derived from) the Work and for which the
+    editorial revisions, annotations, elaborations, or other modifications
+    represent, as a whole, an original work of authorship. For the purposes
+    of this License, Derivative Works shall not include works that remain
+    separable from, or merely link (or bind by name) to the interfaces of,
+    the Work and Derivative Works thereof.
+
+    "Contribution" shall mean any work of authorship, including
+    the original version of the Work and any modifications or additions
+    to that Work or Derivative Works thereof, that is intentionally
+    submitted to Licensor for inclusion in the Work by the copyright owner
+    or by an individual or Legal Entity authorized to submit on behalf of
+    the copyright owner. For the purposes of this definition, "submitted"
+    means any form of electronic, verbal, or written communication sent
+    to the Licensor or its representatives, including but not limited to
+    communication on electronic mailing lists, source code control systems,
+    and issue tracking systems that are managed by, or on behalf of, the
+    Licensor for the purpose of discussing and improving the Work, but
+    excluding communication that is conspicuously marked or otherwise
+    designated in writing by the copyright owner as "Not a Contribution."
+
+    "Contributor" shall mean Licensor and any individual or Legal Entity
+    on behalf of whom a Contribution has been received by Licensor and
+    subsequently incorporated within the Work.
+
+    2. Grant of Copyright License. Subject to the terms and conditions of
+    this License, each Contributor hereby grants to You a perpetual,
+    worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+    copyright license to reproduce, prepare Derivative Works of,
+    publicly display, publicly perform, sublicense, and distribute the
+    Work and such Derivative Works in Source or Object form.
+
+    3. Grant of Patent License. Subject to the terms and conditions of
+    this License, each Contributor hereby grants to You a perpetual,
+    worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+    (except as stated in this section) patent license to make, have made,
+    use, offer to sell, sell, import, and otherwise transfer the Work,
+    where such license applies only to those patent claims licensable
+    by such Contributor that are necessarily infringed by their
+    Contribution(s) alone or by combination of their Contribution(s)
+    with the Work to which such Contribution(s) was submitted. If You
+    institute patent litigation against any entity (including a
+    cross-claim or counterclaim in a lawsuit) alleging that the Work
+    or a Contribution incorporated within the Work constitutes direct
+    or contributory patent infringement, then any patent licenses
+    granted to You under this License for that Work shall terminate
+    as of the date such litigation is filed.
+
+    4. Redistribution. You may reproduce and distribute copies of the
+    Work or Derivative Works thereof in any medium, with or without
+    modifications, and in Source or Object form, provided that You
+    meet the following conditions:
+
+    (a) You must give any other recipients of the Work or
+    Derivative Works a copy of this License; and
+
+    (b) You must cause any modified files to carry prominent notices
+    stating that You changed the files; and
+
+    (c) You must retain, in the Source form of any Derivative Works
+    that You distribute, all copyright, patent, trademark, and
+    attribution notices from the Source form of the Work,
+    excluding those notices that do not pertain to any part of
+    the Derivative Works; and
+
+    (d) If the Work includes a "NOTICE" text file as part of its
+    distribution, then any Derivative Works that You distribute must
+    include a readable copy of the attribution notices contained
+    within such NOTICE file, excluding those notices that do not
+    pertain to any part of the Derivative Works, in at least one
+    of the following places: within a NOTICE text file distributed
+    as part of the Derivative Works; within the Source form or
+    documentation, if provided along with the Derivative Works; or,
+    within a display generated by the Derivative Works, if and
+    wherever such third-party notices normally appear. The contents
+    of the NOTICE file are for informational purposes only and
+    do not modify the License. You may add Your own attribution
+    notices within Derivative Works that You distribute, alongside
+    or as an addendum to the NOTICE text from the Work, provided
+    that such additional attribution notices cannot be construed
+    as modifying the License.
+
+    You may add Your own copyright statement to Your modifications and
+    may provide additional or different license terms and conditions
+    for use, reproduction, or distribution of Your modifications, or
+    for any such Derivative Works as a whole, provided Your use,
+    reproduction, and distribution of the Work otherwise complies with
+    the conditions stated in this License.
+
+    5. Submission of Contributions. Unless You explicitly state otherwise,
+    any Contribution intentionally submitted for inclusion in the Work
+    by You to the Licensor shall be under the terms and conditions of
+    this License, without any additional terms or conditions.
+    Notwithstanding the above, nothing herein shall supersede or modify
+    the terms of any separate license agreement you may have executed
+    with Licensor regarding such Contributions.
+
+    6. Trademarks. This License does not grant permission to use the trade
+    names, trademarks, service marks, or product names of the Licensor,
+    except as required for reasonable and customary use in describing the
+    origin of the Work and reproducing the content of the NOTICE file.
+
+    7. Disclaimer of Warranty. Unless required by applicable law or
+    agreed to in writing, Licensor provides the Work (and each
+    Contributor provides its Contributions) on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+    implied, including, without limitation, any warranties or conditions
+    of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+    PARTICULAR PURPOSE. You are solely responsible for determining the
+    appropriateness of using or redistributing the Work and assume any
+    risks associated with Your exercise of permissions under this License.
+
+    8. Limitation of Liability. In no event and under no legal theory,
+    whether in tort (including negligence), contract, or otherwise,
+    unless required by applicable law (such as deliberate and grossly
+    negligent acts) or agreed to in writing, shall any Contributor be
+    liable to You for damages, including any direct, indirect, special,
+    incidental, or consequential damages of any character arising as a
+    result of this License or out of the use or inability to use the
+    Work (including but not limited to damages for loss of goodwill,
+    work stoppage, computer failure or malfunction, or any and all
+    other commercial damages or losses), even if such Contributor
+    has been advised of the possibility of such damages.
+
+    9. Accepting Warranty or Additional Liability. While redistributing
+    the Work or Derivative Works thereof, You may choose to offer,
+    and charge a fee for, acceptance of support, warranty, indemnity,
+    or other liability obligations and/or rights consistent with this
+    License. However, in accepting such obligations, You may act only
+    on Your own behalf and on Your sole responsibility, not on behalf
+    of any other Contributor, and only if You agree to indemnify,
+    defend, and hold each Contributor harmless for any liability
+    incurred by, or claims asserted against, such Contributor by reason
+    of your accepting any such warranty or additional liability.
+
+    END OF TERMS AND CONDITIONS
+
+    APPENDIX: How to apply the Apache License to your work.
+
+    To apply the Apache License to your work, attach the following
+    boilerplate notice, with the fields enclosed by brackets "[]"
+    replaced with your own identifying information. (Don't include
+    the brackets!)  The text should be enclosed in the appropriate
+    comment syntax for the file format. We also recommend that a
+    file or class name and description of purpose be included on the
+    same "printed page" as the copyright notice for easier
+    identification within third-party archives.
+
+    Copyright [yyyy] [name of copyright owner]
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+ +

petgraph

+
+    Copyright (c) 2015
+
+    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.
+
+ +

meshlite

+
+    MIT License
+
+    Copyright (c) 2018 XingYi Hu
+    
+    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.
+
+ +

Reza Nourai

+
+    https://blogs.msdn.microsoft.com/rezanour/2011/08/07/barycentric-coordinates-and-point-in-triangle-tests/
+
+ +

Cyranose

+
+    https://www.opengl.org/discussion_boards/showthread.php/159385-Deriving-angles-from-0-to-360-from-Dot-Product
+
+ +

Dan Sunday

+
+    http://geomalgorithms.com/index.html
+
+ +

user3146587

+
+    https://stackoverflow.com/questions/21114796/3d-ray-quad-intersection-test-in-java
+
+ +

Qt

+
+    https://www.qt.io/
+
+ +

Qt Dark Theme

+
+    https://gist.github.com/QuantumCD/6245215
+
+ +

A Browser for QDebug Log Output

+
+    https://wiki.qt.io/Browser_for_QDebug_output
+
+ +

Hello GL2 Example

+
+    http://doc.qt.io/qt-5/qtopengl-hellogl2-example.html
+
+ +

Jimmy Gunawan

+
+    INSPIRATION / Pixar Monster Factory Part One
+    http://blendersushi.blogspot.com.au/2013/06/inspiration-pixar-monster-factory-part.html
+
+ +

Zhongping Ji, Ligang Liu, Yigang Wang

+
+    B-Mesh: A Fast Modeling System for Base Meshes of 3D Articulated Shapes
+    http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.357.7134&rep=rep1&type=pdf
+
+ +

danielhst

+
+    https://github.com/danielhst/3d-Hull-gift-wrap/blob/master/giftWrap.lua
+
+ +

Catmull–Clark subdivision surface/C

+
+    https://rosettacode.org/wiki/Catmull%E2%80%93Clark_subdivision_surface/C
+
\ No newline at end of file diff --git a/README.md b/README.md index f40da77d..121905df 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,5 @@ WIP... Build -------- ``` -$ cd thirdparty/carve-1.4.0/build -$ cmake ../ -$ make && make install - $ qmake -spec macx-xcode ``` - -UI Resources Source --------- -Qt Dark Theme: -- https://gist.github.com/QuantumCD/6245215 - -Icons: -- https://material.io/icons/ -18dp white -3d rotation -> rotate -add circle outline -> add -zoom in -> zoomin -zoom out -> zoomout diff --git a/dust3d.pro b/dust3d.pro index c3a00a71..c0be1869 100644 --- a/dust3d.pro +++ b/dust3d.pro @@ -2,6 +2,21 @@ QT += core widgets opengl CONFIG += debug RESOURCES += resources.qrc +HUMAN_VERSION = "0.0-alpha1" +REPOSITORY_URL = "https://github.com/huxingyi/dust3d" +ISSUES_URL = "https://github.com/huxingyi/dust3d/issues" +VERSION = 0.0.0.1 +QMAKE_TARGET_COMPANY = Dust3D +QMAKE_TARGET_PRODUCT = Dust3D +QMAKE_TARGET_DESCRIPTION = "Aim to be a quick modeling tool for game development" +QMAKE_TARGET_COPYRIGHT = "Copyright (C) 2018 Dust3D Project. All Rights Reserved." + +DEFINES += "APP_NAME=\"\\\"$$QMAKE_TARGET_PRODUCT\\\"\"" +DEFINES += "APP_VER=\"\\\"$$VERSION\\\"\"" +DEFINES += "APP_HUMAN_VER=\"\\\"$$HUMAN_VERSION\\\"\"" +DEFINES += "APP_REPOSITORY_URL=\"\\\"$$REPOSITORY_URL\\\"\"" +DEFINES += "APP_ISSUES_URL=\"\\\"$$ISSUES_URL\\\"\"" + include(thirdparty/QtAwesome/QtAwesome/QtAwesome.pri) INCLUDEPATH += src @@ -30,6 +45,9 @@ HEADERS += src/skeletongraphicswidget.h SOURCES += src/skeletonpartlistwidget.cpp HEADERS += src/skeletonpartlistwidget.h +SOURCES += src/aboutwidget.cpp +HEADERS += src/aboutwidget.h + SOURCES += src/meshgenerator.cpp HEADERS += src/meshgenerator.h diff --git a/dust3d_jezzasoft.xcf b/dust3d_jezzasoft.xcf deleted file mode 100644 index c92d2f589d2d87f91f397cd7a17beaa27e94c122..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4104 zcmeHKeQ;D)6~FIe-+ts{^Bst`z6n!Ei<`jErdpFX1zH0rAB|}PAt`Ql-!2RL5q3A2 zM5R(b3fiI>M#dV)DrzO9MN5@hMVvZNz!1P0p(!SmSPHZ@DJdb@-S^&J&w0B`H-)Ny zIpYj>=Is5w-#zEt`|dsWoVRX|uSINgxI~x7D-&o_L4pXN<$^E_{N#g}=6#P46let? zJctpbD2Y+85cF%H&vJuXnbE+@w>&@skwk`zZIF}Y^{DzSoHWEtK|^d2VAa@9KNwYw7!{2Fq2Jj zACisPmqO6vcCQ7&9kncyJ38#4fJ@(F78q;-2p$dvF(ol-o0Awi^64~eNy8awI5Q1r zrC}>z^vi+ZTo?p6AGTCr;1~wO3g{Cj9G2U{Hm|*1)`JUAxVp?|53X%(5lgEQK&paG z^oy>5KV02dy0|^)u{*_O!GJg~;B_{ZN;lrNssa_Is*0ptDQqiRi61vg4cJ1S4q2T2 zxl~Xj)O9wo>n)x(*^38e_lG@pug4w|+w}wV+WqcUh)uCn_S+nObl7YU$u_4aC_BQQ zfM4{6gFt>fIB%Tz%>d(SO=~FZakYzH*%d~Yfi{~j;FLvYyWj5fIFgheOE;Fb+5>BY zP}CQOo1JXj@gzuv1{?-AM2JqIAZ4eCl^CQP!b{kvBo~x( zCOvaE>7;ZMebRY$nvpd%qczQY`pk&)2_r2eX30#lB!Og029hK3y5ve6*cm}S>XDl) zJlN#JL}uOAs@o@Yc!>eqIFUHr!GH0<6Ao-pcvu4uzL><8;R6Q)-0fZb&s%zX7yoCm z7!{VxCHFVNm5wm0-{}2m|IZO-KHlHgy~Bpkxi#oA*A zPj>G?#6GCKSl0teiqT-nMW4k#Idc3d95S?Cy8Ke7t{&X{r_H}a#LaaD2G=9vn>I}S z;#Pfh*N6Ld9zd2GN>F8U3shYoE*Zc0dGq33@Gj?_^8hr&h=8gg~ zy&9)2Id-Jnr5l6~A6f7_9j)2__out`-C;k~_x`~d2yH9tYnx{p5c&t->>oUDN0?Dq zSWvp_0LdfToJO*M#2Hp#;|$b1RVZs};ag(s)_%E|AqJuvo!6;}YDZ^50aTfLqtrsR zo&uAnUnOrg~&Xs(q?3$ran62pHab8M9OErm_{lCy?uON^u#~o2}KPQGpVK1r<(IJ;H|FaI`$seU1uuTadv3n<(ZK2h?d()IjTPK zoJLuy4U_;iQhAle6_rjXbmE#qsfz)(!+Zm!=XUSELh0L^8}~+NH;b&Wnhv`O*%qhU z1aika^l*y3u!IoDDD5zj!gKU>oi(hYmm3I?C+G$MZjm`hk~_hn_$9s6fgt z(r0klBX@ptO*wqW6itQCltOv?y^x?ujLk2}f}~7h8E_=H_?WTj?4C+khw{Mpe`AB* zpr>oh0`-B1;-MD`1B_;ZWI@$QhzTl`UO|$8l`K%;fMKEN z#3lH=%9x~Y1bg&uPT8d#CHhPlFiUrWk`b2jQJJ$OmVtFn0RR84-c7mvX8)IGrmUlb z4?dpv*p{{x4be=S3$&*iHsNG5rxKM5jGNo3s;MpnrSQM zn8CXs&d2#Bf9t(KBmGB@Gsl(Q0}v}l5;Lr${WXvT$GGVY55pHB%kvDekg<%rip|72 z-@fvJY`F6i%%b754P?Da8LDl_1jC3yro$_z9>NPhHV24NQ;-hKo%c39bSz4j^N@~4 zjV<)8`t|g3^<$UlR}IMU4m#0$hW@tbkxR6m2P^HN>);`>kpw=B&u4+SmbZCCQC_>B zY>JJ{fJ-u#^ZCUKrt?IsU7iU!2ojN*XuR1Zm^3rKfNHd#KP{YA&)4FVnz@w - resources/dust3d_jezzasoft.png + resources/dust3d_vertical.png + ACKNOWLEDGEMENTS.html \ No newline at end of file diff --git a/resources/dust3d_jezzasoft.png b/resources/dust3d_jezzasoft.png deleted file mode 100644 index 0e5d3fb85355f5a3c52e3aa013ac81eabf96bf3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2087 zcmV+?2-x?DP)P0043b1^@s6rrLV?00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru;sgN>4J9s6R*V1u03B&m zSad^gZEa<4bN~PV002XBWnpw>WFU8GbZ8()Nlj2>E@cM*00)anL_t(&-tC%sjAT_6 z#(%G>dYPqX254r0L23j$h#3`SUp#1Vl*pp2Za|EQA*gXo6e2_vF~lX~!e2%SE-VQN z5|*HX5nGN-f(U~!(1^e=4{*fU5W1%~YVnWyUcA24@?Q7r#F)8BC0$jo-?{fJ-}jyK z3ihtOYoS&&-y4`&KX4i_?(U;zb{sI`?l&jbROV?Q0zM6_2PVvH3^)#W1SrM`umqTt zM@qmT@N3{_;YIT`@-g60;3vQYa3io7s26Eu2XM7+It`pIBYyy{O)e_W$3h&(&s3|` z9lG(~fqwzpfXCuE9!st(%{RN;{U+c_8CWJ`zYDJ^6_CPZWz>nl^S}wfmkTu_myZB{ z1AYiR0lcGVBX0n|0ZsxQ20jAZQfS2oZd(f+4s3PzM~jA)3UC%M1gr!u0WNd*JBq54 z#O3b<-m2I<3Val}qsW-lfKlK);055ebo~_>lL4Rs^Z^6z{%Vo#!pv3y-;uj2!0&-8 z3NvD6eZa-Q2jrQD1v7=#NhE;$DezZdv$CvEBX!_0;6>n0;NyBvkuiyYgMkBp>w%|% z_ZDrWBv8B%I8rEl#6rYmTCRE)_>@?((33QPr-0Yo{g-Ao1H7r|lc1YkZe}L~4*>fu zMBtYLD}WyZJAv01A|@qq%4G_ErRbB)0vmuSci(Ad#{jDeJ&C(FRgm|X*+vPxVpCYy z^$9^fz(K${3z1!uz;0pI`+=7VGa|uV2krt^1N$iO8;T74TFIZ9Wa$dvPr#NUjl557 zLk(z(6(iuAp|YQEOl|;<2Ts;&9dIvj{(`n{W_FaBUAVW2UAXR7wb1Ljrr;%?5w*^T z0>`BJA@%>WKn(mD*xt@qrHdN82)Hh-p}MSxfJ=eDv}<76x#vjWb|GrkbuchotyV|l zIG*fvk}~jNpkME&B&28C#R3=x4%EF)my361F>*rCMcpgLn@Ghl;gOSoZ;3+|0c*@` zle^ct8HogR=*D-t`}1AgbeEZ35D?XpbV8N72)3m6*pVR~6TuWB%9LGlMe_ZKnQiHi z1c}RryX~MC)DHn)P&${oY^d4|(R&%G0EZPJmOG7@=DPaQajA4@{@07In>h{i0~-V7 z(iyunFPY0~x_hJW8Dy}rMJmP_ci)rSh?(sp!S+F5sbJ%7;2yodlGdjFF{z83Pm>(B>$URm`Fu-@J8%R^{cQn&&93izjX`*^B`k`vWC^(Z~LJdWdLwOXB4 zKwkvL<2bIF*>dS%Q+hA0R;#bYaXgb%5~W*-Qxv07;Ou~q?$fCDX2HxC&1`9IBXa3# z)yOI!GP484>1j_~PV4bTZX;z?G|H-Na?gl98`Y>|R)shsM$KiU85CDp(0;Vg;1%HO z%Di0xB}93%s~#}m0kj^9CHS>TX&tKiskHLU0+2Ozx0BlC(ZJjE^&{@S9XLd+*%Zk& z<}&Zv1pGiTsp;jkp7WxU4AOem_;OmRW*SAg`))JaprRa>O574a$PKg}0`$`wqc7-$ zs5GasM7m}H3AkcK_$l+vfV`_|mpn@ zl+=MST2FfRbU=(cuwB%jZPB zQ5#RJQ2|D3H%82CM|hf22Shzef6oFpsiIFwzuf(JTE8gRr0W+;RP^k$qCm_|0QU=3 z>mB0pq+0BE@t+@(-6Nj>&%BQhGov}WFsZ75M!nb|=am~|T24$Y@OSpj?(xTZ%M z4|QupDm~m)QZu@IJ_TOs;DT|1VLc~;42hC^8mVbOwz3N#c3J~U=Q{tF>)#(VLPr4X RbbbH;002ovPDHLkV1g2~+)Dre diff --git a/resources/dust3d_vertical.png b/resources/dust3d_vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..a8f671565452f662a7e88872428aa9e5a0e579f3 GIT binary patch literal 1567 zcmV+)2H^RLP)P002h_1^@s6@@?$o00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru;sgo<6ftm9Kf3?`03B&m zSad^gZEa<4bN~PV002XBWnpw>WFU8GbZ8()Nlj2>E@cM*00o0dL_t(o!`+x!s8wYY z$A9~reebz?y65v^2Y)-3u3M>P@j&GC`%yTBN z2Iwka`jdgaYPGsPilU=gCn*Eh0G+xY0CvhO|MCUU2Taqqu8@iU$Rp%@U5@}C0QUfg zV?JyGb>KqaF<>q*2AJub+hk_7G$8>nLDz?Yx6JIP6f?c;oV!6n0J`IVDkc1}^}Gh` z&*6{5I#pZ5hDo^h_Gdfib|iqHWwO3B4Z;i!dw@Fx=TeG7)s+2AAt5kpw6u~Wy?Yej z?1=lN-AP7(<-kPGH+feluu*_X=Pu`5`4~^pG_yuw7S6kzb7K^YLuPiQun_0mMBqB$ zQed2DV=eHG?(YXKlP^br?|}P&ZD!UCa|(D^Q1;gnJ)a3Ije%&E7{CF0@+7SVz=uGu z7kq&@ZcyL}bzM`?wv@jGc)pw=AOtQ3Zq%dgBFtaJZxeKa-N19e%TnYt2^s*_lybz; zcwHX^o-(tgW_C63k_YkEfK_I;8n_EMpy$)mg0bD|2Y&PJf7CO8L*BOrfg^sfX9;QQ z$N&`~$v73gFJSaPF z@U}Ms-{^yf%&e834*-wI`ELNtJPVGtN^x{*+>@E`z9Mw=9{ZRZ)`$xyHeH4oH)Uz6?e;2OSS4S6T6$HJHQJO>p$FSWp7dJ>e$Xi@G1l%#vF;A}qn<}d zO||W}3dYHD>JYFC_%r6ul1it?C8%)g&;i^oRDBODH?ws`5?USzH-LA6-=*#5SPZpJ zR8Q(rGIMzpMa^opIw*(!4D?4)RCCVtst$I7_qb}c`gasXLwQT0cq_3`K3Wf4>m{Uh zz>C0Y(afFBxp9Ss$fVO1BU6FEIX6u?J#LA6fjP$_q^yWWsM;g*%+|B@z-Nl5Zq?3V zQ6Wt)xXPmT`C@~;z(Ydb0WT#4MbTCFGT@28-5!5!60H5Iejr|X<{`+My4$1Daz1dn zw!bv9eZUN5&8CuEqo}!B8b2eS)bumpJ?-~Nfxm#=@lrL9xiGVX&bcQQlzplacX$-? z0`Qe`{;&V(ctZ?OeC**Bc^x@gi2xXmvv3}Qtf+!qsro8yjTidXX0_Pu0=5r0B}Y89 z+VjG35@+NjMFLfrd-7(+czgG;%ry|(8v@P%uGb{#72uP?KFQ&;9u0dI3(||sY*%4( z37X}E9aQNEcct8|J$%YFx(=Ompf+8)hm!LDP; zCq>pSv2wkrPwK!n;C-*Ti36t&?9)4&;{`=tS{b8gZluIdzt=t-{}p7Q>MCGS%D^+w zDfR-do7uM9LNpUz3EZk8BFn6;?0GY=A?}k>LY=ih!_7&e_%P$wrBdhtuZ2iOSE%-& z;%T{EsW)h2iJ1z(K<&nC=iGk(G^GSaJxABWz^jVrh4G)6od;Z^8UR%>GvXAKG_vjS zrlcQ)+9ohON>Qv|4Ng9|Xjcj{H)1OQLo&Z*5 zz_=%^4GA;MRZ=tBJDLCw6HL%AGOQP5kRGMvOd&N5$fl$aV$!Mz&UOBm>mM~>v&R)f RWxxOc002ovPDHLkV1ffQ-U +#include +#include "aboutwidget.h" + +AboutWidget::AboutWidget() +{ + QTextEdit *versionInfoLabel = new QTextEdit; + versionInfoLabel->setText(QString("%1 %2 (version: %3 build: %4 %5)").arg(APP_NAME).arg(APP_HUMAN_VER).arg(APP_VER).arg(__DATE__).arg(__TIME__)); + versionInfoLabel->setReadOnly(true); + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->addWidget(versionInfoLabel); + + setLayout(mainLayout); + setFixedSize(QSize(350, 75)); + + setWindowTitle(APP_NAME); +} diff --git a/src/aboutwidget.h b/src/aboutwidget.h new file mode 100644 index 00000000..b81c54a6 --- /dev/null +++ b/src/aboutwidget.h @@ -0,0 +1,12 @@ +#ifndef ABOUT_WIDGET_H +#define ABOUT_WIDGET_H +#include + +class AboutWidget : public QWidget +{ + Q_OBJECT +public: + AboutWidget(); +}; + +#endif diff --git a/src/logbrowser.cpp b/src/logbrowser.cpp index 431d8ed5..c42efc84 100644 --- a/src/logbrowser.cpp +++ b/src/logbrowser.cpp @@ -9,7 +9,6 @@ LogBrowser::LogBrowser(QObject *parent) : qRegisterMetaType("QtMsgType"); m_browserDialog = new LogBrowserDialog; connect(this, SIGNAL(sendMessage(QtMsgType,QString)), m_browserDialog, SLOT(outputMessage(QtMsgType,QString)), Qt::QueuedConnection); - m_browserDialog->show(); } LogBrowser::~LogBrowser() @@ -17,6 +16,23 @@ LogBrowser::~LogBrowser() delete m_browserDialog; } +void LogBrowser::showDialog() +{ + m_browserDialog->show(); + m_browserDialog->activateWindow(); + m_browserDialog->raise(); +} + +void LogBrowser::hideDialog() +{ + m_browserDialog->hide(); +} + +bool LogBrowser::isDialogVisible() +{ + return m_browserDialog->isVisible(); +} + void LogBrowser::outputMessage(QtMsgType type, const QString &msg) { printf("%s\n", msg.toUtf8().constData()); diff --git a/src/logbrowser.h b/src/logbrowser.h index bc8df370..407a152e 100644 --- a/src/logbrowser.h +++ b/src/logbrowser.h @@ -14,6 +14,9 @@ public: public slots: void outputMessage(QtMsgType type, const QString &msg); + void showDialog(); + void hideDialog(); + bool isDialogVisible(); signals: void sendMessage(QtMsgType type, const QString &msg); @@ -22,4 +25,4 @@ private: LogBrowserDialog *m_browserDialog; }; -#endif // LOGBROWSER_H \ No newline at end of file +#endif // LOGBROWSER_H diff --git a/src/logbrowserdialog.cpp b/src/logbrowserdialog.cpp index d3958ff2..a0911321 100644 --- a/src/logbrowserdialog.cpp +++ b/src/logbrowserdialog.cpp @@ -38,6 +38,8 @@ LogBrowserDialog::LogBrowserDialog(QWidget *parent) : connect(m_saveButton, SIGNAL(clicked()), this, SLOT(save())); resize(400, 300); + + hide(); } @@ -100,15 +102,8 @@ void LogBrowserDialog::save() void LogBrowserDialog::closeEvent(QCloseEvent *e) { - QMessageBox::StandardButton answer = QMessageBox::question(this, - tr("Close Log Browser?"), - tr("Do you really want to close the log browser?"), - QMessageBox::Yes | QMessageBox::No); - - if (answer == QMessageBox::Yes) - e->accept(); - else - e->ignore(); + e->ignore(); + hide(); } void LogBrowserDialog::keyPressEvent(QKeyEvent *e) diff --git a/src/main.cpp b/src/main.cpp index 5606e957..522818ab 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,21 +2,11 @@ #include #include #include -#include #include #include -#include "logbrowser.h" #include "skeletondocumentwindow.h" #include "theme.h" -QPointer g_logBrowser; - -void outputMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg) -{ - if (g_logBrowser) - g_logBrowser->outputMessage(type, msg); -} - int main(int argc, char ** argv) { QApplication app(argc, argv); @@ -49,11 +39,8 @@ int main(int argc, char ** argv) font.setPixelSize(9); font.setBold(false); QApplication::setFont(font); - - g_logBrowser = new LogBrowser; - qInstallMessageHandler(&outputMessage); - SkeletonDocumentWindow mainWindow; - mainWindow.showMaximized(); + SkeletonDocumentWindow::createDocumentWindow(); + return app.exec(); } diff --git a/src/modelwidget.cpp b/src/modelwidget.cpp index acfb0c01..a189b576 100644 --- a/src/modelwidget.cpp +++ b/src/modelwidget.cpp @@ -37,6 +37,21 @@ ModelWidget::ModelWidget(QWidget *parent) setMouseTracking(true); } +int ModelWidget::xRot() +{ + return m_xRot; +} + +int ModelWidget::yRot() +{ + return m_yRot; +} + +int ModelWidget::zRot() +{ + return m_zRot; +} + void ModelWidget::setGraphicsFunctions(SkeletonGraphicsFunctions *graphicsFunctions) { m_graphicsFunctions = graphicsFunctions; diff --git a/src/modelwidget.h b/src/modelwidget.h index 475ce3d5..c73e3120 100644 --- a/src/modelwidget.h +++ b/src/modelwidget.h @@ -47,6 +47,12 @@ protected: void mouseMoveEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; + +public: + int xRot(); + int yRot(); + int zRot(); + private: int m_xRot; int m_yRot; diff --git a/src/skeletondocument.cpp b/src/skeletondocument.cpp index 4fb17bb0..c7844d46 100644 --- a/src/skeletondocument.cpp +++ b/src/skeletondocument.cpp @@ -26,6 +26,7 @@ SkeletonDocument::~SkeletonDocument() void SkeletonDocument::uiReady() { + qDebug() << "uiReady"; emit editModeChanged(); } @@ -615,24 +616,19 @@ void SkeletonDocument::addFromSnapshot(const SkeletonSnapshot &snapshot) emit skeletonChanged(); } -void SkeletonDocument::fromSnapshot(const SkeletonSnapshot &snapshot) +void SkeletonDocument::reset() { - for (const auto &nodeIt: nodeMap) { - emit nodeRemoved(nodeIt.first); - } - for (const auto &edgeIt: edgeMap) { - emit edgeRemoved(edgeIt.first); - } - for (const auto &partIt : partMap) { - emit partRemoved(partIt.first); - } - nodeMap.clear(); edgeMap.clear(); partMap.clear(); partIds.clear(); - emit partListChanged(); - + emit cleanup(); + emit skeletonChanged(); +} + +void SkeletonDocument::fromSnapshot(const SkeletonSnapshot &snapshot) +{ + reset(); addFromSnapshot(snapshot); } diff --git a/src/skeletondocument.h b/src/skeletondocument.h index 984b2cf8..4e2e31e0 100644 --- a/src/skeletondocument.h +++ b/src/skeletondocument.h @@ -123,6 +123,7 @@ signals: void partLockStateChanged(QUuid partId); void partVisibleStateChanged(QUuid partId); void partSubdivStateChanged(QUuid partId); + void cleanup(); public: SkeletonDocument(); ~SkeletonDocument(); @@ -168,6 +169,7 @@ public slots: void paste(); void batchChangeBegin(); void batchChangeEnd(); + void reset(); private: void splitPartByNode(std::vector> *groups, QUuid nodeId); void joinNodeAndNeiborsToGroup(std::vector *group, QUuid nodeId, std::set *visitMap, QUuid noUseEdgeId=QUuid()); diff --git a/src/skeletondocumentwindow.cpp b/src/skeletondocumentwindow.cpp index dae69cbc..0022208e 100644 --- a/src/skeletondocumentwindow.cpp +++ b/src/skeletondocumentwindow.cpp @@ -6,15 +6,74 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include "skeletondocumentwindow.h" #include "skeletongraphicswidget.h" #include "skeletonpartlistwidget.h" #include "theme.h" +#include "ds3file.h" +#include "skeletonsnapshot.h" +#include "skeletonxml.h" +#include "logbrowser.h" +#include "util.h" +#include "aboutwidget.h" + +QPointer g_logBrowser; +std::set g_documentWindows; +QTextBrowser *g_acknowlegementsWidget = nullptr; +AboutWidget *g_aboutWidget = nullptr; + +void outputMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + if (g_logBrowser) + g_logBrowser->outputMessage(type, msg); +} + +void SkeletonDocumentWindow::SkeletonDocumentWindow::showAcknowlegements() +{ + if (!g_acknowlegementsWidget) { + g_acknowlegementsWidget = new QTextBrowser; + g_acknowlegementsWidget->setWindowTitle(APP_NAME); + g_acknowlegementsWidget->setMinimumSize(QSize(400, 300)); + QFile file(":/ACKNOWLEDGEMENTS.html"); + file.open(QFile::ReadOnly | QFile::Text); + QTextStream stream(&file); + g_acknowlegementsWidget->setHtml(stream.readAll()); + } + g_acknowlegementsWidget->show(); + g_acknowlegementsWidget->activateWindow(); + g_acknowlegementsWidget->raise(); +} + +void SkeletonDocumentWindow::SkeletonDocumentWindow::showAbout() +{ + if (!g_aboutWidget) { + g_aboutWidget = new AboutWidget; + } + g_aboutWidget->show(); + g_aboutWidget->activateWindow(); + g_aboutWidget->raise(); +} SkeletonDocumentWindow::SkeletonDocumentWindow() : m_document(nullptr), - m_firstShow(true) + m_firstShow(true), + m_documentSaved(true) { + if (!g_logBrowser) { + g_logBrowser = new LogBrowser; + qInstallMessageHandler(&outputMessage); + } + + g_documentWindows.insert(this); + m_document = new SkeletonDocument; QVBoxLayout *toolButtonLayout = new QVBoxLayout; @@ -47,13 +106,13 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() : toolButtonLayout->addWidget(zoomInButton); toolButtonLayout->addWidget(zoomOutButton); - QLabel *dust3dJezzasoftLabel = new QLabel; - QImage dust3dJezzasoftImage; - dust3dJezzasoftImage.load(":/resources/dust3d_jezzasoft.png"); - dust3dJezzasoftLabel->setPixmap(QPixmap::fromImage(dust3dJezzasoftImage)); + QLabel *verticalLogoLabel = new QLabel; + QImage verticalLogoImage; + verticalLogoImage.load(":/resources/dust3d_vertical.png"); + verticalLogoLabel->setPixmap(QPixmap::fromImage(verticalLogoImage)); QHBoxLayout *logoLayout = new QHBoxLayout; - logoLayout->addWidget(dust3dJezzasoftLabel); + logoLayout->addWidget(verticalLogoLabel); logoLayout->setContentsMargins(0, 0, 0, 0); QVBoxLayout *mainLeftLayout = new QVBoxLayout; @@ -65,9 +124,9 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() : mainLeftLayout->addSpacing(10); SkeletonGraphicsWidget *graphicsWidget = new SkeletonGraphicsWidget(m_document); + m_graphicsWidget = graphicsWidget; SkeletonGraphicsContainerWidget *containerWidget = new SkeletonGraphicsContainerWidget; - containerWidget->setGraphicsWidget(graphicsWidget); QGridLayout *containerLayout = new QGridLayout; containerLayout->setSpacing(0); @@ -103,6 +162,147 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() : setCentralWidget(centralWidget); setWindowTitle(tr("Dust3D")); + m_fileMenu = menuBar()->addMenu(tr("File")); + + m_newWindowAction = new QAction(tr("New Window"), this); + connect(m_newWindowAction, &QAction::triggered, this, &SkeletonDocumentWindow::newWindow); + m_fileMenu->addAction(m_newWindowAction); + + m_newDocumentAction = new QAction(tr("New"), this); + connect(m_newDocumentAction, &QAction::triggered, this, &SkeletonDocumentWindow::newDocument); + m_fileMenu->addAction(m_newDocumentAction); + + m_openAction = new QAction(tr("Open..."), this); + connect(m_openAction, &QAction::triggered, this, &SkeletonDocumentWindow::open); + m_fileMenu->addAction(m_openAction); + + m_saveAction = new QAction(tr("Save"), this); + connect(m_saveAction, &QAction::triggered, this, &SkeletonDocumentWindow::save); + m_fileMenu->addAction(m_saveAction); + + m_saveAsAction = new QAction(tr("Save As..."), this); + connect(m_saveAsAction, &QAction::triggered, this, &SkeletonDocumentWindow::saveAs); + m_fileMenu->addAction(m_saveAsAction); + + m_saveAllAction = new QAction(tr("Save All"), this); + connect(m_saveAllAction, &QAction::triggered, this, &SkeletonDocumentWindow::saveAll); + m_fileMenu->addAction(m_saveAllAction); + + m_fileMenu->addSeparator(); + + m_exportAction = new QAction(tr("Export..."), this); + connect(m_exportAction, &QAction::triggered, this, &SkeletonDocumentWindow::exportResult); + m_fileMenu->addAction(m_exportAction); + + m_changeTurnaroundAction = new QAction(tr("Change Turnaround..."), this); + connect(m_changeTurnaroundAction, &QAction::triggered, this, &SkeletonDocumentWindow::changeTurnaround); + m_fileMenu->addAction(m_changeTurnaroundAction); + + connect(m_fileMenu, &QMenu::aboutToShow, [=]() { + m_exportAction->setEnabled(m_graphicsWidget->hasItems()); + }); + + m_editMenu = menuBar()->addMenu(tr("Edit")); + + m_addAction = new QAction(tr("Add..."), this); + connect(m_addAction, &QAction::triggered, [=]() { + m_document->setEditMode(SkeletonDocumentEditMode::Add); + }); + m_editMenu->addAction(m_addAction); + + m_undoAction = new QAction(tr("Undo"), this); + connect(m_undoAction, &QAction::triggered, m_document, &SkeletonDocument::undo); + m_editMenu->addAction(m_undoAction); + + m_redoAction = new QAction(tr("Redo"), this); + connect(m_redoAction, &QAction::triggered, m_document, &SkeletonDocument::redo); + m_editMenu->addAction(m_redoAction); + + m_deleteAction = new QAction(tr("Delete"), this); + connect(m_deleteAction, &QAction::triggered, m_graphicsWidget, &SkeletonGraphicsWidget::deleteSelected); + m_editMenu->addAction(m_deleteAction); + + m_cutAction = new QAction(tr("Cut"), this); + connect(m_cutAction, &QAction::triggered, m_graphicsWidget, &SkeletonGraphicsWidget::cut); + m_editMenu->addAction(m_cutAction); + + m_copyAction = new QAction(tr("Copy"), this); + connect(m_copyAction, &QAction::triggered, m_graphicsWidget, &SkeletonGraphicsWidget::copy); + m_editMenu->addAction(m_copyAction); + + m_pasteAction = new QAction(tr("Paste"), this); + connect(m_pasteAction, &QAction::triggered, m_document, &SkeletonDocument::paste); + m_editMenu->addAction(m_pasteAction); + + m_flipHorizontallyAction = new QAction(tr("H Flip"), this); + connect(m_flipHorizontallyAction, &QAction::triggered, m_graphicsWidget, &SkeletonGraphicsWidget::flipHorizontally); + m_editMenu->addAction(m_flipHorizontallyAction); + + m_flipVerticallyAction = new QAction(tr("V Flip"), this); + connect(m_flipVerticallyAction, &QAction::triggered, m_graphicsWidget, &SkeletonGraphicsWidget::flipVertically); + m_editMenu->addAction(m_flipVerticallyAction); + + m_selectAllAction = new QAction(tr("Select All"), this); + connect(m_selectAllAction, &QAction::triggered, m_graphicsWidget, &SkeletonGraphicsWidget::selectAll); + m_editMenu->addAction(m_selectAllAction); + + m_selectPartAllAction = new QAction(tr("Select Part"), this); + connect(m_selectPartAllAction, &QAction::triggered, m_graphicsWidget, &SkeletonGraphicsWidget::selectPartAll); + m_editMenu->addAction(m_selectPartAllAction); + + m_unselectAllAction = new QAction(tr("Unselect All"), this); + connect(m_unselectAllAction, &QAction::triggered, m_graphicsWidget, &SkeletonGraphicsWidget::unselectAll); + m_editMenu->addAction(m_unselectAllAction); + + connect(m_editMenu, &QMenu::aboutToShow, [=]() { + m_undoAction->setEnabled(m_document->undoable()); + m_redoAction->setEnabled(m_document->redoable()); + m_deleteAction->setEnabled(m_graphicsWidget->hasSelection()); + m_cutAction->setEnabled(m_graphicsWidget->hasSelection()); + m_copyAction->setEnabled(m_graphicsWidget->hasSelection()); + m_pasteAction->setEnabled(m_document->hasPastableContentInClipboard()); + m_flipHorizontallyAction->setEnabled(m_graphicsWidget->hasMultipleSelection()); + m_flipVerticallyAction->setEnabled(m_graphicsWidget->hasMultipleSelection()); + m_selectAllAction->setEnabled(m_graphicsWidget->hasItems()); + m_selectPartAllAction->setEnabled(m_graphicsWidget->hasItems()); + m_unselectAllAction->setEnabled(m_graphicsWidget->hasSelection()); + }); + + m_viewMenu = menuBar()->addMenu(tr("View")); + + m_resetModelWidgetPosAction = new QAction(tr("Show Model"), this); + connect(m_resetModelWidgetPosAction, &QAction::triggered, [=]() { + QRect parentRect = QRect(QPoint(0, 0), m_modelWidget->parentWidget()->size()); + if (!parentRect.contains(m_modelWidget->geometry().center())) { + m_modelWidget->move(16, 16); + } + }); + m_viewMenu->addAction(m_resetModelWidgetPosAction); + + m_showDebugDialogAction = new QAction(tr("Show Debug Dialog"), this); + connect(m_showDebugDialogAction, &QAction::triggered, g_logBrowser, &LogBrowser::showDialog); + m_viewMenu->addAction(m_showDebugDialogAction); + + m_helpMenu = menuBar()->addMenu(tr("Help")); + + m_viewSourceAction = new QAction(tr("Fork me on GitHub"), this); + connect(m_viewSourceAction, &QAction::triggered, this, &SkeletonDocumentWindow::viewSource); + m_helpMenu->addAction(m_viewSourceAction); + + m_helpMenu->addSeparator(); + + m_aboutAction = new QAction(tr("About"), this); + connect(m_aboutAction, &QAction::triggered, this, &SkeletonDocumentWindow::about); + m_helpMenu->addAction(m_aboutAction); + + m_reportIssuesAction = new QAction(tr("Report Issues"), this); + connect(m_reportIssuesAction, &QAction::triggered, this, &SkeletonDocumentWindow::reportIssues); + m_helpMenu->addAction(m_reportIssuesAction); + + m_seeAcknowlegementsAction = new QAction(tr("Acknowlegements"), this); + connect(m_seeAcknowlegementsAction, &QAction::triggered, this, &SkeletonDocumentWindow::seeAcknowlegements); + m_helpMenu->addAction(m_seeAcknowlegementsAction); + connect(containerWidget, &SkeletonGraphicsContainerWidget::containerSizeChanged, graphicsWidget, &SkeletonGraphicsWidget::canvasResized); @@ -146,10 +346,14 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() : connect(graphicsWidget, &SkeletonGraphicsWidget::undo, m_document, &SkeletonDocument::undo); connect(graphicsWidget, &SkeletonGraphicsWidget::redo, m_document, &SkeletonDocument::redo); connect(graphicsWidget, &SkeletonGraphicsWidget::paste, m_document, &SkeletonDocument::paste); - connect(graphicsWidget, &SkeletonGraphicsWidget::changeTurnaround, this, &SkeletonDocumentWindow::changeTurnaround); connect(graphicsWidget, &SkeletonGraphicsWidget::batchChangeBegin, m_document, &SkeletonDocument::batchChangeBegin); connect(graphicsWidget, &SkeletonGraphicsWidget::batchChangeEnd, m_document, &SkeletonDocument::batchChangeEnd); + connect(graphicsWidget, &SkeletonGraphicsWidget::changeTurnaround, this, &SkeletonDocumentWindow::changeTurnaround); + connect(graphicsWidget, &SkeletonGraphicsWidget::save, this, &SkeletonDocumentWindow::save); + connect(graphicsWidget, &SkeletonGraphicsWidget::open, this, &SkeletonDocumentWindow::open); + connect(graphicsWidget, &SkeletonGraphicsWidget::exportResult, this, &SkeletonDocumentWindow::exportResult); + connect(m_document, &SkeletonDocument::nodeAdded, graphicsWidget, &SkeletonGraphicsWidget::nodeAdded); connect(m_document, &SkeletonDocument::nodeRemoved, graphicsWidget, &SkeletonGraphicsWidget::nodeRemoved); connect(m_document, &SkeletonDocument::edgeAdded, graphicsWidget, &SkeletonGraphicsWidget::edgeAdded); @@ -157,12 +361,14 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() : connect(m_document, &SkeletonDocument::nodeRadiusChanged, graphicsWidget, &SkeletonGraphicsWidget::nodeRadiusChanged); connect(m_document, &SkeletonDocument::nodeOriginChanged, graphicsWidget, &SkeletonGraphicsWidget::nodeOriginChanged); connect(m_document, &SkeletonDocument::partVisibleStateChanged, graphicsWidget, &SkeletonGraphicsWidget::partVisibleStateChanged); + connect(m_document, &SkeletonDocument::cleanup, graphicsWidget, &SkeletonGraphicsWidget::removeAllContent); connect(m_document, &SkeletonDocument::partListChanged, partListWidget, &SkeletonPartListWidget::partListChanged); connect(m_document, &SkeletonDocument::partPreviewChanged, partListWidget, &SkeletonPartListWidget::partPreviewChanged); connect(m_document, &SkeletonDocument::partLockStateChanged, partListWidget, &SkeletonPartListWidget::partLockStateChanged); connect(m_document, &SkeletonDocument::partVisibleStateChanged, partListWidget, &SkeletonPartListWidget::partVisibleStateChanged); connect(m_document, &SkeletonDocument::partSubdivStateChanged, partListWidget, &SkeletonPartListWidget::partSubdivStateChanged); + connect(m_document, &SkeletonDocument::cleanup, partListWidget, &SkeletonPartListWidget::partListChanged); connect(m_document, &SkeletonDocument::skeletonChanged, m_document, &SkeletonDocument::generateMesh); @@ -174,14 +380,118 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() : m_modelWidget->setCursor(graphicsWidget->cursor()); }); - connect(this, &SkeletonDocumentWindow::initialized, m_document, [=]() { - m_document->saveSnapshot(); - graphicsWidget->setFocus(); - }); + connect(m_document, &SkeletonDocument::skeletonChanged, this, &SkeletonDocumentWindow::documentChanged); + connect(m_document, &SkeletonDocument::turnaroundChanged, this, &SkeletonDocumentWindow::documentChanged); connect(this, &SkeletonDocumentWindow::initialized, m_document, &SkeletonDocument::uiReady); } +SkeletonDocumentWindow *SkeletonDocumentWindow::createDocumentWindow() +{ + SkeletonDocumentWindow *documentWindow = new SkeletonDocumentWindow(); + documentWindow->setAttribute(Qt::WA_DeleteOnClose); + documentWindow->showMaximized(); + return documentWindow; +} + +void SkeletonDocumentWindow::closeEvent(QCloseEvent *event) +{ + if (m_documentSaved) { + event->accept(); + return; + } + + QMessageBox::StandardButton answer = QMessageBox::question(this, + APP_NAME, + tr("Do you really want to close while there are unsaved changes?"), + QMessageBox::Yes | QMessageBox::No); + if (answer == QMessageBox::Yes) + event->accept(); + else + event->ignore(); +} + +void SkeletonDocumentWindow::setCurrentFilename(const QString &filename) +{ + m_currentFilename = filename; + m_documentSaved = true; + updateTitle(); +} + +void SkeletonDocumentWindow::updateTitle() +{ + QString appName = APP_NAME; + QString appVer = APP_HUMAN_VER; + setWindowTitle(QString("%1 %2 %3%4").arg(appName).arg(appVer).arg(m_currentFilename).arg(m_documentSaved ? "" : "*")); +} + +void SkeletonDocumentWindow::documentChanged() +{ + if (m_documentSaved) { + m_documentSaved = false; + updateTitle(); + } +} + +void SkeletonDocumentWindow::newWindow() +{ + SkeletonDocumentWindow::createDocumentWindow(); +} + +void SkeletonDocumentWindow::newDocument() +{ + if (!m_documentSaved) { + QMessageBox::StandardButton answer = QMessageBox::question(this, + APP_NAME, + tr("Do you really want to create new document and lose the unsaved changes?"), + QMessageBox::Yes | QMessageBox::No); + if (answer != QMessageBox::Yes) + return; + } + m_document->reset(); +} + +void SkeletonDocumentWindow::saveAs() +{ + QString filename = QFileDialog::getSaveFileName(this, QString(), QString(), + tr("Dust3D Document (*.ds3)")); + if (filename.isEmpty()) { + return; + } + saveTo(filename); +} + +void SkeletonDocumentWindow::saveAll() +{ + for (auto &window: g_documentWindows) { + window->save(); + } +} + +void SkeletonDocumentWindow::viewSource() +{ + QString url = APP_REPOSITORY_URL; + qDebug() << "viewSource:" << url; + QDesktopServices::openUrl(QUrl(url)); +} + +void SkeletonDocumentWindow::about() +{ + SkeletonDocumentWindow::showAbout(); +} + +void SkeletonDocumentWindow::reportIssues() +{ + QString url = APP_ISSUES_URL; + qDebug() << "reportIssues:" << url; + QDesktopServices::openUrl(QUrl(url)); +} + +void SkeletonDocumentWindow::seeAcknowlegements() +{ + SkeletonDocumentWindow::showAcknowlegements(); +} + void SkeletonDocumentWindow::initButton(QPushButton *button) { button->setFont(Theme::awesome()->font(Theme::toolIconFontSize)); @@ -191,6 +501,7 @@ void SkeletonDocumentWindow::initButton(QPushButton *button) SkeletonDocumentWindow::~SkeletonDocumentWindow() { + g_documentWindows.erase(this); } void SkeletonDocumentWindow::showEvent(QShowEvent *event) @@ -198,14 +509,16 @@ void SkeletonDocumentWindow::showEvent(QShowEvent *event) QMainWindow::showEvent(event); if (m_firstShow) { m_firstShow = false; + updateTitle(); + m_document->saveSnapshot(); + m_graphicsWidget->setFocus(); emit initialized(); } } void SkeletonDocumentWindow::changeTurnaround() { - QString fileName = QFileDialog::getOpenFileName(this, tr("Open Turnaround Reference Image"), - QString(), + QString fileName = QFileDialog::getOpenFileName(this, QString(), QString(), tr("Image Files (*.png *.jpg *.bmp)")).trimmed(); if (fileName.isEmpty()) return; @@ -214,3 +527,102 @@ void SkeletonDocumentWindow::changeTurnaround() return; m_document->updateTurnaround(image); } + +void SkeletonDocumentWindow::save() +{ + saveTo(m_currentFilename); +} + +void SkeletonDocumentWindow::saveTo(const QString &saveAsFilename) +{ + QString filename = saveAsFilename; + + if (filename.isEmpty()) { + filename = QFileDialog::getSaveFileName(this, QString(), QString(), + tr("Dust3D Document (*.ds3)")); + if (filename.isEmpty()) { + return; + } + } + + QApplication::setOverrideCursor(Qt::WaitCursor); + + Ds3FileWriter ds3Writer; + + QByteArray modelXml; + QXmlStreamWriter stream(&modelXml); + SkeletonSnapshot snapshot; + m_document->toSnapshot(&snapshot); + saveSkeletonToXmlStream(&snapshot, &stream); + if (modelXml.size() > 0) + ds3Writer.add("model.xml", "model", &modelXml); + + QByteArray imageByteArray; + QBuffer pngBuffer(&imageByteArray); + if (!m_document->turnaround.isNull()) { + pngBuffer.open(QIODevice::WriteOnly); + m_document->turnaround.save(&pngBuffer, "PNG"); + if (imageByteArray.size() > 0) + ds3Writer.add("canvas.png", "asset", &imageByteArray); + } + + if (ds3Writer.save(filename)) { + setCurrentFilename(filename); + } + + QApplication::restoreOverrideCursor(); +} + +void SkeletonDocumentWindow::open() +{ + if (!m_documentSaved) { + QMessageBox::StandardButton answer = QMessageBox::question(this, + APP_NAME, + tr("Do you really want to open another file and lose the unsaved changes?"), + QMessageBox::Yes | QMessageBox::No); + if (answer != QMessageBox::Yes) + return; + } + + QString filename = QFileDialog::getOpenFileName(this, QString(), QString(), + tr("Dust3D Document (*.ds3)")); + if (filename.isEmpty()) + return; + + QApplication::setOverrideCursor(Qt::WaitCursor); + Ds3FileReader ds3Reader(filename); + for (int i = 0; i < ds3Reader.items().size(); ++i) { + Ds3ReaderItem item = ds3Reader.items().at(i); + if (item.type == "model") { + QByteArray data; + ds3Reader.loadItem(item.name, &data); + QXmlStreamReader stream(data); + SkeletonSnapshot snapshot; + loadSkeletonFromXmlStream(&snapshot, stream); + m_document->fromSnapshot(snapshot); + } else if (item.type == "asset") { + if (item.name == "canvas.png") { + QByteArray data; + ds3Reader.loadItem(item.name, &data); + QImage image = QImage::fromData(data, "PNG"); + m_document->updateTurnaround(image); + } + } + } + QApplication::restoreOverrideCursor(); + + setCurrentFilename(filename); +} + +void SkeletonDocumentWindow::exportResult() +{ + QString filename = QFileDialog::getSaveFileName(this, QString(), QString(), + tr("Wavefront (*.obj)")); + if (filename.isEmpty()) { + return; + } + QApplication::setOverrideCursor(Qt::WaitCursor); + m_modelWidget->exportMeshAsObj(filename); + QApplication::restoreOverrideCursor(); +} + diff --git a/src/skeletondocumentwindow.h b/src/skeletondocumentwindow.h index 3581fee9..1aefb72a 100644 --- a/src/skeletondocumentwindow.h +++ b/src/skeletondocumentwindow.h @@ -3,9 +3,15 @@ #include #include #include +#include +#include +#include +#include #include "skeletondocument.h" #include "modelwidget.h" +class SkeletonGraphicsWidget; + class SkeletonDocumentWindow : public QMainWindow { Q_OBJECT @@ -14,16 +20,72 @@ signals: public: SkeletonDocumentWindow(); ~SkeletonDocumentWindow(); + static SkeletonDocumentWindow *createDocumentWindow(); + static void showAcknowlegements(); + static void showAbout(); protected: void showEvent(QShowEvent *event); + void closeEvent(QCloseEvent *event); public slots: void changeTurnaround(); + void save(); + void saveTo(const QString &saveAsFilename); + void open(); + void exportResult(); + void newWindow(); + void newDocument(); + void saveAs(); + void saveAll(); + void viewSource(); + void about(); + void reportIssues(); + void seeAcknowlegements(); + void documentChanged(); private: void initButton(QPushButton *button); + void setCurrentFilename(const QString &filename); + void updateTitle(); private: SkeletonDocument *m_document; bool m_firstShow; ModelWidget *m_modelWidget; + SkeletonGraphicsWidget *m_graphicsWidget; + QString m_currentFilename; + bool m_documentSaved; + + QMenu *m_fileMenu; + QAction *m_newWindowAction; + QAction *m_newDocumentAction; + QAction *m_openAction; + QAction *m_saveAction; + QAction *m_saveAsAction; + QAction *m_saveAllAction; + QAction *m_exportAction; + QAction *m_changeTurnaroundAction; + + QMenu *m_editMenu; + QAction *m_addAction; + QAction *m_undoAction; + QAction *m_redoAction; + QAction *m_deleteAction; + QAction *m_cutAction; + QAction *m_copyAction; + QAction *m_pasteAction; + QAction *m_flipHorizontallyAction; + QAction *m_flipVerticallyAction; + QAction *m_selectAllAction; + QAction *m_selectPartAllAction; + QAction *m_unselectAllAction; + + QMenu *m_viewMenu; + QAction *m_resetModelWidgetPosAction; + QAction *m_showDebugDialogAction; + + QMenu *m_helpMenu; + QAction *m_viewSourceAction; + QAction *m_aboutAction; + QAction *m_reportIssuesAction; + QAction *m_seeAcknowlegementsAction; }; #endif diff --git a/src/skeletongraphicswidget.cpp b/src/skeletongraphicswidget.cpp index 5ca43861..c385dbfc 100644 --- a/src/skeletongraphicswidget.cpp +++ b/src/skeletongraphicswidget.cpp @@ -46,10 +46,12 @@ SkeletonGraphicsWidget::SkeletonGraphicsWidget(const SkeletonDocument *document) m_cursorNodeItem = new SkeletonGraphicsNodeItem(); m_cursorNodeItem->hide(); + m_cursorNodeItem->setData(0, "cursorNode"); scene()->addItem(m_cursorNodeItem); m_cursorEdgeItem = new SkeletonGraphicsEdgeItem(); m_cursorEdgeItem->hide(); + m_cursorEdgeItem->setData(0, "cursorEdge"); scene()->addItem(m_cursorEdgeItem); m_selectionItem = new SkeletonGraphicsSelectionItem(); @@ -77,7 +79,7 @@ void SkeletonGraphicsWidget::showContextMenu(const QPoint &pos) QMenu contextMenu(this); - QAction addAction(tr("Add.."), this); + QAction addAction(tr("Add..."), this); connect(&addAction, &QAction::triggered, [=]() { emit setEditMode(SkeletonDocumentEditMode::Add); }); @@ -96,19 +98,19 @@ void SkeletonGraphicsWidget::showContextMenu(const QPoint &pos) } QAction deleteAction(tr("Delete"), this); - if (!m_rangeSelectionSet.empty()) { + if (hasSelection()) { connect(&deleteAction, &QAction::triggered, this, &SkeletonGraphicsWidget::deleteSelected); contextMenu.addAction(&deleteAction); } QAction cutAction(tr("Cut"), this); - if (!m_rangeSelectionSet.empty()) { + if (hasSelection()) { connect(&cutAction, &QAction::triggered, this, &SkeletonGraphicsWidget::cut); contextMenu.addAction(&cutAction); } QAction copyAction(tr("Copy"), this); - if (!m_rangeSelectionSet.empty()) { + if (hasSelection()) { connect(©Action, &QAction::triggered, this, &SkeletonGraphicsWidget::copy); contextMenu.addAction(©Action); } @@ -120,38 +122,44 @@ void SkeletonGraphicsWidget::showContextMenu(const QPoint &pos) } QAction flipHorizontallyAction(tr("H Flip"), this); - if (!m_rangeSelectionSet.empty() && !isSingleNodeSelected()) { + if (hasMultipleSelection()) { connect(&flipHorizontallyAction, &QAction::triggered, this, &SkeletonGraphicsWidget::flipHorizontally); contextMenu.addAction(&flipHorizontallyAction); } QAction flipVerticallyAction(tr("V Flip"), this); - if (!m_rangeSelectionSet.empty() && !isSingleNodeSelected()) { + if (hasMultipleSelection()) { connect(&flipVerticallyAction, &QAction::triggered, this, &SkeletonGraphicsWidget::flipVertically); contextMenu.addAction(&flipVerticallyAction); } QAction selectAllAction(tr("Select All"), this); - if (!nodeItemMap.empty()) { + if (hasItems()) { connect(&selectAllAction, &QAction::triggered, this, &SkeletonGraphicsWidget::selectAll); contextMenu.addAction(&selectAllAction); } QAction selectPartAllAction(tr("Select Part"), this); - if (!nodeItemMap.empty()) { + if (hasItems()) { connect(&selectPartAllAction, &QAction::triggered, this, &SkeletonGraphicsWidget::selectPartAll); contextMenu.addAction(&selectPartAllAction); } QAction unselectAllAction(tr("Unselect All"), this); - if (!m_rangeSelectionSet.empty()) { + if (hasSelection()) { connect(&unselectAllAction, &QAction::triggered, this, &SkeletonGraphicsWidget::unselectAll); contextMenu.addAction(&unselectAllAction); } contextMenu.addSeparator(); - QAction changeTurnaroundAction(tr("Change Turnaround.."), this); + QAction exportResultAction(tr("Export..."), this); + if (hasItems()) { + connect(&exportResultAction, &QAction::triggered, this, &SkeletonGraphicsWidget::exportResult); + contextMenu.addAction(&exportResultAction); + } + + QAction changeTurnaroundAction(tr("Change Turnaround..."), this); connect(&changeTurnaroundAction, &QAction::triggered, [=]() { emit changeTurnaround(); }); @@ -160,6 +168,21 @@ void SkeletonGraphicsWidget::showContextMenu(const QPoint &pos) contextMenu.exec(mapToGlobal(pos)); } +bool SkeletonGraphicsWidget::hasSelection() +{ + return !m_rangeSelectionSet.empty(); +} + +bool SkeletonGraphicsWidget::hasItems() +{ + return !nodeItemMap.empty(); +} + +bool SkeletonGraphicsWidget::hasMultipleSelection() +{ + return !m_rangeSelectionSet.empty() && !isSingleNodeSelected(); +} + void SkeletonGraphicsWidget::updateItems() { for (auto nodeItemIt = nodeItemMap.begin(); nodeItemIt != nodeItemMap.end(); nodeItemIt++) { @@ -669,6 +692,10 @@ bool SkeletonGraphicsWidget::mouseDoubleClick(QMouseEvent *event) selectPartAll(); return true; } + if (QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ControlModifier)) { + emit open(); + return true; + } return false; } @@ -734,6 +761,10 @@ bool SkeletonGraphicsWidget::keyPress(QKeyEvent *event) if (QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ControlModifier)) { emit paste(); } + } else if (event->key() == Qt::Key_S) { + if (QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ControlModifier)) { + emit save(); + } } return false; } @@ -1153,6 +1184,33 @@ void SkeletonGraphicsWidget::copy() clipboard->setText(snapshotXml); } +void SkeletonGraphicsWidget::removeAllContent() +{ + nodeItemMap.clear(); + edgeItemMap.clear(); + m_rangeSelectionSet.clear(); + m_hoveredEdgeItem = nullptr; + m_hoveredNodeItem = nullptr; + m_addFromNodeItem = nullptr; + m_cursorEdgeItem->hide(); + m_cursorNodeItem->hide(); + std::set contentItems; + for (const auto &it: items()) { + QGraphicsItem *item = it; + if (item->data(0) == "node") { + contentItems.insert(item); + } else if (item->data(0) == "edge") { + contentItems.insert(item); + } + } + for (const auto &it: contentItems) { + QGraphicsItem *item = it; + Q_ASSERT(item != m_cursorEdgeItem); + Q_ASSERT(item != m_cursorNodeItem); + scene()->removeItem(item); + } +} + bool SkeletonGraphicsWidget::isSingleNodeSelected() { if (m_rangeSelectionSet.size() != 1) diff --git a/src/skeletongraphicswidget.h b/src/skeletongraphicswidget.h index 1cc74409..a88b5521 100644 --- a/src/skeletongraphicswidget.h +++ b/src/skeletongraphicswidget.h @@ -266,6 +266,9 @@ signals: void changeTurnaround(); void batchChangeBegin(); void batchChangeEnd(); + void save(); + void open(); + void exportResult(); public: SkeletonGraphicsWidget(const SkeletonDocument *document); std::map> nodeItemMap; @@ -281,6 +284,9 @@ public: void readMergedSkeletonNodeSetFromRangeSelection(std::set *nodeItemSet); void readSkeletonNodeAndEdgeIdSetFromRangeSelection(std::set *nodeIdSet, std::set *edgeIdSet); bool readSkeletonNodeAndAnyEdgeOfNodeFromRangeSelection(SkeletonGraphicsNodeItem **nodeItem, SkeletonGraphicsEdgeItem **edgeItem); + bool hasSelection(); + bool hasItems(); + bool hasMultipleSelection(); protected: void mouseMoveEvent(QMouseEvent *event); void wheelEvent(QWheelEvent *event); @@ -310,6 +316,7 @@ public slots: void copy(); void flipHorizontally(); void flipVertically(); + void removeAllContent(); private slots: void turnaroundImageReady(); private: diff --git a/src/skeletonsnapshot.cpp b/src/skeletonsnapshot.cpp index c829b64e..872c1717 100644 --- a/src/skeletonsnapshot.cpp +++ b/src/skeletonsnapshot.cpp @@ -1,282 +1,2 @@ #include "skeletonsnapshot.h" #include - -SkeletonSnapshot::SkeletonSnapshot() : - m_boundingBoxResolved(false), - m_rootNodeResolved(false) -{ -} - -void joinNodeAndNeighborsToGroup(std::map> &nodeLinkMap, - std::map &nodeGroupMap, - const QString &nodeId, - int groupId, - std::vector &group) -{ - group.push_back(nodeId); - nodeGroupMap[nodeId] = groupId; - std::vector *neighbors = &nodeLinkMap[nodeId]; - for (size_t i = 0; i < neighbors->size(); i++) { - QString neighborNodeId = (*neighbors)[i]; - if (nodeGroupMap.find(neighborNodeId) != nodeGroupMap.end()) - continue; - nodeGroupMap[neighborNodeId] = groupId; - joinNodeAndNeighborsToGroup(nodeLinkMap, - nodeGroupMap, - neighborNodeId, - groupId, - group); - } -} - -void SkeletonSnapshot::splitByConnectivity(std::vector *groups) -{ - std::map> nodeLinkMap; - std::map>::iterator edgeIterator; - std::map neighborCountMap; - for (edgeIterator = edges.begin(); edgeIterator != edges.end(); edgeIterator++) { - neighborCountMap[edgeIterator->second["from"]]++; - neighborCountMap[edgeIterator->second["to"]]++; - } - int nextNewXnodeId = 1; - std::vector> newPendingEdges; - for (edgeIterator = edges.begin(); edgeIterator != edges.end(); ) { - std::map *oneNode = &nodes[edgeIterator->second["from"]]; - std::map *linkNode = &nodes[edgeIterator->second["to"]]; - if ("true" != (*oneNode)["isBranch"]) { - oneNode = &nodes[edgeIterator->second["to"]]; - linkNode = &nodes[edgeIterator->second["from"]]; - if ("true" != (*oneNode)["isBranch"]) { - edgeIterator++; - continue; - } else { - if (neighborCountMap[(*linkNode)["id"]] < 3) { - edgeIterator++; - continue; - } - } - } else { - if (neighborCountMap[(*linkNode)["id"]] < 3) { - edgeIterator++; - continue; - } - } - - if ("red" == (*oneNode)["sideColorName"]) { - QString nodeId = QString("nodex%1").arg(nextNewXnodeId++); - std::map *newNode = &nodes[nodeId]; - *newNode = *linkNode; - (*newNode)["id"] = nodeId; - (*newNode)["radius"] = QString("%1").arg((*newNode)["radius"].toFloat() / 5); - - std::map *pairNode = &nodes[(*oneNode)["nextSidePair"]]; - QString pairNodeId = QString("nodex%1").arg(nextNewXnodeId++); - std::map *newPairNode = &nodes[pairNodeId]; - *newPairNode = *pairNode; - (*newPairNode)["id"] = pairNodeId; - (*newPairNode)["radius"] = QString("%1").arg((*newPairNode)["radius"].toFloat() / 5); - - (*newNode)["nextSidePair"] = pairNodeId; - (*newPairNode)["nextSidePair"] = nodeId; - - newPendingEdges.push_back(std::make_pair((*oneNode)["id"], nodeId)); - } - - edgeIterator = edges.erase(edgeIterator); - } - int nextNewXedgeId = 1; - for (size_t i = 0; i < newPendingEdges.size(); i++) { - QString edgeId = QString("edgex%1").arg(nextNewXedgeId++); - std::map *newEdge = &edges[edgeId]; - (*newEdge)["id"] = edgeId; - (*newEdge)["from"] = newPendingEdges[i].first; - (*newEdge)["to"] = newPendingEdges[i].second; - - edgeId = QString("edgex%1").arg(nextNewXedgeId++); - newEdge = &edges[edgeId]; - (*newEdge)["id"] = edgeId; - (*newEdge)["from"] = nodes[newPendingEdges[i].first]["nextSidePair"]; - (*newEdge)["to"] = nodes[newPendingEdges[i].second]["nextSidePair"]; - } - for (edgeIterator = edges.begin(); edgeIterator != edges.end(); edgeIterator++) { - nodeLinkMap[edgeIterator->second["from"]].push_back(edgeIterator->second["to"]); - nodeLinkMap[edgeIterator->second["to"]].push_back(edgeIterator->second["from"]); - } - std::map>::iterator nodeIterator; - for (nodeIterator = nodes.begin(); nodeIterator != nodes.end(); nodeIterator++) { - nodeLinkMap[nodeIterator->first].push_back(nodeIterator->second["nextSidePair"]); - } - std::map nodeGroupMap; - std::vector> nameGroups; - for (nodeIterator = nodes.begin(); nodeIterator != nodes.end(); nodeIterator++) { - if (nodeGroupMap.find(nodeIterator->first) != nodeGroupMap.end()) - continue; - std::vector nameGroup; - joinNodeAndNeighborsToGroup(nodeLinkMap, - nodeGroupMap, - nodeIterator->first, - nameGroups.size(), - nameGroup); - for (size_t i = 0; i < nameGroup.size(); i++) { - nodeGroupMap[nameGroup[i]] = nameGroups.size(); - } - nameGroups.push_back(nameGroup); - } - groups->resize(nameGroups.size()); - for (edgeIterator = edges.begin(); edgeIterator != edges.end(); edgeIterator++) { - int groupIndex = nodeGroupMap[edgeIterator->second["from"]]; - (*groups)[groupIndex].edges[edgeIterator->first] = edgeIterator->second; - } - for (nodeIterator = nodes.begin(); nodeIterator != nodes.end(); nodeIterator++) { - int groupIndex = nodeGroupMap[nodeIterator->first]; - (*groups)[groupIndex].nodes[nodeIterator->first] = nodeIterator->second; - } - for (size_t i = 0; i < groups->size(); i++) { - (*groups)[i].canvas = canvas; - } -} - -const QRectF &SkeletonSnapshot::boundingBoxFront() -{ - if (!m_boundingBoxResolved) - resolveBoundingBox(); - return m_boundingBoxFront; -} - -const QRectF &SkeletonSnapshot::boundingBoxSide() -{ - if (!m_boundingBoxResolved) - resolveBoundingBox(); - return m_boundingBoxSide; -} - -void SkeletonSnapshot::resolveBoundingBox() -{ - if (m_boundingBoxResolved) - return; - m_boundingBoxResolved = true; - - float left = -1; - float right = -1; - float top = -1; - float bottom = -1; - float zLeft = -1; - float zRight = -1; - std::map>::iterator nodeIterator; - for (nodeIterator = nodes.begin(); nodeIterator != nodes.end(); nodeIterator++) { - printf("loop node:%s color: %s\n", nodeIterator->first.toUtf8().constData(), nodeIterator->second["sideColorName"].toUtf8().constData()); - } - for (nodeIterator = nodes.begin(); nodeIterator != nodes.end(); nodeIterator++) { - if ("red" != nodeIterator->second["sideColorName"]) - continue; - float originX = nodeIterator->second["x"].toFloat(); - float originY = nodeIterator->second["y"].toFloat(); - float originZ = 0; - QString nextSidePairId = nodeIterator->second["nextSidePair"]; - printf("nextSidePair: %s\n", nextSidePairId.toUtf8().constData()); - std::map>::iterator findNextSidePair = nodes.find(nextSidePairId); - if (findNextSidePair != nodes.end()) { - originZ = findNextSidePair->second["x"].toFloat(); - } - if (left < 0 || originX < left) { - left = originX; - } - if (top < 0 || originY < top) { - top = originY; - } - if (originX > right) { - right = originX; - } - if (originY > bottom) { - bottom = originY; - } - if (zLeft < 0 || originZ < zLeft) { - zLeft = originZ; - } - if (originZ > zRight) { - zRight = originZ; - } - } - - m_boundingBoxFront = QRectF(left, top, right - left, bottom - top); - m_boundingBoxSide = QRectF(zLeft, top, zRight - zLeft, bottom - top); -} - -QString SkeletonSnapshot::rootNode() -{ - if (!m_rootNodeResolved) - resolveRootNode(); - return m_rootNode; -} - -void SkeletonSnapshot::setRootNode(const QString &nodeName) -{ - assert(!nodeName.isEmpty()); - m_rootNode = nodeName; - m_rootNodeResolved = true; -} - -QString SkeletonSnapshot::findMaxNeighborNumberNode() -{ - std::map>::iterator edgeIterator; - std::map nodeNeighborCountMap; - for (edgeIterator = edges.begin(); edgeIterator != edges.end(); edgeIterator++) { - if ("red" != nodes[edgeIterator->second["from"]]["sideColorName"]) - continue; - nodeNeighborCountMap[edgeIterator->second["from"]]++; - nodeNeighborCountMap[edgeIterator->second["to"]]++; - } - if (nodeNeighborCountMap.size() == 0) - return ""; - auto x = std::max_element(nodeNeighborCountMap.begin(), nodeNeighborCountMap.end(), - [](const std::pair& p1, const std::pair& p2) { - return p1.second < p2.second; - }); - return x->first; -} - -void SkeletonSnapshot::resolveRootNode() -{ - if (m_rootNodeResolved) - return; - m_rootNodeResolved = true; - - std::map>::iterator edgeIterator; - std::map nodeNeighborCountMap; - for (edgeIterator = edges.begin(); edgeIterator != edges.end(); edgeIterator++) { - if ("red" != nodes[edgeIterator->second["from"]]["sideColorName"]) - continue; - nodeNeighborCountMap[edgeIterator->second["from"]]++; - nodeNeighborCountMap[edgeIterator->second["to"]]++; - } - std::map>::iterator nodeIterator; - - // First try to select the node with more than 2 neighbors and have the largest radius. - float maxRadius = 0; - m_rootNode = ""; - for (nodeIterator = nodes.begin(); nodeIterator != nodes.end(); nodeIterator++) { - if ("red" != nodeIterator->second["sideColorName"]) - continue; - if ("true" == nodeIterator->second["isRoot"]) { - m_rootNode = nodeIterator->first; - break; - } - if (nodeNeighborCountMap[nodeIterator->first] < 3) - continue; - float radius = nodeIterator->second["radius"].toFloat(); - if (radius > maxRadius) { - maxRadius = radius; - m_rootNode = nodeIterator->first; - } - } - - if (m_rootNode.isEmpty()) - m_rootNode = findMaxNeighborNumberNode(); - - if (m_rootNode.isEmpty()) { - nodeIterator = nodes.begin(); - if (nodeIterator != nodes.end()) { - m_rootNode = nodeIterator->first; - } - } -} diff --git a/src/skeletonsnapshot.h b/src/skeletonsnapshot.h index f6e5b850..154984f6 100644 --- a/src/skeletonsnapshot.h +++ b/src/skeletonsnapshot.h @@ -13,26 +13,7 @@ public: std::map> nodes; std::map> edges; std::map> parts; - std::map> mirrors; - std::map> trivialMarkers; std::vector partIdList; -public: - SkeletonSnapshot(); - void splitByConnectivity(std::vector *groups); - QString rootNode(); - void setRootNode(const QString &nodeName); - const QRectF &boundingBoxFront(); - const QRectF &boundingBoxSide(); -private: - QString m_rootNode; - QRectF m_boundingBoxFront; - QRectF m_boundingBoxSide; - bool m_boundingBoxResolved; - bool m_rootNodeResolved; -private: - void resolveBoundingBox(); - void resolveRootNode(); - QString findMaxNeighborNumberNode(); }; #endif